UNPKG

@wordpress/block-editor

Version:
286 lines (243 loc) 8.52 kB
/** * WordPress dependencies */ import { useRef } from '@wordpress/element'; import { useRefEffect } from '@wordpress/compose'; import { getFilesFromDataTransfer } from '@wordpress/dom'; import { pasteHandler, findTransform, getBlockTransforms } from '@wordpress/blocks'; import { isEmpty, insert, create, replace, __UNSTABLE_LINE_SEPARATOR as LINE_SEPARATOR } from '@wordpress/rich-text'; import { isURL } from '@wordpress/url'; /** * Internal dependencies */ import { addActiveFormats, isShortcode } from './utils'; import { splitValue } from './split-value'; import { shouldDismissPastedFiles } from '../../utils/pasting'; /** @typedef {import('@wordpress/rich-text').RichTextValue} RichTextValue */ /** * Replaces line separators with line breaks if not multiline. * Replaces line breaks with line separators if multiline. * * @param {RichTextValue} value Value to adjust. * @param {boolean} isMultiline Whether to adjust to multiline or not. * * @return {RichTextValue} Adjusted value. */ function adjustLines(value, isMultiline) { if (isMultiline) { return replace(value, /\n+/g, LINE_SEPARATOR); } return replace(value, new RegExp(LINE_SEPARATOR, 'g'), '\n'); } export function usePasteHandler(props) { const propsRef = useRef(props); propsRef.current = props; return useRefEffect(element => { function _onPaste(event) { const { isSelected, disableFormats, onChange, value, formatTypes, tagName, onReplace, onSplit, onSplitMiddle, __unstableEmbedURLOnPaste, multilineTag, preserveWhiteSpace, pastePlainText } = propsRef.current; if (!isSelected) { return; } const { clipboardData } = event; let plainText = ''; let html = ''; // IE11 only supports `Text` as an argument for `getData` and will // otherwise throw an invalid argument error, so we try the standard // arguments first, then fallback to `Text` if they fail. try { plainText = clipboardData.getData('text/plain'); html = clipboardData.getData('text/html'); } catch (error1) { try { html = clipboardData.getData('Text'); } catch (error2) { // Some browsers like UC Browser paste plain text by default and // don't support clipboardData at all, so allow default // behaviour. return; } } // Remove Windows-specific metadata appended within copied HTML text. html = removeWindowsFragments(html); // Strip meta tag. html = removeCharsetMetaTag(html); event.preventDefault(); // Allows us to ask for this information when we get a report. window.console.log('Received HTML:\n\n', html); window.console.log('Received plain text:\n\n', plainText); if (disableFormats) { onChange(insert(value, plainText)); return; } const transformed = formatTypes.reduce((accumlator, { __unstablePasteRule }) => { // Only allow one transform. if (__unstablePasteRule && accumlator === value) { accumlator = __unstablePasteRule(value, { html, plainText }); } return accumlator; }, value); if (transformed !== value) { onChange(transformed); return; } const files = [...getFilesFromDataTransfer(clipboardData)]; const isInternal = clipboardData.getData('rich-text') === 'true'; // If the data comes from a rich text instance, we can directly use it // without filtering the data. The filters are only meant for externally // pasted content and remove inline styles. if (isInternal) { const pastedMultilineTag = clipboardData.getData('rich-text-multi-line-tag') || undefined; let pastedValue = create({ html, multilineTag: pastedMultilineTag, multilineWrapperTags: pastedMultilineTag === 'li' ? ['ul', 'ol'] : undefined, preserveWhiteSpace }); pastedValue = adjustLines(pastedValue, !!multilineTag); addActiveFormats(pastedValue, value.activeFormats); onChange(insert(value, pastedValue)); return; } if (pastePlainText) { onChange(insert(value, create({ text: plainText }))); return; } if (files?.length) { // Allows us to ask for this information when we get a report. // eslint-disable-next-line no-console window.console.log('Received items:\n\n', files); } // Process any attached files, unless we infer that the files in // question are redundant "screenshots" of the actual HTML payload, // as created by certain office-type programs. // // @see shouldDismissPastedFiles if (files?.length && !shouldDismissPastedFiles(files, html, plainText)) { const fromTransforms = getBlockTransforms('from'); const 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(); if (!blocks.length) { return; } if (onReplace && isEmpty(value)) { onReplace(blocks); } else { splitValue({ value, pastedBlocks: blocks, onReplace, onSplit, onSplitMiddle, multilineTag }); } return; } let mode = onReplace && onSplit ? 'AUTO' : 'INLINE'; // Force the blocks mode when the user is pasting // on a new line & the content resembles a shortcode. // Otherwise it's going to be detected as inline // and the shortcode won't be replaced. if (mode === 'AUTO' && isEmpty(value) && isShortcode(plainText)) { mode = 'BLOCKS'; } if (__unstableEmbedURLOnPaste && isEmpty(value) && isURL(plainText.trim())) { mode = 'BLOCKS'; } const content = pasteHandler({ HTML: html, plainText, mode, tagName, preserveWhiteSpace }); if (typeof content === 'string') { let valueToInsert = create({ html: content }); // If the content should be multiline, we should process text // separated by a line break as separate lines. valueToInsert = adjustLines(valueToInsert, !!multilineTag); addActiveFormats(valueToInsert, value.activeFormats); onChange(insert(value, valueToInsert)); } else if (content.length > 0) { if (onReplace && isEmpty(value)) { onReplace(content, content.length - 1, -1); } else { splitValue({ value, pastedBlocks: content, onReplace, onSplit, onSplitMiddle, multilineTag }); } } } element.addEventListener('paste', _onPaste); return () => { element.removeEventListener('paste', _onPaste); }; }, []); } /** * Normalizes a given string of HTML to remove the Windows-specific "Fragment" * comments and any preceding and trailing content. * * @param {string} html the html to be normalized * @return {string} the normalized html */ function removeWindowsFragments(html) { const startStr = '<!--StartFragment-->'; const startIdx = html.indexOf(startStr); if (startIdx > -1) { html = html.substring(startIdx + startStr.length); } else { // No point looking for EndFragment return html; } const endStr = '<!--EndFragment-->'; const endIdx = html.indexOf(endStr); if (endIdx > -1) { html = html.substring(0, endIdx); } return html; } /** * Removes the charset meta tag inserted by Chromium. * See: * - https://github.com/WordPress/gutenberg/issues/33585 * - https://bugs.chromium.org/p/chromium/issues/detail?id=1264616#c4 * * @param {string} html the html to be stripped of the meta tag. * @return {string} the cleaned html */ function removeCharsetMetaTag(html) { const metaTag = `<meta charset='utf-8'>`; if (html.startsWith(metaTag)) { return html.slice(metaTag.length); } return html; } //# sourceMappingURL=use-paste-handler.js.map