@gechiui/block-editor
Version:
282 lines (244 loc) • 7.3 kB
JavaScript
/**
* GeChiUI dependencies
*/
import { useRef } from '@gechiui/element';
import { useRefEffect } from '@gechiui/compose';
import { getFilesFromDataTransfer } from '@gechiui/dom';
import { pasteHandler } from '@gechiui/blocks';
import {
isEmpty,
insert,
create,
replace,
__UNSTABLE_LINE_SEPARATOR as LINE_SEPARATOR,
} from '@gechiui/rich-text';
import { isURL } from '@gechiui/url';
/**
* Internal dependencies
*/
import { filePasteHandler } from './file-paste-handler';
import { addActiveFormats, isShortcode } from './utils';
import { splitValue } from './split-value';
/** @typedef {import('@gechiui/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 ) {
event.preventDefault();
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;
}
// Only process file if no HTML is present.
// Note: a pasted file may have the URL as plain text.
if ( files && files.length && ! html ) {
const content = pasteHandler( {
HTML: filePasteHandler( files ),
mode: 'BLOCKS',
tagName,
preserveWhiteSpace,
} );
// 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 );
if ( onReplace && isEmpty( value ) ) {
onReplace( content );
} else {
splitValue( {
value,
pastedBlocks: content,
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 preceeding and trailing whitespace.
*
* @param {string} html the html to be normalized
* @return {string} the normalized html
*/
function removeWindowsFragments( html ) {
const startReg = /.*<!--StartFragment-->/s;
const endReg = /<!--EndFragment-->.*/s;
return html.replace( startReg, '' ).replace( endReg, '' );
}
/**
* Removes the charset meta tag inserted by Chromium.
* See:
* - https://github.com/GeChiUI/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;
}