From c900fd3b0fc0c7e950e5619522694df2702303dd Mon Sep 17 00:00:00 2001
From: Andrew Serong <14988353+andrewserong@users.noreply.github.com>
Date: Mon, 15 Jan 2024 12:18:39 +1100
Subject: [PATCH 01/10] List View: Try adding keyboard clipboard events for
cut, copy, paste
---
.../block-editor/src/components/list-view/index.js | 12 +++++++++++-
1 file changed, 11 insertions(+), 1 deletion(-)
diff --git a/packages/block-editor/src/components/list-view/index.js b/packages/block-editor/src/components/list-view/index.js
index 5270a7af3a2962..016661505b5c8d 100644
--- a/packages/block-editor/src/components/list-view/index.js
+++ b/packages/block-editor/src/components/list-view/index.js
@@ -42,6 +42,7 @@ import useListViewExpandSelectedItem from './use-list-view-expand-selected-item'
import { store as blockEditorStore } from '../../store';
import { BlockSettingsDropdown } from '../block-settings-menu/block-settings-dropdown';
import { focusListItem } from './utils';
+import useClipboardHandler from '../writing-flow/use-clipboard-handler';
const expanded = ( state, action ) => {
if ( Array.isArray( action.clientIds ) ) {
@@ -143,7 +144,16 @@ function ListViewComponent(
setExpandedState,
} );
const elementRef = useRef();
- const treeGridRef = useMergeRefs( [ elementRef, dropZoneRef, ref ] );
+
+ // Allow handling of copy, cut, and paste events.
+ const clipBoardRef = useClipboardHandler();
+
+ const treeGridRef = useMergeRefs( [
+ clipBoardRef,
+ elementRef,
+ dropZoneRef,
+ ref,
+ ] );
const [ insertedBlock, setInsertedBlock ] = useState( null );
From a12cac8a72ad7b218a07cf3815e1e8f260266ee5 Mon Sep 17 00:00:00 2001
From: Andrew Serong <14988353+andrewserong@users.noreply.github.com>
Date: Wed, 17 Jan 2024 12:39:52 +1100
Subject: [PATCH 02/10] Make useClipboardHandler that works with the list view
---
.../src/components/list-view/index.js | 2 +-
.../list-view/use-clipboard-handler.js | 232 ++++++++++++++++++
2 files changed, 233 insertions(+), 1 deletion(-)
create mode 100644 packages/block-editor/src/components/list-view/use-clipboard-handler.js
diff --git a/packages/block-editor/src/components/list-view/index.js b/packages/block-editor/src/components/list-view/index.js
index 016661505b5c8d..915857505169be 100644
--- a/packages/block-editor/src/components/list-view/index.js
+++ b/packages/block-editor/src/components/list-view/index.js
@@ -42,7 +42,7 @@ import useListViewExpandSelectedItem from './use-list-view-expand-selected-item'
import { store as blockEditorStore } from '../../store';
import { BlockSettingsDropdown } from '../block-settings-menu/block-settings-dropdown';
import { focusListItem } from './utils';
-import useClipboardHandler from '../writing-flow/use-clipboard-handler';
+import useClipboardHandler from './use-clipboard-handler';
const expanded = ( state, action ) => {
if ( Array.isArray( action.clientIds ) ) {
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
new file mode 100644
index 00000000000000..0b33569a06227b
--- /dev/null
+++ b/packages/block-editor/src/components/list-view/use-clipboard-handler.js
@@ -0,0 +1,232 @@
+/**
+ * WordPress dependencies
+ */
+import {
+ serialize,
+ pasteHandler,
+ createBlock,
+ findTransform,
+ getBlockTransforms,
+} from '@wordpress/blocks';
+import { __unstableStripHTML as stripHTML } from '@wordpress/dom';
+import { useDispatch, useSelect } from '@wordpress/data';
+import { useRefEffect } from '@wordpress/compose';
+
+/**
+ * Internal dependencies
+ */
+import { getPasteEventData } from '../../utils/pasting';
+import { store as blockEditorStore } from '../../store';
+import { useNotifyCopy } from '../../utils/use-notify-copy';
+
+export default function useClipboardHandler() {
+ const {
+ getBlockRootClientId,
+ getBlocksByClientId,
+ getSelectedBlockClientIds,
+ getSettings,
+ canInsertBlockType,
+ canRemoveBlocks,
+ } = useSelect( blockEditorStore );
+ const { flashBlock, removeBlocks, replaceBlocks, insertBlocks } =
+ useDispatch( blockEditorStore );
+ const notifyCopy = useNotifyCopy();
+
+ return useRefEffect( ( node ) => {
+ // Determine which blocks to update:
+ // If the current (focused) block is part of the block selection, use the whole selection.
+ // If the focused block is not part of the block selection, only update the focused block.
+ function getBlocksToUpdate( clientId ) {
+ const selectedBlockClientIds = getSelectedBlockClientIds();
+ const isUpdatingSelectedBlocks =
+ selectedBlockClientIds.includes( clientId );
+ const firstBlockClientId = isUpdatingSelectedBlocks
+ ? selectedBlockClientIds[ 0 ]
+ : clientId;
+ const firstBlockRootClientId =
+ getBlockRootClientId( firstBlockClientId );
+
+ const blocksToUpdate = isUpdatingSelectedBlocks
+ ? selectedBlockClientIds
+ : [ clientId ];
+
+ return {
+ blocksToUpdate,
+ firstBlockClientId,
+ firstBlockRootClientId,
+ selectedBlockClientIds,
+ };
+ }
+
+ function handler( event ) {
+ if ( event.defaultPrevented ) {
+ // This was possibly already handled in rich-text/use-paste-handler.js.
+ return;
+ }
+
+ // Only handle events that occur within the list view.
+ if ( ! node.contains( event.target.ownerDocument.activeElement ) ) {
+ return;
+ }
+
+ // Retrieve the block clientId associated with the focused list view row.
+ // This enables applying copy / cut / paste behavior to the focused block,
+ // rather than just the blocks that are currently selected.
+ const listViewRow =
+ event.target.ownerDocument.activeElement?.closest(
+ '[role=row]'
+ );
+ const clientId = listViewRow?.dataset?.block;
+ if ( ! clientId ) {
+ return;
+ }
+
+ const {
+ blocksToUpdate: selectedBlockClientIds,
+ firstBlockRootClientId,
+ } = getBlocksToUpdate( clientId );
+
+ if ( selectedBlockClientIds.length === 0 ) {
+ return;
+ }
+
+ event.preventDefault();
+
+ if ( event.type === 'copy' || event.type === 'cut' ) {
+ if ( selectedBlockClientIds.length === 1 ) {
+ flashBlock( selectedBlockClientIds[ 0 ] );
+ }
+
+ notifyCopy( event.type, selectedBlockClientIds );
+ let blocks;
+ // Check if we have partial selection.
+ blocks = getBlocksByClientId( selectedBlockClientIds );
+
+ const wrapperBlockName = event.clipboardData.getData(
+ '__unstableWrapperBlockName'
+ );
+
+ if ( wrapperBlockName ) {
+ blocks = createBlock(
+ wrapperBlockName,
+ JSON.parse(
+ event.clipboardData.getData(
+ '__unstableWrapperBlockAttributes'
+ )
+ ),
+ blocks
+ );
+ }
+
+ const serialized = serialize( blocks );
+
+ event.clipboardData.setData(
+ 'text/plain',
+ toPlainText( serialized )
+ );
+ event.clipboardData.setData( 'text/html', serialized );
+ }
+
+ if ( event.type === 'cut' ) {
+ // Don't update the selection if the blocks cannot be deleted.
+ if (
+ ! canRemoveBlocks(
+ selectedBlockClientIds,
+ firstBlockRootClientId
+ )
+ ) {
+ return;
+ }
+ removeBlocks( selectedBlockClientIds );
+ } else if ( event.type === 'paste' ) {
+ const {
+ __experimentalCanUserUseUnfilteredHTML:
+ canUserUseUnfilteredHTML,
+ } = getSettings();
+ const { plainText, html, files } = getPasteEventData( event );
+ let blocks = [];
+
+ if ( files.length ) {
+ const fromTransforms = getBlockTransforms( 'from' );
+ blocks = files
+ .reduce( ( accumulator, file ) => {
+ const transformation = findTransform(
+ fromTransforms,
+ ( transform ) =>
+ transform.type === 'files' &&
+ transform.isMatch( [ file ] )
+ );
+ if ( transformation ) {
+ accumulator.push(
+ transformation.transform( [ file ] )
+ );
+ }
+ return accumulator;
+ }, [] )
+ .flat();
+ } else {
+ blocks = pasteHandler( {
+ HTML: html,
+ plainText,
+ mode: 'BLOCKS',
+ canUserUseUnfilteredHTML,
+ } );
+ }
+
+ if ( selectedBlockClientIds.length === 1 ) {
+ const [ selectedBlockClientId ] = selectedBlockClientIds;
+
+ if (
+ blocks.every( ( block ) =>
+ canInsertBlockType(
+ block.name,
+ selectedBlockClientId
+ )
+ )
+ ) {
+ insertBlocks(
+ blocks,
+ undefined,
+ selectedBlockClientId
+ );
+ return;
+ }
+ }
+
+ replaceBlocks(
+ selectedBlockClientIds,
+ blocks,
+ blocks.length - 1,
+ -1
+ );
+ }
+ }
+
+ node.ownerDocument.addEventListener( 'copy', handler );
+ node.ownerDocument.addEventListener( 'cut', handler );
+ node.ownerDocument.addEventListener( 'paste', handler );
+
+ return () => {
+ node.ownerDocument.removeEventListener( 'copy', handler );
+ node.ownerDocument.removeEventListener( 'cut', handler );
+ node.ownerDocument.removeEventListener( 'paste', handler );
+ };
+ }, [] );
+}
+
+/**
+ * Given a string of HTML representing serialized blocks, returns the plain
+ * text extracted after stripping the HTML of any tags and fixing line breaks.
+ *
+ * @param {string} html Serialized blocks.
+ * @return {string} The plain-text content with any html removed.
+ */
+function toPlainText( html ) {
+ // Manually handle BR tags as line breaks prior to `stripHTML` call
+ html = html.replace( /
/g, '\n' );
+
+ const plainText = stripHTML( html ).trim();
+
+ // Merge any consecutive line breaks
+ return plainText.replace( /\n\n+/g, '\n\n' );
+}
From 46b59f208d38e691f58775c55cd22853a6d2f12a Mon Sep 17 00:00:00 2001
From: Andrew Serong <14988353+andrewserong@users.noreply.github.com>
Date: Wed, 17 Jan 2024 14:06:29 +1100
Subject: [PATCH 03/10] Ensure focus remains within list view after cut and
paste
---
.../src/components/list-view/block.js | 2 +-
.../src/components/list-view/index.js | 39 ++++++++--------
.../list-view/use-clipboard-handler.js | 46 +++++++++++++++++--
.../src/components/list-view/utils.js | 8 ++--
4 files changed, 69 insertions(+), 26 deletions(-)
diff --git a/packages/block-editor/src/components/list-view/block.js b/packages/block-editor/src/components/list-view/block.js
index aab9d444a5948f..6dedabb48f8b85 100644
--- a/packages/block-editor/src/components/list-view/block.js
+++ b/packages/block-editor/src/components/list-view/block.js
@@ -187,7 +187,7 @@ function ListViewBlock( {
selectBlock( undefined, focusClientId, null, null );
}
- focusListItem( focusClientId, treeGridElementRef );
+ focusListItem( focusClientId, treeGridElementRef?.current );
},
[ selectBlock, treeGridElementRef ]
);
diff --git a/packages/block-editor/src/components/list-view/index.js b/packages/block-editor/src/components/list-view/index.js
index 915857505169be..f11eca023463f3 100644
--- a/packages/block-editor/src/components/list-view/index.js
+++ b/packages/block-editor/src/components/list-view/index.js
@@ -138,23 +138,6 @@ function ListViewComponent(
const [ expandedState, setExpandedState ] = useReducer( expanded, {} );
- const { ref: dropZoneRef, target: blockDropTarget } = useListViewDropZone( {
- dropZoneElement,
- expandedState,
- setExpandedState,
- } );
- const elementRef = useRef();
-
- // Allow handling of copy, cut, and paste events.
- const clipBoardRef = useClipboardHandler();
-
- const treeGridRef = useMergeRefs( [
- clipBoardRef,
- elementRef,
- dropZoneRef,
- ref,
- ] );
-
const [ insertedBlock, setInsertedBlock ] = useState( null );
const { setSelectedTreeId } = useListViewExpandSelectedItem( {
@@ -176,11 +159,31 @@ function ListViewComponent(
},
[ setSelectedTreeId, updateBlockSelection, onSelect, getBlock ]
);
+
+ const { ref: dropZoneRef, target: blockDropTarget } = useListViewDropZone( {
+ dropZoneElement,
+ expandedState,
+ setExpandedState
+ } );
+ const elementRef = useRef();
+
+ // Allow handling of copy, cut, and paste events.
+ const clipBoardRef = useClipboardHandler( {
+ selectBlock: selectEditorBlock,
+ } );
+
+ const treeGridRef = useMergeRefs( [
+ clipBoardRef,
+ elementRef,
+ dropZoneRef,
+ ref,
+ ] );
+
useEffect( () => {
// If a blocks are already selected when the list view is initially
// mounted, shift focus to the first selected block.
if ( selectedClientIds?.length ) {
- focusListItem( selectedClientIds[ 0 ], elementRef );
+ focusListItem( selectedClientIds[ 0 ], elementRef?.current );
}
// Disable reason: Only focus on the selected item when the list view is mounted.
// eslint-disable-next-line react-hooks/exhaustive-deps
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 0b33569a06227b..0a5d61c9dae4d0 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
@@ -18,11 +18,14 @@ import { useRefEffect } from '@wordpress/compose';
import { getPasteEventData } from '../../utils/pasting';
import { store as blockEditorStore } from '../../store';
import { useNotifyCopy } from '../../utils/use-notify-copy';
+import { focusListItem } from './utils';
-export default function useClipboardHandler() {
+export default function useClipboardHandler( { selectBlock } ) {
const {
+ getBlockOrder,
getBlockRootClientId,
getBlocksByClientId,
+ getPreviousBlockClientId,
getSelectedBlockClientIds,
getSettings,
canInsertBlockType,
@@ -33,6 +36,14 @@ export default function useClipboardHandler() {
const notifyCopy = useNotifyCopy();
return useRefEffect( ( node ) => {
+ function updateFocusAndSelection( focusClientId, shouldSelectBlock ) {
+ if ( shouldSelectBlock ) {
+ selectBlock( undefined, focusClientId, null, null );
+ }
+
+ focusListItem( focusClientId, node );
+ }
+
// Determine which blocks to update:
// If the current (focused) block is part of the block selection, use the whole selection.
// If the focused block is not part of the block selection, only update the focused block.
@@ -54,7 +65,7 @@ export default function useClipboardHandler() {
blocksToUpdate,
firstBlockClientId,
firstBlockRootClientId,
- selectedBlockClientIds,
+ originallySelectedBlockClientIds: selectedBlockClientIds,
};
}
@@ -83,7 +94,9 @@ export default function useClipboardHandler() {
const {
blocksToUpdate: selectedBlockClientIds,
+ firstBlockClientId,
firstBlockRootClientId,
+ originallySelectedBlockClientIds,
} = getBlocksToUpdate( clientId );
if ( selectedBlockClientIds.length === 0 ) {
@@ -137,7 +150,27 @@ export default function useClipboardHandler() {
) {
return;
}
- removeBlocks( selectedBlockClientIds );
+
+ let blockToFocus =
+ getPreviousBlockClientId( firstBlockClientId ) ??
+ // If the previous block is not found (when the first block is deleted),
+ // fallback to focus the parent block.
+ firstBlockRootClientId;
+
+ // Remove blocks, but don't update selection, and it will be handled below.
+ removeBlocks( selectedBlockClientIds, false );
+
+ // Update the selection if the original selection has been removed.
+ const shouldUpdateSelection =
+ originallySelectedBlockClientIds.length > 0 &&
+ getSelectedBlockClientIds().length === 0;
+
+ // If there's no previous block nor parent block, focus the first block.
+ if ( ! blockToFocus ) {
+ blockToFocus = getBlockOrder()[ 0 ];
+ }
+
+ updateFocusAndSelection( blockToFocus, shouldUpdateSelection );
} else if ( event.type === 'paste' ) {
const {
__experimentalCanUserUseUnfilteredHTML:
@@ -176,6 +209,11 @@ export default function useClipboardHandler() {
if ( selectedBlockClientIds.length === 1 ) {
const [ selectedBlockClientId ] = selectedBlockClientIds;
+ // If a single block is focused, and the blocks to be posted can
+ // be inserted within the block, then append the pasted blocks
+ // within the focused block. For example, if you have copied a paragraph
+ // block and paste it within a single Group block, this will append
+ // the paragraph block within the Group block.
if (
blocks.every( ( block ) =>
canInsertBlockType(
@@ -189,6 +227,7 @@ export default function useClipboardHandler() {
undefined,
selectedBlockClientId
);
+ updateFocusAndSelection( blocks[ 0 ]?.clientId, false );
return;
}
}
@@ -199,6 +238,7 @@ export default function useClipboardHandler() {
blocks.length - 1,
-1
);
+ updateFocusAndSelection( blocks[ 0 ]?.clientId, false );
}
}
diff --git a/packages/block-editor/src/components/list-view/utils.js b/packages/block-editor/src/components/list-view/utils.js
index ed7a321dea0c86..3e596ca5015465 100644
--- a/packages/block-editor/src/components/list-view/utils.js
+++ b/packages/block-editor/src/components/list-view/utils.js
@@ -63,12 +63,12 @@ export function getCommonDepthClientIds(
*
* @typedef {import('@wordpress/element').RefObject} RefObject
*
- * @param {string} focusClientId The client ID of the block to focus.
- * @param {RefObject} treeGridElementRef The container element to search within.
+ * @param {string} focusClientId The client ID of the block to focus.
+ * @param {HTMLElement} treeGridElement The container element to search within.
*/
-export function focusListItem( focusClientId, treeGridElementRef ) {
+export function focusListItem( focusClientId, treeGridElement ) {
const getFocusElement = () => {
- const row = treeGridElementRef.current?.querySelector(
+ const row = treeGridElement?.querySelector(
`[role=row][data-block="${ focusClientId }"]`
);
if ( ! row ) return null;
From 75d06fac9a5dec8d97e1bd00e66a38069edf5652 Mon Sep 17 00:00:00 2001
From: Andrew Serong <14988353+andrewserong@users.noreply.github.com>
Date: Thu, 18 Jan 2024 11:22:14 +1100
Subject: [PATCH 04/10] Add e2e tests
---
.../specs/editor/various/list-view.spec.js | 96 ++++++++++++++++++-
1 file changed, 95 insertions(+), 1 deletion(-)
diff --git a/test/e2e/specs/editor/various/list-view.spec.js b/test/e2e/specs/editor/various/list-view.spec.js
index 00f21b4e51c5ea..cb15c12c84b490 100644
--- a/test/e2e/specs/editor/various/list-view.spec.js
+++ b/test/e2e/specs/editor/various/list-view.spec.js
@@ -450,7 +450,7 @@ test.describe( 'List View', () => {
).toBeFocused();
} );
- test( 'should select, duplicate, delete, and deselect blocks using keyboard', async ( {
+ test( 'should cut, copy, paste, select, duplicate, delete, and deselect blocks using keyboard', async ( {
editor,
page,
pageUtils,
@@ -808,6 +808,100 @@ test.describe( 'List View', () => {
{ name: 'core/heading', selected: false, focused: false },
{ name: 'core/file', selected: false, focused: true },
] );
+
+ // Copy and paste blocks. To begin, add another Group block.
+ await editor.insertBlock( {
+ name: 'core/group',
+ innerBlocks: [
+ { name: 'core/paragraph' },
+ { name: 'core/pullquote' },
+ ],
+ } );
+
+ // Click the newly inserted Group block List View item to ensure it is focused.
+ await listView
+ .getByRole( 'link', {
+ name: 'Group',
+ expanded: false,
+ } )
+ .click();
+
+ // Move down to group block, expand, and then move to the paragraph block.
+ await page.keyboard.press( 'ArrowDown' );
+ await page.keyboard.press( 'ArrowRight' );
+ await page.keyboard.press( 'ArrowDown' );
+ await page.keyboard.press( 'ArrowDown' );
+ await pageUtils.pressKeys( 'primary+c' );
+ await page.keyboard.press( 'ArrowUp' );
+ await pageUtils.pressKeys( 'primary+v' );
+
+ await expect
+ .poll(
+ listViewUtils.getBlocksWithA11yAttributes,
+ 'Should be able to copy focused block and paste in the list view via keyboard shortcuts'
+ )
+ .toMatchObject( [
+ { name: 'core/heading', selected: false, focused: false },
+ { name: 'core/file', selected: false, focused: false },
+ {
+ name: 'core/group',
+ selected: true,
+ innerBlocks: [
+ {
+ name: 'core/pullquote',
+ selected: false,
+ focused: true,
+ },
+ {
+ name: 'core/pullquote',
+ selected: false,
+ focused: false,
+ },
+ ],
+ },
+ ] );
+
+ // Cut and paste blocks.
+ await page.keyboard.press( 'ArrowUp' );
+ await pageUtils.pressKeys( 'primary+x' );
+
+ await expect
+ .poll(
+ listViewUtils.getBlocksWithA11yAttributes,
+ 'Should be able to cut a block in the list view, with the preceding block being selected'
+ )
+ .toMatchObject( [
+ { name: 'core/heading', selected: false, focused: false },
+ { name: 'core/file', selected: true, focused: true },
+ ] );
+
+ await pageUtils.pressKeys( 'primary+v' );
+
+ await expect
+ .poll(
+ listViewUtils.getBlocksWithA11yAttributes,
+ 'Should be able to paste previously cut block in the list view via keyboard shortcuts'
+ )
+ .toMatchObject( [
+ { name: 'core/heading', selected: false, focused: false },
+ {
+ name: 'core/group',
+ selected: true,
+ focused: true,
+ innerBlocks: [
+ {
+ name: 'core/pullquote',
+ selected: false,
+ focused: false,
+ },
+ {
+ name: 'core/pullquote',
+ selected: false,
+ focused: false,
+ },
+ ],
+ },
+ ] );
} );
test( 'block settings dropdown menu', async ( {
From eb2825f22598f1d786a63652b10157b91c039575 Mon Sep 17 00:00:00 2001
From: Andrew Serong <14988353+andrewserong@users.noreply.github.com>
Date: Tue, 23 Jan 2024 12:04:31 +1100
Subject: [PATCH 05/10] Fix linting issue
---
packages/block-editor/src/components/list-view/index.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/block-editor/src/components/list-view/index.js b/packages/block-editor/src/components/list-view/index.js
index f11eca023463f3..895571755e4fa0 100644
--- a/packages/block-editor/src/components/list-view/index.js
+++ b/packages/block-editor/src/components/list-view/index.js
@@ -163,7 +163,7 @@ function ListViewComponent(
const { ref: dropZoneRef, target: blockDropTarget } = useListViewDropZone( {
dropZoneElement,
expandedState,
- setExpandedState
+ setExpandedState,
} );
const elementRef = useRef();
From 68ed6c7b31bdf308cc1987d5822800ef5c3bbb5d Mon Sep 17 00:00:00 2001
From: Andrew Serong <14988353+andrewserong@users.noreply.github.com>
Date: Tue, 23 Jan 2024 16:12:26 +1100
Subject: [PATCH 06/10] Update
packages/block-editor/src/components/list-view/utils.js
Co-authored-by: Robert Anderson
---
packages/block-editor/src/components/list-view/utils.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/packages/block-editor/src/components/list-view/utils.js b/packages/block-editor/src/components/list-view/utils.js
index 3e596ca5015465..c91376b0472116 100644
--- a/packages/block-editor/src/components/list-view/utils.js
+++ b/packages/block-editor/src/components/list-view/utils.js
@@ -63,8 +63,8 @@ export function getCommonDepthClientIds(
*
* @typedef {import('@wordpress/element').RefObject} RefObject
*
- * @param {string} focusClientId The client ID of the block to focus.
- * @param {HTMLElement} treeGridElement The container element to search within.
+ * @param {string} focusClientId The client ID of the block to focus.
+ * @param {?HTMLElement} treeGridElement The container element to search within.
*/
export function focusListItem( focusClientId, treeGridElement ) {
const getFocusElement = () => {
From 885b1477ae9442e6a305c6af8f83dc4a4a5b46ac Mon Sep 17 00:00:00 2001
From: Andrew Serong <14988353+andrewserong@users.noreply.github.com>
Date: Wed, 24 Jan 2024 15:58:42 +1100
Subject: [PATCH 07/10] Try consolidating some of the copy and paste behaviour
---
.../list-view/use-clipboard-handler.js | 89 ++-----------------
.../writing-flow/use-clipboard-handler.js | 84 ++---------------
.../src/components/writing-flow/utils.js | 89 +++++++++++++++++++
3 files changed, 102 insertions(+), 160 deletions(-)
create mode 100644 packages/block-editor/src/components/writing-flow/utils.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 0a5d61c9dae4d0..3145f18934b452 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,24 +1,16 @@
/**
* WordPress dependencies
*/
-import {
- serialize,
- pasteHandler,
- createBlock,
- findTransform,
- getBlockTransforms,
-} from '@wordpress/blocks';
-import { __unstableStripHTML as stripHTML } from '@wordpress/dom';
import { useDispatch, useSelect } from '@wordpress/data';
import { useRefEffect } from '@wordpress/compose';
/**
* Internal dependencies
*/
-import { getPasteEventData } from '../../utils/pasting';
import { store as blockEditorStore } from '../../store';
import { useNotifyCopy } from '../../utils/use-notify-copy';
import { focusListItem } from './utils';
+import { getPasteBlocks, setClipboardBlocks } from '../writing-flow/utils';
export default function useClipboardHandler( { selectBlock } ) {
const {
@@ -111,33 +103,8 @@ export default function useClipboardHandler( { selectBlock } ) {
}
notifyCopy( event.type, selectedBlockClientIds );
- let blocks;
- // Check if we have partial selection.
- blocks = getBlocksByClientId( selectedBlockClientIds );
-
- const wrapperBlockName = event.clipboardData.getData(
- '__unstableWrapperBlockName'
- );
-
- if ( wrapperBlockName ) {
- blocks = createBlock(
- wrapperBlockName,
- JSON.parse(
- event.clipboardData.getData(
- '__unstableWrapperBlockAttributes'
- )
- ),
- blocks
- );
- }
-
- const serialized = serialize( blocks );
-
- event.clipboardData.setData(
- 'text/plain',
- toPlainText( serialized )
- );
- event.clipboardData.setData( 'text/html', serialized );
+ const blocks = getBlocksByClientId( selectedBlockClientIds );
+ setClipboardBlocks( event, blocks );
}
if ( event.type === 'cut' ) {
@@ -176,35 +143,10 @@ export default function useClipboardHandler( { selectBlock } ) {
__experimentalCanUserUseUnfilteredHTML:
canUserUseUnfilteredHTML,
} = getSettings();
- const { plainText, html, files } = getPasteEventData( event );
- let blocks = [];
-
- if ( files.length ) {
- const fromTransforms = getBlockTransforms( 'from' );
- blocks = files
- .reduce( ( accumulator, file ) => {
- const transformation = findTransform(
- fromTransforms,
- ( transform ) =>
- transform.type === 'files' &&
- transform.isMatch( [ file ] )
- );
- if ( transformation ) {
- accumulator.push(
- transformation.transform( [ file ] )
- );
- }
- return accumulator;
- }, [] )
- .flat();
- } else {
- blocks = pasteHandler( {
- HTML: html,
- plainText,
- mode: 'BLOCKS',
- canUserUseUnfilteredHTML,
- } );
- }
+ const blocks = getPasteBlocks(
+ event,
+ canUserUseUnfilteredHTML
+ );
if ( selectedBlockClientIds.length === 1 ) {
const [ selectedBlockClientId ] = selectedBlockClientIds;
@@ -253,20 +195,3 @@ export default function useClipboardHandler( { selectBlock } ) {
};
}, [] );
}
-
-/**
- * Given a string of HTML representing serialized blocks, returns the plain
- * text extracted after stripping the HTML of any tags and fixing line breaks.
- *
- * @param {string} html Serialized blocks.
- * @return {string} The plain-text content with any html removed.
- */
-function toPlainText( html ) {
- // Manually handle BR tags as line breaks prior to `stripHTML` call
- html = html.replace( /
/g, '\n' );
-
- const plainText = stripHTML( html ).trim();
-
- // Merge any consecutive line breaks
- return plainText.replace( /\n\n+/g, '\n\n' );
-}
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 5b78d2f8656b61..8528655c1dcc9e 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
@@ -1,17 +1,9 @@
/**
* WordPress dependencies
*/
-import {
- serialize,
- pasteHandler,
- createBlock,
- findTransform,
- getBlockTransforms,
-} from '@wordpress/blocks';
import {
documentHasSelection,
documentHasUncollapsedSelection,
- __unstableStripHTML as stripHTML,
} from '@wordpress/dom';
import { useDispatch, useSelect } from '@wordpress/data';
import { useRefEffect } from '@wordpress/compose';
@@ -19,9 +11,9 @@ import { useRefEffect } from '@wordpress/compose';
/**
* Internal dependencies
*/
-import { getPasteEventData } from '../../utils/pasting';
import { store as blockEditorStore } from '../../store';
import { useNotifyCopy } from '../../utils/use-notify-copy';
+import { getPasteBlocks, setClipboardBlocks } from './utils';
export default function useClipboardHandler() {
const {
@@ -112,29 +104,7 @@ export default function useClipboardHandler() {
blocks = [ head, ...inBetweenBlocks, tail ];
}
- const wrapperBlockName = event.clipboardData.getData(
- '__unstableWrapperBlockName'
- );
-
- if ( wrapperBlockName ) {
- blocks = createBlock(
- wrapperBlockName,
- JSON.parse(
- event.clipboardData.getData(
- '__unstableWrapperBlockAttributes'
- )
- ),
- blocks
- );
- }
-
- const serialized = serialize( blocks );
-
- event.clipboardData.setData(
- 'text/plain',
- toPlainText( serialized )
- );
- event.clipboardData.setData( 'text/html', serialized );
+ setClipboardBlocks( event, blocks );
}
}
@@ -153,35 +123,10 @@ export default function useClipboardHandler() {
__experimentalCanUserUseUnfilteredHTML:
canUserUseUnfilteredHTML,
} = getSettings();
- const { plainText, html, files } = getPasteEventData( event );
- let blocks = [];
-
- if ( files.length ) {
- const fromTransforms = getBlockTransforms( 'from' );
- blocks = files
- .reduce( ( accumulator, file ) => {
- const transformation = findTransform(
- fromTransforms,
- ( transform ) =>
- transform.type === 'files' &&
- transform.isMatch( [ file ] )
- );
- if ( transformation ) {
- accumulator.push(
- transformation.transform( [ file ] )
- );
- }
- return accumulator;
- }, [] )
- .flat();
- } else {
- blocks = pasteHandler( {
- HTML: html,
- plainText,
- mode: 'BLOCKS',
- canUserUseUnfilteredHTML,
- } );
- }
+ const blocks = getPasteBlocks(
+ event,
+ canUserUseUnfilteredHTML
+ );
if ( selectedBlockClientIds.length === 1 ) {
const [ selectedBlockClientId ] = selectedBlockClientIds;
@@ -223,20 +168,3 @@ export default function useClipboardHandler() {
};
}, [] );
}
-
-/**
- * Given a string of HTML representing serialized blocks, returns the plain
- * text extracted after stripping the HTML of any tags and fixing line breaks.
- *
- * @param {string} html Serialized blocks.
- * @return {string} The plain-text content with any html removed.
- */
-function toPlainText( html ) {
- // Manually handle BR tags as line breaks prior to `stripHTML` call
- html = html.replace( /
/g, '\n' );
-
- const plainText = stripHTML( html ).trim();
-
- // Merge any consecutive line breaks
- return plainText.replace( /\n\n+/g, '\n\n' );
-}
diff --git a/packages/block-editor/src/components/writing-flow/utils.js b/packages/block-editor/src/components/writing-flow/utils.js
new file mode 100644
index 00000000000000..b0e230fbb1b2b9
--- /dev/null
+++ b/packages/block-editor/src/components/writing-flow/utils.js
@@ -0,0 +1,89 @@
+/**
+ * WordPress dependencies
+ */
+import { __unstableStripHTML as stripHTML } from '@wordpress/dom';
+import {
+ serialize,
+ createBlock,
+ pasteHandler,
+ findTransform,
+ getBlockTransforms,
+} from '@wordpress/blocks';
+
+/**
+ * Internal dependencies
+ */
+import { getPasteEventData } from '../../utils/pasting';
+
+export function setClipboardBlocks( event, blocks ) {
+ let _blocks = blocks;
+ const wrapperBlockName = event.clipboardData.getData(
+ '__unstableWrapperBlockName'
+ );
+
+ if ( wrapperBlockName ) {
+ _blocks = createBlock(
+ wrapperBlockName,
+ JSON.parse(
+ event.clipboardData.getData(
+ '__unstableWrapperBlockAttributes'
+ )
+ ),
+ _blocks
+ );
+ }
+
+ const serialized = serialize( _blocks );
+
+ event.clipboardData.setData( 'text/plain', toPlainText( serialized ) );
+ event.clipboardData.setData( 'text/html', serialized );
+}
+
+export function getPasteBlocks( event, canUserUseUnfilteredHTML ) {
+ const { plainText, html, files } = getPasteEventData( event );
+ let blocks = [];
+
+ if ( files.length ) {
+ const fromTransforms = getBlockTransforms( 'from' );
+ blocks = files
+ .reduce( ( accumulator, file ) => {
+ const transformation = findTransform(
+ fromTransforms,
+ ( transform ) =>
+ transform.type === 'files' &&
+ transform.isMatch( [ file ] )
+ );
+ if ( transformation ) {
+ accumulator.push( transformation.transform( [ file ] ) );
+ }
+ return accumulator;
+ }, [] )
+ .flat();
+ } else {
+ blocks = pasteHandler( {
+ HTML: html,
+ plainText,
+ mode: 'BLOCKS',
+ canUserUseUnfilteredHTML,
+ } );
+ }
+
+ return blocks;
+}
+
+/**
+ * Given a string of HTML representing serialized blocks, returns the plain
+ * text extracted after stripping the HTML of any tags and fixing line breaks.
+ *
+ * @param {string} html Serialized blocks.
+ * @return {string} The plain-text content with any html removed.
+ */
+function toPlainText( html ) {
+ // Manually handle BR tags as line breaks prior to `stripHTML` call
+ html = html.replace( /
/g, '\n' );
+
+ const plainText = stripHTML( html ).trim();
+
+ // Merge any consecutive line breaks
+ return plainText.replace( /\n\n+/g, '\n\n' );
+}
From 3238c6ac7b6e422392d1365e6b0084a00bc95b87 Mon Sep 17 00:00:00 2001
From: Andrew Serong <14988353+andrewserong@users.noreply.github.com>
Date: Wed, 24 Jan 2024 16:02:23 +1100
Subject: [PATCH 08/10] Add comment references the other useClipboardHandler
---
.../src/components/list-view/use-clipboard-handler.js | 3 +++
1 file changed, 3 insertions(+)
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 3145f18934b452..6b2ce85faa1154 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
@@ -12,6 +12,9 @@ import { useNotifyCopy } from '../../utils/use-notify-copy';
import { focusListItem } from './utils';
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. In the future,
+// consider refactoring the two hooks to share more code.
export default function useClipboardHandler( { selectBlock } ) {
const {
getBlockOrder,
From cc37c0bce33f29947762935a7e93ff428f5dee3b Mon Sep 17 00:00:00 2001
From: Andrew Serong <14988353+andrewserong@users.noreply.github.com>
Date: Wed, 24 Jan 2024 16:11:46 +1100
Subject: [PATCH 09/10] Add JSDoc comments for the new utility functions
---
.../src/components/writing-flow/utils.js | 14 ++++++++++++++
1 file changed, 14 insertions(+)
diff --git a/packages/block-editor/src/components/writing-flow/utils.js b/packages/block-editor/src/components/writing-flow/utils.js
index b0e230fbb1b2b9..ef1827077ccbf1 100644
--- a/packages/block-editor/src/components/writing-flow/utils.js
+++ b/packages/block-editor/src/components/writing-flow/utils.js
@@ -15,6 +15,13 @@ import {
*/
import { getPasteEventData } from '../../utils/pasting';
+/**
+ * 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.
+ */
export function setClipboardBlocks( event, blocks ) {
let _blocks = blocks;
const wrapperBlockName = event.clipboardData.getData(
@@ -39,6 +46,13 @@ export function setClipboardBlocks( event, blocks ) {
event.clipboardData.setData( 'text/html', serialized );
}
+/**
+ * Returns the blocks to be pasted from the clipboard event.
+ *
+ * @param {ClipboardEvent} event The clipboard event.
+ * @param {boolean} canUserUseUnfilteredHTML Whether the user can or can't post unfiltered HTML.
+ * @return {Array|string} A list of blocks or a string, depending on `handlerMode`.
+ */
export function getPasteBlocks( event, canUserUseUnfilteredHTML ) {
const { plainText, html, files } = getPasteEventData( event );
let blocks = [];
From bf2d13f9b92bfc6c6c3419b08d575680010258b4 Mon Sep 17 00:00:00 2001
From: Andrew Serong <14988353+andrewserong@users.noreply.github.com>
Date: Thu, 25 Jan 2024 10:03:51 +1100
Subject: [PATCH 10/10] Tidy up comment
---
.../src/components/list-view/use-clipboard-handler.js | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
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 6b2ce85faa1154..cd25c71e9bf7c4 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
@@ -13,8 +13,7 @@ import { focusListItem } from './utils';
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. In the future,
-// consider refactoring the two hooks to share more code.
+// and adds behaviour for the list view, while skipping partial selection.
export default function useClipboardHandler( { selectBlock } ) {
const {
getBlockOrder,