From 3c4dad4bb2fbc0c33f39a93cc1ca34ce0a969fe1 Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Thu, 29 Feb 2024 18:45:26 +0200 Subject: [PATCH] List: copy wrapper when multi selecting items (#59460) Co-authored-by: ellatrix Co-authored-by: t-hamano Co-authored-by: annezazu --- .../list-view/use-clipboard-handler.js | 5 +- .../writing-flow/use-clipboard-handler.js | 5 +- .../src/components/writing-flow/utils.js | 47 ++++++++++++------- packages/block-editor/src/private-apis.js | 2 + packages/block-library/src/list-item/edit.js | 3 +- .../src/list-item/hooks/index.js | 1 - .../src/list-item/hooks/use-copy.js | 38 --------------- packages/block-library/src/list-item/index.js | 3 ++ .../src/page-utils/press-keys.ts | 34 +++++++------- test/e2e/specs/editor/blocks/list.spec.js | 30 ++++++++++++ 10 files changed, 91 insertions(+), 77 deletions(-) delete mode 100644 packages/block-library/src/list-item/hooks/use-copy.js diff --git a/packages/block-editor/src/components/list-view/use-clipboard-handler.js b/packages/block-editor/src/components/list-view/use-clipboard-handler.js index cd25c71e9bf7c..dd3ac65ac79d2 100644 --- a/packages/block-editor/src/components/list-view/use-clipboard-handler.js +++ b/packages/block-editor/src/components/list-view/use-clipboard-handler.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { useDispatch, useSelect } from '@wordpress/data'; +import { useDispatch, useRegistry, useSelect } from '@wordpress/data'; import { useRefEffect } from '@wordpress/compose'; /** @@ -15,6 +15,7 @@ import { getPasteBlocks, setClipboardBlocks } from '../writing-flow/utils'; // This hook borrows from useClipboardHandler in ../writing-flow/use-clipboard-handler.js // and adds behaviour for the list view, while skipping partial selection. export default function useClipboardHandler( { selectBlock } ) { + const registry = useRegistry(); const { getBlockOrder, getBlockRootClientId, @@ -106,7 +107,7 @@ export default function useClipboardHandler( { selectBlock } ) { notifyCopy( event.type, selectedBlockClientIds ); const blocks = getBlocksByClientId( selectedBlockClientIds ); - setClipboardBlocks( event, blocks ); + setClipboardBlocks( event, blocks, registry ); } if ( event.type === 'cut' ) { diff --git a/packages/block-editor/src/components/writing-flow/use-clipboard-handler.js b/packages/block-editor/src/components/writing-flow/use-clipboard-handler.js index 8528655c1dcc9..43e887888dbd1 100644 --- a/packages/block-editor/src/components/writing-flow/use-clipboard-handler.js +++ b/packages/block-editor/src/components/writing-flow/use-clipboard-handler.js @@ -5,7 +5,7 @@ import { documentHasSelection, documentHasUncollapsedSelection, } from '@wordpress/dom'; -import { useDispatch, useSelect } from '@wordpress/data'; +import { useDispatch, useRegistry, useSelect } from '@wordpress/data'; import { useRefEffect } from '@wordpress/compose'; /** @@ -16,6 +16,7 @@ import { useNotifyCopy } from '../../utils/use-notify-copy'; import { getPasteBlocks, setClipboardBlocks } from './utils'; export default function useClipboardHandler() { + const registry = useRegistry(); const { getBlocksByClientId, getSelectedBlockClientIds, @@ -104,7 +105,7 @@ export default function useClipboardHandler() { blocks = [ head, ...inBetweenBlocks, tail ]; } - setClipboardBlocks( event, blocks ); + setClipboardBlocks( event, blocks, registry ); } } diff --git a/packages/block-editor/src/components/writing-flow/utils.js b/packages/block-editor/src/components/writing-flow/utils.js index ef1827077ccbf..2a2010854ed20 100644 --- a/packages/block-editor/src/components/writing-flow/utils.js +++ b/packages/block-editor/src/components/writing-flow/utils.js @@ -8,36 +8,51 @@ import { pasteHandler, findTransform, getBlockTransforms, + store as blocksStore, } from '@wordpress/blocks'; /** * Internal dependencies */ import { getPasteEventData } from '../../utils/pasting'; +import { store as blockEditorStore } from '../../store'; + +export const requiresWrapperOnCopy = Symbol( 'requiresWrapperOnCopy' ); /** * Sets the clipboard data for the provided blocks, with both HTML and plain * text representations. * - * @param {ClipboardEvent} event Clipboard event. - * @param {WPBlock[]} blocks Blocks to set as clipboard data. + * @param {ClipboardEvent} event Clipboard event. + * @param {WPBlock[]} blocks Blocks to set as clipboard data. + * @param {Object} registry The registry to select from. */ -export function setClipboardBlocks( event, blocks ) { +export function setClipboardBlocks( event, blocks, registry ) { let _blocks = blocks; - const wrapperBlockName = event.clipboardData.getData( - '__unstableWrapperBlockName' - ); - if ( wrapperBlockName ) { - _blocks = createBlock( - wrapperBlockName, - JSON.parse( - event.clipboardData.getData( - '__unstableWrapperBlockAttributes' - ) - ), - _blocks - ); + const [ firstBlock ] = blocks; + + if ( firstBlock ) { + const firstBlockType = registry + .select( blocksStore ) + .getBlockType( firstBlock.name ); + + if ( firstBlockType[ requiresWrapperOnCopy ] ) { + const { getBlockRootClientId, getBlockName, getBlockAttributes } = + registry.select( blockEditorStore ); + const wrapperBlockClientId = getBlockRootClientId( + firstBlock.clientId + ); + const wrapperBlockName = getBlockName( wrapperBlockClientId ); + + if ( wrapperBlockName ) { + _blocks = createBlock( + wrapperBlockName, + getBlockAttributes( wrapperBlockClientId ), + _blocks + ); + } + } } const serialized = serialize( _blocks ); diff --git a/packages/block-editor/src/private-apis.js b/packages/block-editor/src/private-apis.js index ec6843ead2489..6862d2a542457 100644 --- a/packages/block-editor/src/private-apis.js +++ b/packages/block-editor/src/private-apis.js @@ -27,6 +27,7 @@ import { ExperimentalBlockCanvas } from './components/block-canvas'; import { getDuotoneFilter } from './components/duotone/utils'; import { useFlashEditableBlocks } from './components/use-flash-editable-blocks'; import { selectBlockPatternsKey } from './store/private-keys'; +import { requiresWrapperOnCopy } from './components/writing-flow/utils'; import { PrivateRichText } from './components/rich-text/'; /** @@ -59,5 +60,6 @@ lock( privateApis, { usesContextKey, useFlashEditableBlocks, selectBlockPatternsKey, + requiresWrapperOnCopy, PrivateRichText, } ); diff --git a/packages/block-library/src/list-item/edit.js b/packages/block-library/src/list-item/edit.js index 46cbd3a94831d..467154f76992e 100644 --- a/packages/block-library/src/list-item/edit.js +++ b/packages/block-library/src/list-item/edit.js @@ -29,7 +29,6 @@ import { useOutdentListItem, useSplit, useMerge, - useCopy, } from './hooks'; import { convertToListItems } from './utils'; @@ -79,7 +78,7 @@ export default function ListItemEdit( { mergeBlocks, } ) { const { placeholder, content } = attributes; - const blockProps = useBlockProps( { ref: useCopy( clientId ) } ); + const blockProps = useBlockProps(); const innerBlocksProps = useInnerBlocksProps( blockProps, { renderAppender: false, __unstableDisableDropZone: true, diff --git a/packages/block-library/src/list-item/hooks/index.js b/packages/block-library/src/list-item/hooks/index.js index 3bbc3167abed3..1687adbe740d0 100644 --- a/packages/block-library/src/list-item/hooks/index.js +++ b/packages/block-library/src/list-item/hooks/index.js @@ -4,4 +4,3 @@ export { default as useEnter } from './use-enter'; export { default as useSpace } from './use-space'; export { default as useSplit } from './use-split'; export { default as useMerge } from './use-merge'; -export { default as useCopy } from './use-copy'; diff --git a/packages/block-library/src/list-item/hooks/use-copy.js b/packages/block-library/src/list-item/hooks/use-copy.js deleted file mode 100644 index 7a76019ad11a4..0000000000000 --- a/packages/block-library/src/list-item/hooks/use-copy.js +++ /dev/null @@ -1,38 +0,0 @@ -/** - * WordPress dependencies - */ -import { useRefEffect } from '@wordpress/compose'; -import { store as blockEditorStore } from '@wordpress/block-editor'; -import { useSelect } from '@wordpress/data'; - -export default function useCopy( clientId ) { - const { getBlockRootClientId, getBlockName, getBlockAttributes } = - useSelect( blockEditorStore ); - - return useRefEffect( ( node ) => { - function onCopy( event ) { - // The event propagates through all nested lists, so don't override - // when copying nested list items. - if ( event.clipboardData.getData( '__unstableWrapperBlockName' ) ) { - return; - } - - const rootClientId = getBlockRootClientId( clientId ); - event.clipboardData.setData( - '__unstableWrapperBlockName', - getBlockName( rootClientId ) - ); - event.clipboardData.setData( - '__unstableWrapperBlockAttributes', - JSON.stringify( getBlockAttributes( rootClientId ) ) - ); - } - - node.addEventListener( 'copy', onCopy ); - node.addEventListener( 'cut', onCopy ); - return () => { - node.removeEventListener( 'copy', onCopy ); - node.removeEventListener( 'cut', onCopy ); - }; - }, [] ); -} diff --git a/packages/block-library/src/list-item/index.js b/packages/block-library/src/list-item/index.js index 00adc1c2c4026..07c5bb7fda901 100644 --- a/packages/block-library/src/list-item/index.js +++ b/packages/block-library/src/list-item/index.js @@ -2,6 +2,7 @@ * WordPress dependencies */ import { listItem as icon } from '@wordpress/icons'; +import { privateApis } from '@wordpress/block-editor'; /** * Internal dependencies @@ -11,6 +12,7 @@ import metadata from './block.json'; import edit from './edit'; import save from './save'; import transforms from './transforms'; +import { unlock } from '../lock-unlock'; const { name } = metadata; @@ -27,6 +29,7 @@ export const settings = { }; }, transforms, + [ unlock( privateApis ).requiresWrapperOnCopy ]: true, }; export const init = () => initBlock( { name, metadata, settings } ); diff --git a/packages/e2e-test-utils-playwright/src/page-utils/press-keys.ts b/packages/e2e-test-utils-playwright/src/page-utils/press-keys.ts index 3b187625fd47c..11bc11c43f603 100644 --- a/packages/e2e-test-utils-playwright/src/page-utils/press-keys.ts +++ b/packages/e2e-test-utils-playwright/src/page-utils/press-keys.ts @@ -55,18 +55,26 @@ async function emulateClipboard( page: Page, type: 'copy' | 'cut' | 'paste' ) { const canvasDoc = // @ts-ignore document.activeElement?.contentDocument ?? document; - const clipboardDataTransfer = new DataTransfer(); + const event = new ClipboardEvent( _type, { + bubbles: true, + cancelable: true, + clipboardData: new DataTransfer(), + } ); + + if ( ! event.clipboardData ) { + throw new Error( 'ClipboardEvent.clipboardData is null' ); + } if ( _type === 'paste' ) { - clipboardDataTransfer.setData( + event.clipboardData.setData( 'text/plain', _clipboardData[ 'text/plain' ] ); - clipboardDataTransfer.setData( + event.clipboardData.setData( 'text/html', _clipboardData[ 'text/html' ] ); - clipboardDataTransfer.setData( + event.clipboardData.setData( 'rich-text', _clipboardData[ 'rich-text' ] ); @@ -85,22 +93,16 @@ async function emulateClipboard( page: Page, type: 'copy' | 'cut' | 'paste' ) { ) .join( '' ); } - clipboardDataTransfer.setData( 'text/plain', plainText ); - clipboardDataTransfer.setData( 'text/html', html ); + event.clipboardData.setData( 'text/plain', plainText ); + event.clipboardData.setData( 'text/html', html ); } - canvasDoc.activeElement?.dispatchEvent( - new ClipboardEvent( _type, { - bubbles: true, - cancelable: true, - clipboardData: clipboardDataTransfer, - } ) - ); + canvasDoc.activeElement.dispatchEvent( event ); return { - 'text/plain': clipboardDataTransfer.getData( 'text/plain' ), - 'text/html': clipboardDataTransfer.getData( 'text/html' ), - 'rich-text': clipboardDataTransfer.getData( 'rich-text' ), + 'text/plain': event.clipboardData.getData( 'text/plain' ), + 'text/html': event.clipboardData.getData( 'text/html' ), + 'rich-text': event.clipboardData.getData( 'rich-text' ), }; }, [ type, clipboardDataHolder ] as const diff --git a/test/e2e/specs/editor/blocks/list.spec.js b/test/e2e/specs/editor/blocks/list.spec.js index 48d50a862a50b..10f25d6b3609f 100644 --- a/test/e2e/specs/editor/blocks/list.spec.js +++ b/test/e2e/specs/editor/blocks/list.spec.js @@ -8,6 +8,36 @@ test.describe( 'List (@firefox)', () => { await admin.createNewPost(); } ); + test( 'can be copied from multi selection', async ( { + editor, + page, + pageUtils, + } ) => { + await editor.insertBlock( { name: 'core/list' } ); + await page.keyboard.type( 'one' ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( 'two' ); + await pageUtils.pressKeys( 'primary+a' ); + await pageUtils.pressKeys( 'primary+a' ); + await pageUtils.pressKeys( 'primary+c' ); + await editor.insertBlock( { name: 'core/paragraph' } ); + await pageUtils.pressKeys( 'primary+v' ); + + const copied = ` +
    +
  • one
  • + + + +
  • two
  • +
+`; + + expect( await editor.getEditedPostContent() ).toBe( + copied + '\n\n' + copied + ); + } ); + test( 'can be created by using an asterisk at the start of a paragraph block', async ( { editor, page,