Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Download blob: remove downloadjs dependency #56024

Merged
merged 7 commits into from
Nov 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 4 additions & 12 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions packages/blob/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### New feature

- Add `downloadBlob` function and remove `downloadjs` dependency ([#56024](https://github.com/WordPress/gutenberg/pull/56024)).

## 3.45.0 (2023-11-02)

## 3.44.0 (2023-10-18)
Expand Down
25 changes: 25 additions & 0 deletions packages/blob/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,31 @@ _Returns_

- `string`: The blob URL.

### downloadBlob

Downloads a file, e.g., a text or readable stream, in the browser. Appropriate for downloading smaller file sizes, e.g., \< 5 MB.

Example usage:

```js
const fileContent = JSON.stringify(
{
title: 'My Post',
},
null,
2
);
const fileName = 'file.json';

downloadBlob( 'file.json', fileContent, 'application/json' );
```

_Parameters_

- _filename_ `string`: File name.
- _content_ `BlobPart`: File content (BufferSource | Blob | string).
- _contentType_ `string`: (Optional) File mime type. Default is `''`.

### getBlobByURL

Retrieve a file based on a blob URL. The file must have been created by `createBlobURL` and not removed by `revokeBlobURL`, otherwise it will return `undefined`.
Expand Down
40 changes: 40 additions & 0 deletions packages/blob/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,43 @@ export function isBlobURL( url ) {
}
return url.indexOf( 'blob:' ) === 0;
}

/**
* Downloads a file, e.g., a text or readable stream, in the browser.
* Appropriate for downloading smaller file sizes, e.g., < 5 MB.
*
* Example usage:
*
* ```js
* const fileContent = JSON.stringify(
* {
* "title": "My Post",
* },
* null,
* 2
* );
* const fileName = 'file.json';
*
* downloadBlob( 'file.json', fileContent, 'application/json' );
* ```
*
* @param {string} filename File name.
* @param {BlobPart} content File content (BufferSource | Blob | string).
* @param {string} contentType (Optional) File mime type. Default is `''`.
*/
export function downloadBlob( filename, content, contentType = '' ) {
if ( ! filename || ! content ) {
return;
}

const file = new window.Blob( [ content ], { type: contentType } );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add some validation to the parameters passed?

Should at least a default mime type be defined in case nothing is passed? Will it work correctly if you pass the incorrect content type?

It's going to be a general purpose util available also outside the WordPress context.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add some validation to the parameters passed?

Yes, good idea. Thanks!

Should at least a default mime type be defined in case nothing is passed? Will it work correctly if you pass the incorrect content type?

I'll test this. Maybe text/plain could work.

It might be that a default mime-type might be incompatible with content<any> 🤔 so in that case we could require all the args.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will it work correctly if you pass the incorrect content type?

I've tested with a fallback content type of text/plain and things like zips and images are still downloadable, and the browser somehow works it out.

However, type is an optional parameter and the default value is "", so I think we can can fallback to "" and require the other args.

const url = window.URL.createObjectURL( file );
const anchorElement = document.createElement( 'a' );
anchorElement.href = url;
anchorElement.download = filename;
anchorElement.style.display = 'none';
document.body.appendChild( anchorElement );
anchorElement.click();
document.body.removeChild( anchorElement );
window.URL.revokeObjectURL( url );
}
59 changes: 58 additions & 1 deletion packages/blob/src/test/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Internal dependencies
*/
import { isBlobURL, getBlobTypeByURL } from '../';
import { isBlobURL, getBlobTypeByURL, downloadBlob } from '../';

describe( 'isBlobURL', () => {
it( 'returns true if the url starts with "blob:"', () => {
Expand All @@ -26,3 +26,60 @@ describe( 'getBlobTypeByURL', () => {
expect( getBlobTypeByURL() ).toBe( undefined );
} );
} );

describe( 'downloadBlob', () => {
const originalURL = window.URL;
const createObjectURL = jest.fn().mockReturnValue( 'blob:pannacotta' );
const revokeObjectURL = jest.fn().mockReturnValue( false );
const mockAnchorElement = document.createElement( 'a' );
mockAnchorElement.click = jest.fn();
const createElementSpy = jest
.spyOn( global.document, 'createElement' )
.mockReturnValue( mockAnchorElement );
const mockBlob = jest.fn();
const blobSpy = jest.spyOn( window, 'Blob' ).mockReturnValue( mockBlob );
jest.spyOn( document.body, 'appendChild' );
jest.spyOn( document.body, 'removeChild' );
beforeEach( () => {
// Can't seem to spy on these static methods. They are `undefined`.
// Possibly overwritten: https://github.com/WordPress/gutenberg/blob/trunk/packages/jest-preset-default/scripts/setup-globals.js#L5
window.URL = {
createObjectURL,
revokeObjectURL,
};
ramonjd marked this conversation as resolved.
Show resolved Hide resolved
} );

afterAll( () => {
window.URL = originalURL;
} );

it( 'requires a filename argument', () => {
downloadBlob( '', '{}', 'application/json' );
expect( blobSpy ).not.toHaveBeenCalled();
} );

it( 'requires a content argument', () => {
downloadBlob( 'text.txt', '', 'text/plain' );
expect( blobSpy ).not.toHaveBeenCalled();
} );

it( 'constructs an anchor element with attributes and removes it', () => {
downloadBlob( 'filename.json', '{}', 'application/json' );
expect( blobSpy ).toHaveBeenCalledWith( [ '{}' ], {
type: 'application/json',
} );
expect( createObjectURL ).toHaveBeenCalledWith( mockBlob );
expect( createElementSpy ).toHaveBeenCalledWith( 'a' );
expect( mockAnchorElement.download ).toBe( 'filename.json' );
expect( mockAnchorElement.href ).toBe( 'blob:pannacotta' );
expect( mockAnchorElement ).toHaveStyle( 'display:none' );
expect( document.body.appendChild ).toHaveBeenCalledWith(
mockAnchorElement
);
expect( mockAnchorElement.click ).toHaveBeenCalledTimes( 1 );
expect( document.body.removeChild ).toHaveBeenCalledWith(
mockAnchorElement
);
expect( revokeObjectURL ).toHaveBeenCalled();
} );
} );
2 changes: 1 addition & 1 deletion packages/edit-site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"@tanstack/react-table": "^8.10.3",
"@wordpress/a11y": "file:../a11y",
"@wordpress/api-fetch": "file:../api-fetch",
"@wordpress/blob": "file:../blob",
"@wordpress/block-editor": "file:../block-editor",
"@wordpress/block-library": "file:../block-library",
"@wordpress/blocks": "file:../blocks",
Expand Down Expand Up @@ -70,7 +71,6 @@
"classnames": "^2.3.1",
"colord": "^2.9.2",
"deepmerge": "^4.3.0",
"downloadjs": "^1.4.7",
"fast-deep-equal": "^3.1.3",
"is-plain-object": "^5.0.0",
"memize": "^2.1.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
/**
* External dependencies
*/
import downloadjs from 'downloadjs';

/**
* WordPress dependencies
*/
Expand All @@ -11,6 +6,7 @@ import { MenuItem } from '@wordpress/components';
import apiFetch from '@wordpress/api-fetch';
import { download } from '@wordpress/icons';
import { useDispatch } from '@wordpress/data';
import { downloadBlob } from '@wordpress/blob';
import { store as noticesStore } from '@wordpress/notices';

export default function SiteExport() {
Expand All @@ -35,7 +31,7 @@ export default function SiteExport() {
? contentDispositionMatches[ 1 ]
: 'edit-site-export';

downloadjs( blob, fileName + '.zip', 'application/zip' );
downloadBlob( fileName + '.zip', blob, 'application/zip' );
} catch ( errorResponse ) {
let error = {};
try {
Expand Down
22 changes: 2 additions & 20 deletions packages/edit-site/src/components/page-patterns/grid-item.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
} from '@wordpress/icons';
import { store as noticesStore } from '@wordpress/notices';
import { store as reusableBlocksStore } from '@wordpress/reusable-blocks';
import { downloadBlob } from '@wordpress/blob';

/**
* Internal dependencies
Expand All @@ -51,25 +52,6 @@ import { store as editSiteStore } from '../../store';
import { useLink } from '../routes/link';
import { unlock } from '../../lock-unlock';

/**
* Downloads a file.
* Also used in packages/list-reusable-blocks/src/utils/file.js.
*
* @param {string} fileName File Name.
* @param {string} content File Content.
* @param {string} contentType File mime type.
*/
function download( fileName, content, contentType ) {
const file = new window.Blob( [ content ], { type: contentType } );
const a = document.createElement( 'a' );
a.href = URL.createObjectURL( file );
a.download = fileName;
a.style.display = 'none';
document.body.appendChild( a );
a.click();
document.body.removeChild( a );
}

const { useGlobalStyle } = unlock( blockEditorPrivateApis );

const templatePartIcons = { header, footer, uncategorized };
Expand Down Expand Up @@ -136,7 +118,7 @@ function GridItem( { categoryId, item, ...props } ) {
syncStatus: item.patternBlock.wp_pattern_sync_status,
};

return download(
return downloadBlob(
`${ kebabCase( item.title || item.name ) }.json`,
JSON.stringify( json, null, 2 ),
'application/json'
Expand Down
1 change: 1 addition & 0 deletions packages/list-reusable-blocks/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"dependencies": {
"@babel/runtime": "^7.16.0",
"@wordpress/api-fetch": "file:../api-fetch",
"@wordpress/blob": "file:../blob",
"@wordpress/components": "file:../components",
"@wordpress/compose": "file:../compose",
"@wordpress/element": "file:../element",
Expand Down
4 changes: 2 additions & 2 deletions packages/list-reusable-blocks/src/utils/export.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import apiFetch from '@wordpress/api-fetch';
/**
* Internal dependencies
*/
import { download } from './file';
import { downloadBlob } from '@wordpress/blob';

/**
* Export a reusable block as a JSON file.
Expand All @@ -38,7 +38,7 @@ async function exportReusableBlock( id ) {
);
const fileName = kebabCase( title ) + '.json';

download( fileName, fileContent, 'application/json' );
downloadBlob( fileName, fileContent, 'application/json' );
}

export default exportReusableBlock;
26 changes: 0 additions & 26 deletions packages/list-reusable-blocks/src/utils/file.js
Original file line number Diff line number Diff line change
@@ -1,29 +1,3 @@
/**
* Downloads a file.
*
* @param {string} fileName File Name.
* @param {string} content File Content.
* @param {string} contentType File mime type.
*/
export function download( fileName, content, contentType ) {
const file = new window.Blob( [ content ], { type: contentType } );

// IE11 can't use the click to download technique
// we use a specific IE11 technique instead.
if ( window.navigator.msSaveOrOpenBlob ) {
window.navigator.msSaveOrOpenBlob( file, fileName );
} else {
const a = document.createElement( 'a' );
a.href = URL.createObjectURL( file );
a.download = fileName;

a.style.display = 'none';
document.body.appendChild( a );
a.click();
document.body.removeChild( a );
}
}

/**
* Reads the textual content of the given file.
*
Expand Down
Loading