UNPKG

@wordpress/block-editor

Version:
304 lines (276 loc) 8.44 kB
/** * 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'; /** * 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 ) { 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; } const eventDefaultPrevented = event.defaultPrevented; 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 { __unstableDeleteSelection(); } } else if ( event.type === 'paste' ) { if ( eventDefaultPrevented ) { // This was likely already handled in rich-text/use-paste-handler.js. return; } 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>; } /** * 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( /<br>/g, '\n' ); const plainText = stripHTML( html ).trim(); // Merge any consecutive line breaks return plainText.replace( /\n\n+/g, '\n\n' ); } /** * @see https://github.com/WordPress/gutenberg/blob/HEAD/packages/block-editor/src/components/copy-handler/README.md */ export default CopyHandler;