Skip to content

Commit

Permalink
Writing flow: absorb clipboard handler (WordPress#55006)
Browse files Browse the repository at this point in the history
  • Loading branch information
ellatrix authored Oct 4, 2023
1 parent 1be856b commit 1710fac
Show file tree
Hide file tree
Showing 11 changed files with 341 additions and 307 deletions.
2 changes: 2 additions & 0 deletions packages/block-editor/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased

- Deprecated `CopyHandler`, absorbed into `WritingFlow`.

## 12.10.0 (2023-09-20)

- The Deprecated multiline prop on RichText will now fall back to using multiple
Expand Down
6 changes: 4 additions & 2 deletions packages/block-editor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -334,9 +334,11 @@ _Related_

### CopyHandler

_Related_
> **Deprecated**
- <https://github.com/WordPress/gutenberg/blob/HEAD/packages/block-editor/src/components/copy-handler/README.md>
_Parameters_

- _props_ `Object`:

### createCustomColorsHOC

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
/**
* Internal dependencies
*/
import { useNotifyCopy } from '../copy-handler';
import { useNotifyCopy } from '../../utils/use-notify-copy';
import usePasteStyles from '../use-paste-styles';
import { store as blockEditorStore } from '../../store';

Expand Down
8 changes: 1 addition & 7 deletions packages/block-editor/src/components/block-canvas/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import EditorStyles from '../editor-styles';
import Iframe from '../iframe';
import WritingFlow from '../writing-flow';
import { useMouseMoveTypingReset } from '../observe-typing';
import { useClipboardHandler } from '../copy-handler';
import { useBlockSelectionClearer } from '../block-selection-clearer';

export function ExperimentalBlockCanvas( {
Expand All @@ -23,13 +22,8 @@ export function ExperimentalBlockCanvas( {
iframeProps,
} ) {
const resetTypingRef = useMouseMoveTypingReset();
const copyHandler = useClipboardHandler();
const clearerRef = useBlockSelectionClearer();
const contentRef = useMergeRefs( [
copyHandler,
contentRefProp,
clearerRef,
] );
const contentRef = useMergeRefs( [ contentRefProp, clearerRef ] );

if ( ! shouldIframe ) {
return (
Expand Down
2 changes: 2 additions & 0 deletions packages/block-editor/src/components/copy-handler/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Copy Handler

**Deprecated**

The `CopyHandler` component handles the copy/cut and paste events of its children blocks. It handles multiple selection of blocks as well.

Concretely, it handles the display of success messages and takes care of copying the block to the clipboard. It uses for that the [serialize function](https://github.com/WordPress/gutenberg/blob/HEAD/packages/blocks/src/api/serializer.js), which outputs HTML augmented with the HTML-comment demarcations to denote blocks.
Expand Down
312 changes: 21 additions & 291 deletions packages/block-editor/src/components/copy-handler/index.js
Original file line number Diff line number Diff line change
@@ -1,304 +1,34 @@
/**
* WordPress dependencies
*/
import { useCallback } from '@wordpress/element';
import {
serialize,
pasteHandler,
store as blocksStore,
createBlock,
findTransform,
getBlockTransforms,
} from '@wordpress/blocks';
import {
documentHasSelection,
documentHasUncollapsedSelection,
__unstableStripHTML as stripHTML,
} from '@wordpress/dom';
import { useDispatch, useSelect } from '@wordpress/data';
import { __, _n, sprintf } from '@wordpress/i18n';
import { store as noticesStore } from '@wordpress/notices';
import { useRefEffect } from '@wordpress/compose';
import deprecated from '@wordpress/deprecated';

/**
* Internal dependencies
*/
import { getPasteEventData } from '../../utils/pasting';
import { store as blockEditorStore } from '../../store';

export function useNotifyCopy() {
const { getBlockName } = useSelect( blockEditorStore );
const { getBlockType } = useSelect( blocksStore );
const { createSuccessNotice } = useDispatch( noticesStore );

return useCallback( ( eventType, selectedBlockClientIds ) => {
let notice = '';
if ( selectedBlockClientIds.length === 1 ) {
const clientId = selectedBlockClientIds[ 0 ];
const title = getBlockType( getBlockName( clientId ) )?.title;
notice =
eventType === 'copy'
? sprintf(
// Translators: Name of the block being copied, e.g. "Paragraph".
__( 'Copied "%s" to clipboard.' ),
title
)
: sprintf(
// Translators: Name of the block being cut, e.g. "Paragraph".
__( 'Moved "%s" to clipboard.' ),
title
);
} else {
notice =
eventType === 'copy'
? sprintf(
// Translators: %d: Number of blocks being copied.
_n(
'Copied %d block to clipboard.',
'Copied %d blocks to clipboard.',
selectedBlockClientIds.length
),
selectedBlockClientIds.length
)
: sprintf(
// Translators: %d: Number of blocks being cut.
_n(
'Moved %d block to clipboard.',
'Moved %d blocks to clipboard.',
selectedBlockClientIds.length
),
selectedBlockClientIds.length
);
}
createSuccessNotice( notice, {
type: 'snackbar',
} );
}, [] );
}

export function useClipboardHandler() {
const {
getBlocksByClientId,
getSelectedBlockClientIds,
hasMultiSelection,
getSettings,
__unstableIsFullySelected,
__unstableIsSelectionCollapsed,
__unstableIsSelectionMergeable,
__unstableGetSelectedBlocksWithPartialSelection,
canInsertBlockType,
} = useSelect( blockEditorStore );
const {
flashBlock,
removeBlocks,
replaceBlocks,
__unstableDeleteSelection,
__unstableExpandSelection,
insertBlocks,
} = useDispatch( blockEditorStore );
const notifyCopy = useNotifyCopy();

return useRefEffect( ( node ) => {
function handler( event ) {
if ( event.defaultPrevented ) {
// This was likely already handled in rich-text/use-paste-handler.js.
return;
}

const selectedBlockClientIds = getSelectedBlockClientIds();

if ( selectedBlockClientIds.length === 0 ) {
return;
}

// Always handle multiple selected blocks.
if ( ! hasMultiSelection() ) {
const { target } = event;
const { ownerDocument } = target;
// If copying, only consider actual text selection as selection.
// Otherwise, any focus on an input field is considered.
const hasSelection =
event.type === 'copy' || event.type === 'cut'
? documentHasUncollapsedSelection( ownerDocument )
: documentHasSelection( ownerDocument );

// Let native copy behaviour take over in input fields.
if ( hasSelection ) {
return;
}
}

if ( ! node.contains( event.target.ownerDocument.activeElement ) ) {
return;
}

event.preventDefault();

const isSelectionMergeable = __unstableIsSelectionMergeable();
const shouldHandleWholeBlocks =
__unstableIsSelectionCollapsed() || __unstableIsFullySelected();
const expandSelectionIsNeeded =
! shouldHandleWholeBlocks && ! isSelectionMergeable;
if ( event.type === 'copy' || event.type === 'cut' ) {
if ( selectedBlockClientIds.length === 1 ) {
flashBlock( selectedBlockClientIds[ 0 ] );
}
// If we have a partial selection that is not mergeable, just
// expand the selection to the whole blocks.
if ( expandSelectionIsNeeded ) {
__unstableExpandSelection();
} else {
notifyCopy( event.type, selectedBlockClientIds );
let blocks;
// Check if we have partial selection.
if ( shouldHandleWholeBlocks ) {
blocks = getBlocksByClientId( selectedBlockClientIds );
} else {
const [ head, tail ] =
__unstableGetSelectedBlocksWithPartialSelection();
const inBetweenBlocks = getBlocksByClientId(
selectedBlockClientIds.slice(
1,
selectedBlockClientIds.length - 1
)
);
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 );
}
}

if ( event.type === 'cut' ) {
// We need to also check if at the start we needed to
// expand the selection, as in this point we might have
// programmatically fully selected the blocks above.
if ( shouldHandleWholeBlocks && ! expandSelectionIsNeeded ) {
removeBlocks( selectedBlockClientIds );
} else {
event.target.ownerDocument.activeElement.contentEditable = false;
__unstableDeleteSelection();
}
} 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 );
};
}, [] );
}

function CopyHandler( { children } ) {
return <div ref={ useClipboardHandler() }>{ children }</div>;
}
import useClipboardHandler from '../writing-flow/use-clipboard-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.
* @deprecated
*/
function toPlainText( html ) {
// Manually handle BR tags as line breaks prior to `stripHTML` call
html = html.replace( /<br>/g, '\n' );

const plainText = stripHTML( html ).trim();

// Merge any consecutive line breaks
return plainText.replace( /\n\n+/g, '\n\n' );
}
export const __unstableUseClipboardHandler = () => {
deprecated( '__unstableUseClipboardHandler', {
alternative: 'BlockCanvas or WritingFlow',
since: '6.4',
version: '6.7',
} );
return useClipboardHandler();
};

/**
* @see https://github.com/WordPress/gutenberg/blob/HEAD/packages/block-editor/src/components/copy-handler/README.md
* @deprecated
* @param {Object} props
*/
export default CopyHandler;
export default function CopyHandler( props ) {
deprecated( 'CopyHandler', {
alternative: 'BlockCanvas or WritingFlow',
since: '6.4',
version: '6.7',
} );
return <div { ...props } ref={ useClipboardHandler() } />;
}
2 changes: 1 addition & 1 deletion packages/block-editor/src/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ export { default as BlockToolbar } from './block-toolbar';
export { default as BlockTools } from './block-tools';
export {
default as CopyHandler,
useClipboardHandler as __unstableUseClipboardHandler,
__unstableUseClipboardHandler,
} from './copy-handler';
export { default as DefaultBlockAppender } from './default-block-appender';
export { default as __unstableEditorStyles } from './editor-styles';
Expand Down
Loading

0 comments on commit 1710fac

Please sign in to comment.