UNPKG

@wordpress/block-editor

Version:
319 lines (280 loc) 8.15 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; }