Skip to content

Commit

Permalink
Download blob: remove downloadjs dependency (#56024)
Browse files Browse the repository at this point in the history
* Create a new function in the blob package: downloadBlob
Removes downloadjs dependency
Adds tests

* Use new function in reusable blocks package

Whoops

* Update package json deps

* - fileName var all to lower case
- changed a var to anchorElement
- regenerate docs

* Removing HTMLElement return value and updating tests
Updated docs

* Updated test description

* Set a default value of `''` for contentType, which matches the Web API spec for Blob
Updates tests
  • Loading branch information
ramonjd authored Nov 14, 2023
1 parent c64d93b commit 8850bfb
Show file tree
Hide file tree
Showing 11 changed files with 139 additions and 68 deletions.
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 } );
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,
};
} );

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

1 comment on commit 8850bfb

@github-actions
Copy link

Choose a reason for hiding this comment

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

Flaky tests detected in 8850bfb.
Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/6869832532
📝 Reported issues:

Please sign in to comment.