@wordpress/block-editor
Version:
319 lines (280 loc) • 8.15 kB
JavaScript
/**
* 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;
}