diff --git a/packages/block-editor/CHANGELOG.md b/packages/block-editor/CHANGELOG.md index 18e5be254a4f03..e5c5a81e9daf92 100644 --- a/packages/block-editor/CHANGELOG.md +++ b/packages/block-editor/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- `MediaPlaceholder`: Remove the undocumented `onHTMLDrop` prop ([#49673](https://github.com/WordPress/gutenberg/pull/49673)). + ## 12.0.0 (2023-04-26) ### Breaking Changes diff --git a/packages/block-editor/src/components/block-draggable/index.js b/packages/block-editor/src/components/block-draggable/index.js index 40f61bfc79ce4d..02844e88993976 100644 --- a/packages/block-editor/src/components/block-draggable/index.js +++ b/packages/block-editor/src/components/block-draggable/index.js @@ -67,14 +67,18 @@ const BlockDraggable = ( { __experimentalTransferDataType="wp-blocks" transferData={ transferData } onDragStart={ ( event ) => { - startDraggingBlocks( clientIds ); - isDragging.current = true; + // Defer hiding the dragged source element to the next + // frame to enable dragging. + window.requestAnimationFrame( () => { + startDraggingBlocks( clientIds ); + isDragging.current = true; - startScrolling( event ); + startScrolling( event ); - if ( onDragStart ) { - onDragStart(); - } + if ( onDragStart ) { + onDragStart(); + } + } ); } } onDragOver={ scrollOnDragOver } onDragEnd={ () => { diff --git a/packages/block-editor/src/components/inserter-draggable-blocks/index.js b/packages/block-editor/src/components/inserter-draggable-blocks/index.js index c5cee77fa00255..0de9b3a2260f6b 100644 --- a/packages/block-editor/src/components/inserter-draggable-blocks/index.js +++ b/packages/block-editor/src/components/inserter-draggable-blocks/index.js @@ -2,6 +2,7 @@ * WordPress dependencies */ import { Draggable } from '@wordpress/components'; +import { serialize } from '@wordpress/blocks'; /** * Internal dependencies */ @@ -23,6 +24,9 @@ const InserterDraggableBlocks = ( { { + event.dataTransfer.setData( 'text/html', serialize( blocks ) ); + } } __experimentalDragComponent={ { const { getSettings } = select( blockEditorStore ); return getSettings().mediaUpload; @@ -177,6 +186,70 @@ export function MediaPlaceholder( { } ); }; + async function handleBlocksDrop( blocks ) { + if ( ! blocks || ! Array.isArray( blocks ) ) { + return; + } + + function recursivelyFindMediaFromBlocks( _blocks ) { + return _blocks.flatMap( ( block ) => + ( block.name === 'core/image' || + block.name === 'core/audio' || + block.name === 'core/video' ) && + block.attributes.url + ? [ block ] + : recursivelyFindMediaFromBlocks( block.innerBlocks ) + ); + } + + const mediaBlocks = recursivelyFindMediaFromBlocks( blocks ); + + if ( ! mediaBlocks.length ) { + return; + } + + const uploadedMediaList = await Promise.all( + mediaBlocks.map( ( block ) => + block.attributes.id + ? block.attributes + : new Promise( ( resolve, reject ) => { + window + .fetch( block.attributes.url ) + .then( ( response ) => response.blob() ) + .then( ( blob ) => + mediaUpload( { + filesList: [ blob ], + additionalData: { + title: block.attributes.title, + alt_text: block.attributes.alt, + caption: block.attributes.caption, + }, + onFileChange: ( [ media ] ) => { + if ( media.id ) { + resolve( media ); + } + }, + allowedTypes, + onError: reject, + } ) + ) + .catch( () => resolve( block.attributes.url ) ); + } ) + ) + ).catch( ( err ) => onError( err ) ); + + if ( multiple ) { + onSelect( uploadedMediaList ); + } else { + onSelect( uploadedMediaList[ 0 ] ); + } + } + + async function onHTMLDrop( HTML ) { + const blocks = pasteHandler( { HTML } ); + return await handleBlocksDrop( blocks ); + } + const onUpload = ( event ) => { onFilesUpload( event.target.files ); }; diff --git a/packages/block-library/src/gallery/editor.scss b/packages/block-library/src/gallery/editor.scss index 2fdde40f4c19af..ec2b3b08a459cf 100644 --- a/packages/block-library/src/gallery/editor.scss +++ b/packages/block-library/src/gallery/editor.scss @@ -4,12 +4,6 @@ figure.wp-block-gallery { // See https://github.com/WordPress/gutenberg/pull/10358 display: block; - &.has-nested-images { - .components-drop-zone { - display: none; - pointer-events: none; - } - } > .blocks-gallery-caption { flex: 0 0 100%; diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 4b39d87de055f6..76c3f829dda42f 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -4,12 +4,16 @@ - `CheckboxControl`, `CustomGradientPicker`, `FormToggle`, : Refactor and correct the focus style for consistency ([#50127](https://github.com/WordPress/gutenberg/pull/50127)). +### Breaking Changes + +- `onDragStart` in `` is now a synchronous function to allow setting additional data for `event.dataTransfer` ([#49673](https://github.com/WordPress/gutenberg/pull/49673)). + ### Internal - `NavigableContainer`: Convert to TypeScript ([#49377](https://github.com/WordPress/gutenberg/pull/49377)). - `ToolbarItem`: Convert to TypeScript ([#49190](https://github.com/WordPress/gutenberg/pull/49190)). - Move rich-text related types to the rich-text package ([#49651](https://github.com/WordPress/gutenberg/pull/49651)). -- `SlotFill`: simplified the implementation and removed unused code ([#50098](https://github.com/WordPress/gutenberg/pull/50098) and [#50133](https://github.com/WordPress/gutenberg/pull/50133)) +- `SlotFill`: simplified the implementation and removed unused code ([#50098](https://github.com/WordPress/gutenberg/pull/50098) and [#50133](https://github.com/WordPress/gutenberg/pull/50133)). ### Documentation diff --git a/packages/components/src/draggable/index.tsx b/packages/components/src/draggable/index.tsx index 1f839332b66c0a..0a3000538dbf24 100644 --- a/packages/components/src/draggable/index.tsx +++ b/packages/components/src/draggable/index.tsx @@ -212,14 +212,8 @@ export function Draggable( { // Update cursor to 'grabbing', document wide. ownerDocument.body.classList.add( bodyClass ); - // Allow the Synthetic Event to be accessed from asynchronous code. - // https://reactjs.org/docs/events.html#event-pooling - event.persist(); - - let timerId: number | undefined; - if ( onDragStart ) { - timerId = setTimeout( () => onDragStart( event ) ); + onDragStart( event ); } cleanup.current = () => { @@ -236,8 +230,6 @@ export function Draggable( { ownerDocument.body.classList.remove( bodyClass ); ownerDocument.removeEventListener( 'dragover', throttledDragOver ); - - clearTimeout( timerId ); }; } diff --git a/test/e2e/specs/editor/blocks/image.spec.js b/test/e2e/specs/editor/blocks/image.spec.js index 3684980affb291..513cdddb6aec45 100644 --- a/test/e2e/specs/editor/blocks/image.spec.js +++ b/test/e2e/specs/editor/blocks/image.spec.js @@ -538,6 +538,152 @@ test.describe( 'Image', () => {
` ); } ); + + test( 'can be replaced by dragging-and-dropping images from the inserter', async ( { + page, + editor, + } ) => { + await editor.insertBlock( { name: 'core/image' } ); + const imageBlock = page.getByRole( 'document', { + name: 'Block: Image', + } ); + const blockLibrary = page.getByRole( 'region', { + name: 'Block Library', + } ); + + async function openMediaTab() { + await page + .getByRole( 'button', { name: 'Toggle block inserter' } ) + .click(); + + await blockLibrary.getByRole( 'tab', { name: 'Media' } ).click(); + + await blockLibrary + .getByRole( 'tabpanel', { name: 'Media' } ) + .getByRole( 'button', { name: 'Openverse' } ) + .click(); + } + + await openMediaTab(); + + // Drag the first image from the media library into the image block. + await blockLibrary + .getByRole( 'listbox', { name: 'Media List' } ) + .getByRole( 'option' ) + .first() + .dragTo( imageBlock ); + + await expect( async () => { + const blocks = await editor.getBlocks(); + expect( blocks ).toHaveLength( 1 ); + + const [ + { + attributes: { url }, + }, + ] = blocks; + expect( + await imageBlock.getByRole( 'img' ).getAttribute( 'src' ) + ).toBe( url ); + expect( + new URL( url ).host, + 'should be updated to the media library' + ).toBe( new URL( page.url() ).host ); + }, 'should update the image from the media inserter' ).toPass(); + const [ + { + attributes: { url: firstUrl }, + }, + ] = await editor.getBlocks(); + + await openMediaTab(); + + // Drag the second image from the media library into the image block. + await blockLibrary + .getByRole( 'listbox', { name: 'Media List' } ) + .getByRole( 'option' ) + .nth( 1 ) + .dragTo( imageBlock ); + + await expect( async () => { + const blocks = await editor.getBlocks(); + expect( blocks ).toHaveLength( 1 ); + + const [ + { + attributes: { url }, + }, + ] = blocks; + expect( url ).not.toBe( firstUrl ); + expect( + await imageBlock.getByRole( 'img' ).getAttribute( 'src' ) + ).toBe( url ); + expect( + new URL( url ).host, + 'should be updated to the media library' + ).toBe( new URL( page.url() ).host ); + }, 'should replace the original image with the second image' ).toPass(); + } ); + + test( 'should allow dragging and dropping HTML to media placeholder', async ( { + page, + editor, + } ) => { + await editor.insertBlock( { name: 'core/image' } ); + const imageBlock = page.getByRole( 'document', { + name: 'Block: Image', + } ); + + const html = ` +
+ Cat +
"Cat" by tomhouslay is licensed under CC BY-NC 2.0.
+
+ `; + + await page.evaluate( ( _html ) => { + const dummy = document.createElement( 'div' ); + dummy.style.width = '10px'; + dummy.style.height = '10px'; + dummy.style.zIndex = 99999; + dummy.style.position = 'fixed'; + dummy.style.top = 0; + dummy.style.left = 0; + dummy.draggable = 'true'; + dummy.addEventListener( 'dragstart', ( event ) => { + event.dataTransfer.setData( 'text/html', _html ); + setTimeout( () => { + dummy.remove(); + }, 0 ); + } ); + document.body.appendChild( dummy ); + }, html ); + + await page.mouse.move( 0, 0 ); + await page.mouse.down(); + await imageBlock.hover(); + await page.mouse.up(); + + const host = new URL( page.url() ).host; + + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/image', + attributes: { + link: expect.stringContaining( host ), + url: expect.stringContaining( host ), + id: expect.any( Number ), + alt: 'Cat', + caption: `"Cat" by tomhouslay is licensed under CC BY-NC 2.0.`, + }, + }, + ] ); + const url = ( await editor.getBlocks() )[ 0 ].attributes.url; + await expect( imageBlock.getByRole( 'img' ) ).toHaveAttribute( + 'src', + url + ); + } ); } ); class ImageBlockUtils {