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
ramonjd authored Nov 14, 2023

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent c64d93b commit 8850bfb
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
@@ -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)
25 changes: 25 additions & 0 deletions packages/blob/README.md
Original file line number Diff line number Diff line change
@@ -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`.
40 changes: 40 additions & 0 deletions packages/blob/src/index.js
Original file line number Diff line number Diff line change
@@ -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:"', () => {
@@ -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
@@ -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",
@@ -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",
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
/**
* External dependencies
*/
import downloadjs from 'downloadjs';

/**
* WordPress dependencies
*/
@@ -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() {
@@ -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 {
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
@@ -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
@@ -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 };
@@ -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'
1 change: 1 addition & 0 deletions packages/list-reusable-blocks/package.json
Original file line number Diff line number Diff line change
@@ -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",
4 changes: 2 additions & 2 deletions packages/list-reusable-blocks/src/utils/export.js
Original file line number Diff line number Diff line change
@@ -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.
@@ -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.
*

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.