UNPKG

@wordpress/block-editor

Version:
240 lines (203 loc) 8.42 kB
import { createElement } from "@wordpress/element"; /** * 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 createElement("div", { ref: useClipboardHandler() }, children); } /** * 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; //# sourceMappingURL=index.js.map