From f0d6fb8d8dc7cf51bc8955c402f1d50d86ed1ab4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20van=C2=A0Durpe?= Date: Wed, 12 May 2021 17:30:35 +0300 Subject: [PATCH] Rich text core: remove dependency on block client ID and other block specific behaviour (#31752) --- .../src/components/rich-text/format-edit.js | 44 +++ .../src/components/rich-text/index.js | 264 ++++++------------ .../components/rich-text/use-format-types.js | 150 ++++++++++ .../components/rich-text/use-input-rules.js | 106 +++++++ .../components/rich-text/use-paste-handler.js | 209 ++++++++++++++ packages/rich-text/src/component/index.js | 112 ++------ .../src/component/use-input-and-selection.js | 34 --- .../src/component/use-paste-handler.js | 112 -------- 8 files changed, 615 insertions(+), 416 deletions(-) create mode 100644 packages/block-editor/src/components/rich-text/format-edit.js create mode 100644 packages/block-editor/src/components/rich-text/use-format-types.js create mode 100644 packages/block-editor/src/components/rich-text/use-input-rules.js create mode 100644 packages/block-editor/src/components/rich-text/use-paste-handler.js delete mode 100644 packages/rich-text/src/component/use-paste-handler.js diff --git a/packages/block-editor/src/components/rich-text/format-edit.js b/packages/block-editor/src/components/rich-text/format-edit.js new file mode 100644 index 0000000000000..75b077ab321d4 --- /dev/null +++ b/packages/block-editor/src/components/rich-text/format-edit.js @@ -0,0 +1,44 @@ +/** + * WordPress dependencies + */ +import { getActiveFormat, getActiveObject } from '@wordpress/rich-text'; + +export default function FormatEdit( { + formatTypes, + onChange, + onFocus, + value, + forwardedRef, +} ) { + return formatTypes.map( ( settings ) => { + const { name, edit: Edit } = settings; + + if ( ! Edit ) { + return null; + } + + const activeFormat = getActiveFormat( value, name ); + const isActive = activeFormat !== undefined; + const activeObject = getActiveObject( value ); + const isObjectActive = + activeObject !== undefined && activeObject.type === name; + + return ( + + ); + } ); +} diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index 555dc569394db..7abf9033da847 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -10,7 +10,6 @@ import { omit } from 'lodash'; import { RawHTML, useRef, useCallback, forwardRef } from '@wordpress/element'; import { useDispatch, useSelect } from '@wordpress/data'; import { - pasteHandler, children as childrenSource, getBlockTransforms, findTransform, @@ -23,16 +22,12 @@ import { __unstableIsEmptyLine as isEmptyLine, insert, __unstableInsertLineSeparator as insertLineSeparator, - create, - replace, split, - __UNSTABLE_LINE_SEPARATOR as LINE_SEPARATOR, toHTMLString, - slice, isCollapsed, + removeFormat, } from '@wordpress/rich-text'; import deprecated from '@wordpress/deprecated'; -import { isURL } from '@wordpress/url'; import { BACKSPACE, DELETE, ENTER } from '@wordpress/keycodes'; /** @@ -41,17 +36,15 @@ import { BACKSPACE, DELETE, ENTER } from '@wordpress/keycodes'; import { useBlockEditorAutocompleteProps } from '../autocomplete'; import { useBlockEditContext } from '../block-edit'; import { RemoveBrowserShortcuts } from './remove-browser-shortcuts'; -import { filePasteHandler } from './file-paste-handler'; import FormatToolbarContainer from './format-toolbar-container'; import { store as blockEditorStore } from '../../store'; import { useUndoAutomaticChange } from './use-undo-automatic-change'; import { useCaretInFormat } from './use-caret-in-format'; -import { - addActiveFormats, - getMultilineTag, - getAllowedFormats, - isShortcode, -} from './utils'; +import { usePasteHandler } from './use-paste-handler'; +import { useInputRules } from './use-input-rules'; +import { useFormatTypes } from './use-format-types'; +import FormatEdit from './format-edit'; +import { getMultilineTag, getAllowedFormats } from './utils'; function RichTextWrapper( { @@ -240,163 +233,48 @@ function RichTextWrapper( [ onReplace, onSplit, multilineTag, onSplitMiddle ] ); - const onPaste = useCallback( - ( { - value, - onChange, - html, - plainText, - isInternal, - files, - activeFormats, - } ) => { - // 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 pastedValue = create( { - html, - multilineTag, - multilineWrapperTags: - multilineTag === 'li' ? [ 'ul', 'ol' ] : undefined, - preserveWhiteSpace, - } ); - addActiveFormats( pastedValue, 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, content ); - } - - 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 } ); - - addActiveFormats( valueToInsert, activeFormats ); - - // If the content should be multiline, we should process text - // separated by a line break as separate lines. - if ( multilineTag ) { - valueToInsert = replace( - valueToInsert, - /\n+/g, - LINE_SEPARATOR - ); - } - - onChange( insert( value, valueToInsert ) ); - } else if ( content.length > 0 ) { - if ( onReplace && isEmpty( value ) ) { - onReplace( content, content.length - 1, -1 ); - } else { - splitValue( value, content ); - } - } - }, - [ - tagName, - onReplace, - onSplit, - splitValue, - __unstableEmbedURLOnPaste, - multilineTag, - preserveWhiteSpace, - pastePlainText, - ] - ); - - const inputRule = useCallback( - ( value, valueToFormat ) => { - if ( ! onReplace ) { - return; - } - - const { start, text } = value; - const characterBefore = text.slice( start - 1, start ); - - // The character right before the caret must be a plain space. - if ( characterBefore !== ' ' ) { - return; - } + const { + formatTypes, + prepareHandlers, + valueHandlers, + changeHandlers, + dependencies, + } = useFormatTypes( { + clientId, + identifier, + withoutInteractiveFormatting, + allowedFormats: adjustedAllowedFormats, + } ); - const trimmedTextBefore = text.slice( 0, start ).trim(); - const prefixTransforms = getBlockTransforms( 'from' ).filter( - ( { type } ) => type === 'prefix' - ); - const transformation = findTransform( - prefixTransforms, - ( { prefix } ) => { - return trimmedTextBefore === prefix; - } - ); + function addEditorOnlyFormats( value ) { + return valueHandlers.reduce( + ( accumulator, fn ) => fn( accumulator, value.text ), + value.formats + ); + } - if ( ! transformation ) { - return; + function removeEditorOnlyFormats( value ) { + formatTypes.forEach( ( formatType ) => { + // Remove formats created by prepareEditableTree, because they are editor only. + if ( formatType.__experimentalCreatePrepareEditableTree ) { + value = removeFormat( + value, + formatType.name, + 0, + value.text.length + ); } + } ); - const content = valueToFormat( slice( value, start, text.length ) ); - const block = transformation.transform( content ); + return value.formats; + } - onReplace( [ block ] ); - __unstableMarkAutomaticChange(); - }, - [ onReplace, __unstableMarkAutomaticChange ] - ); + function addInvisibleFormats( value ) { + return prepareHandlers.reduce( + ( accumulator, fn ) => fn( accumulator, value.text ), + value.formats + ); + } const { value, @@ -404,28 +282,27 @@ function RichTextWrapper( onFocus, ref: richTextRef, hasActiveFormats, - removeEditorOnlyFormats, - children: richTextChildren, } = useRichText( { - clientId, - identifier, value: adjustedValue, - onChange: adjustedOnChange, + onChange( html, { __unstableFormats, __unstableText } ) { + adjustedOnChange( html ); + Object.values( changeHandlers ).forEach( ( changeHandler ) => { + changeHandler( __unstableFormats, __unstableText ); + } ); + }, selectionStart, selectionEnd, onSelectionChange, placeholder, - allowedFormats: adjustedAllowedFormats, - withoutInteractiveFormatting, - onPaste, __unstableIsSelected: isSelected, - __unstableInputRule: inputRule, __unstableMultilineTag: multilineTag, __unstableOnCreateUndoLevel: __unstableMarkLastChangeAsPersistent, - __unstableMarkAutomaticChange, __unstableDisableFormats: disableFormats, preserveWhiteSpace, - __unstableAllowPrefixTransformations, + __unstableDependencies: dependencies, + __unstableAfterParse: addEditorOnlyFormats, + __unstableBeforeSerialize: removeEditorOnlyFormats, + __unstableAddInvisibleFormats: addInvisibleFormats, } ); const autocompleteProps = useBlockEditorAutocompleteProps( { onReplace, @@ -446,7 +323,8 @@ function RichTextWrapper( if ( event.keyCode === ENTER ) { event.preventDefault(); - const _value = removeEditorOnlyFormats( value ); + const _value = { ...value }; + _value.formats = removeEditorOnlyFormats( value ); const canSplit = onReplace && onSplit; if ( onReplace ) { @@ -528,7 +406,15 @@ function RichTextWrapper( { children && children( { value, onChange, onFocus } ) } { isSelected && } { isSelected && autocompleteProps.children } - { isSelected && richTextChildren } + { isSelected && ( + + ) } { isSelected && hasFormats && ( { + return allFormatTypes.filter( ( { name, tagName } ) => { + if ( allowedFormats && ! allowedFormats.includes( name ) ) { + return false; + } + + if ( + withoutInteractiveFormatting && + interactiveContentTags.has( tagName ) + ) { + return false; + } + + return true; + } ); + }, [ allFormatTypes, allowedFormats, interactiveContentTags ] ); + const keyedSelected = useSelect( + ( select ) => + formatTypes.reduce( ( accumulator, type ) => { + if ( type.__experimentalGetPropsForEditableTreePreparation ) { + accumulator[ + type.name + ] = type.__experimentalGetPropsForEditableTreePreparation( + select, + { + richTextIdentifier: identifier, + blockClientId: clientId, + } + ); + } + + return accumulator; + }, {} ), + [ formatTypes, clientId, identifier ] + ); + const dispatch = useDispatch(); + const prepareHandlers = []; + const valueHandlers = []; + const changeHandlers = []; + const dependencies = []; + + formatTypes.forEach( ( type ) => { + if ( type.__experimentalCreatePrepareEditableTree ) { + const selected = keyedSelected[ type.name ]; + const handler = type.__experimentalCreatePrepareEditableTree( + selected, + { + richTextIdentifier: identifier, + blockClientId: clientId, + } + ); + + if ( type.__experimentalCreateOnChangeEditableValue ) { + valueHandlers.push( handler ); + } else { + prepareHandlers.push( handler ); + } + + for ( const key in selected ) { + dependencies.push( selected[ key ] ); + } + } + + if ( type.__experimentalCreateOnChangeEditableValue ) { + let dispatchers = {}; + + if ( type.__experimentalGetPropsForEditableTreeChangeHandler ) { + dispatchers = type.__experimentalGetPropsForEditableTreeChangeHandler( + dispatch, + { + richTextIdentifier: identifier, + blockClientId: clientId, + } + ); + } + + changeHandlers.push( + type.__experimentalCreateOnChangeEditableValue( + { + ...( keyedSelected[ type.name ] || {} ), + ...dispatchers, + }, + { + richTextIdentifier: identifier, + blockClientId: clientId, + } + ) + ); + } + } ); + + return { + formatTypes, + prepareHandlers, + valueHandlers, + changeHandlers, + dependencies, + }; +} diff --git a/packages/block-editor/src/components/rich-text/use-input-rules.js b/packages/block-editor/src/components/rich-text/use-input-rules.js new file mode 100644 index 0000000000000..8715960ba32f4 --- /dev/null +++ b/packages/block-editor/src/components/rich-text/use-input-rules.js @@ -0,0 +1,106 @@ +/** + * WordPress dependencies + */ +import { useRef } from '@wordpress/element'; +import { useRefEffect } from '@wordpress/compose'; +import { slice, toHTMLString } from '@wordpress/rich-text'; +import { getBlockTransforms, findTransform } from '@wordpress/blocks'; +import { useDispatch } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { store as blockEditorStore } from '../../store'; + +export function useInputRules( props ) { + const { + __unstableMarkLastChangeAsPersistent, + __unstableMarkAutomaticChange, + } = useDispatch( blockEditorStore ); + const propsRef = useRef( props ); + propsRef.current = props; + return useRefEffect( ( element ) => { + function inputRule() { + const { value, onReplace } = propsRef.current; + + if ( ! onReplace ) { + return; + } + + const { start, text } = value; + const characterBefore = text.slice( start - 1, start ); + + // The character right before the caret must be a plain space. + if ( characterBefore !== ' ' ) { + return; + } + + const trimmedTextBefore = text.slice( 0, start ).trim(); + const prefixTransforms = getBlockTransforms( 'from' ).filter( + ( { type } ) => type === 'prefix' + ); + const transformation = findTransform( + prefixTransforms, + ( { prefix } ) => { + return trimmedTextBefore === prefix; + } + ); + + if ( ! transformation ) { + return; + } + + const content = toHTMLString( { + value: slice( value, start, text.length ), + } ); + const block = transformation.transform( content ); + + onReplace( [ block ] ); + __unstableMarkAutomaticChange(); + } + + function onInput( event ) { + const { inputType } = event; + const { + value, + onChange, + __unstableAllowPrefixTransformations, + formatTypes, + } = propsRef.current; + + // Only run input rules when inserting text. + if ( inputType !== 'insertText' ) { + return; + } + + if ( __unstableAllowPrefixTransformations && inputRule ) { + inputRule(); + } + + const transformed = formatTypes.reduce( + ( accumlator, { __unstableInputRule } ) => { + if ( __unstableInputRule ) { + accumlator = __unstableInputRule( accumlator ); + } + + return accumlator; + }, + value + ); + + if ( transformed !== value ) { + __unstableMarkLastChangeAsPersistent(); + onChange( { + ...transformed, + activeFormats: value.activeFormats, + } ); + __unstableMarkAutomaticChange(); + } + } + + element.addEventListener( 'input', onInput ); + return () => { + element.removeEventListener( 'input', onInput ); + }; + }, [] ); +} diff --git a/packages/block-editor/src/components/rich-text/use-paste-handler.js b/packages/block-editor/src/components/rich-text/use-paste-handler.js new file mode 100644 index 0000000000000..6e89e25d28566 --- /dev/null +++ b/packages/block-editor/src/components/rich-text/use-paste-handler.js @@ -0,0 +1,209 @@ +/** + * WordPress dependencies + */ +import { useRef } from '@wordpress/element'; +import { useRefEffect } from '@wordpress/compose'; +import { getFilesFromDataTransfer } from '@wordpress/dom'; +import { pasteHandler } 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 { filePasteHandler } from './file-paste-handler'; +import { addActiveFormats, isShortcode } from './utils'; + +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, + splitValue, + __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; + } + } + + 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 pastedValue = create( { + html, + multilineTag, + multilineWrapperTags: + multilineTag === 'li' ? [ 'ul', 'ol' ] : undefined, + preserveWhiteSpace, + } ); + 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, content ); + } + + 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 } ); + + addActiveFormats( valueToInsert, value.activeFormats ); + + // If the content should be multiline, we should process text + // separated by a line break as separate lines. + if ( multilineTag ) { + valueToInsert = replace( + valueToInsert, + /\n+/g, + LINE_SEPARATOR + ); + } + + onChange( insert( value, valueToInsert ) ); + } else if ( content.length > 0 ) { + if ( onReplace && isEmpty( value ) ) { + onReplace( content, content.length - 1, -1 ); + } else { + splitValue( value, content ); + } + } + } + + element.addEventListener( 'paste', _onPaste ); + return () => { + element.removeEventListener( 'paste', _onPaste ); + }; + }, [] ); +} diff --git a/packages/rich-text/src/component/index.js b/packages/rich-text/src/component/index.js index 31aae296745dd..b8893521f68d7 100644 --- a/packages/rich-text/src/component/index.js +++ b/packages/rich-text/src/component/index.js @@ -7,69 +7,41 @@ import { useMergeRefs, useRefEffect } from '@wordpress/compose'; /** * Internal dependencies */ -import FormatEdit from './format-edit'; import { create } from '../create'; import { apply } from '../to-dom'; import { toHTMLString } from '../to-html-string'; -import { removeFormat } from '../remove-format'; -import { useFormatTypes } from './use-format-types'; import { useDefaultStyle } from './use-default-style'; import { useBoundaryStyle } from './use-boundary-style'; import { useInlineWarning } from './use-inline-warning'; import { useCopyHandler } from './use-copy-handler'; import { useFormatBoundaries } from './use-format-boundaries'; import { useSelectObject } from './use-select-object'; -import { usePasteHandler } from './use-paste-handler'; import { useIndentListItemOnSpace } from './use-indent-list-item-on-space'; import { useInputAndSelection } from './use-input-and-selection'; import { useDelete } from './use-delete'; /** @typedef {import('@wordpress/element').WPSyntheticEvent} WPSyntheticEvent */ -function createPrepareEditableTree( fns ) { - return ( value ) => - fns.reduce( - ( accumulator, fn ) => fn( accumulator, value.text ), - value.formats - ); -} - export function useRichText( { value = '', selectionStart, selectionEnd, - allowedFormats, - withoutInteractiveFormatting, placeholder, preserveWhiteSpace, - onPaste, format = 'string', onSelectionChange, onChange, - clientId, - identifier, __unstableMultilineTag: multilineTag, __unstableDisableFormats: disableFormats, - __unstableInputRule: inputRule, - __unstableMarkAutomaticChange: markAutomaticChange, - __unstableAllowPrefixTransformations: allowPrefixTransformations, __unstableOnCreateUndoLevel: onCreateUndoLevel, __unstableIsSelected: isSelected, + __unstableDependencies, + __unstableAfterParse, + __unstableBeforeSerialize, + __unstableAddInvisibleFormats, } ) { const ref = useRef(); const [ activeFormats = [], setActiveFormats ] = useState(); - const { - formatTypes, - prepareHandlers, - valueHandlers, - changeHandlers, - dependencies, - } = useFormatTypes( { - clientId, - identifier, - withoutInteractiveFormatting, - allowedFormats, - } ); /** * Converts the outside data structure to our internal representation. @@ -91,8 +63,6 @@ export function useRichText( { return string; } - const prepare = createPrepareEditableTree( valueHandlers ); - const result = create( { html: string, multilineTag, @@ -101,32 +71,11 @@ export function useRichText( { preserveWhiteSpace, } ); - result.formats = prepare( result ); + result.formats = __unstableAfterParse( result ); return result; } - /** - * Removes editor only formats from the value. - * - * Editor only formats are applied using `prepareEditableTree`, so we need to - * remove them before converting the internal state - * - * @param {Object} val The internal rich-text value. - * - * @return {Object} A new rich-text value. - */ - function removeEditorOnlyFormats( val ) { - formatTypes.forEach( ( formatType ) => { - // Remove formats created by prepareEditableTree, because they are editor only. - if ( formatType.__experimentalCreatePrepareEditableTree ) { - val = removeFormat( val, formatType.name, 0, val.text.length ); - } - } ); - - return val; - } - /** * Converts the internal value to the external data format. * @@ -139,12 +88,12 @@ export function useRichText( { return val.text; } - val = removeEditorOnlyFormats( val ); - if ( format !== 'string' ) { return; } + val.formats = __unstableBeforeSerialize( val ); + return toHTMLString( { value: val, multilineTag, preserveWhiteSpace } ); } @@ -174,7 +123,7 @@ export function useRichText( { multilineTag, multilineWrapperTags: multilineTag === 'li' ? [ 'ul', 'ol' ] : undefined, - prepareEditableTree: createPrepareEditableTree( prepareHandlers ), + prepareEditableTree: __unstableAddInvisibleFormats, __unstableDomOnly: domOnly, placeholder, } ); @@ -222,7 +171,13 @@ export function useRichText( { applyRecord( newRecord ); - const { start, end, activeFormats: newActiveFormats = [] } = newRecord; + const { + start, + end, + formats, + text, + activeFormats: newActiveFormats = [], + } = newRecord; _value.current = valueToFormat( newRecord ); record.current = newRecord; @@ -230,12 +185,11 @@ export function useRichText( { // Selection must be updated first, so it is recorded in history when // the content change happens. onSelectionChange( start, end ); - onChange( _value.current ); - setActiveFormats( newActiveFormats ); - - Object.values( changeHandlers ).forEach( ( changeHandler ) => { - changeHandler( newRecord.formats, newRecord.text ); + onChange( _value.current, { + __unstableFormats: formats, + __unstableText: text, } ); + setActiveFormats( newActiveFormats ); if ( ! withoutHistory ) { createUndoLevel(); @@ -301,27 +255,12 @@ export function useRichText( { createRecord, handleChange, } ), - usePasteHandler( { - isSelected, - disableFormats, - handleChange, - record, - formatTypes, - onPaste, - removeEditorOnlyFormats, - activeFormats, - } ), useInputAndSelection( { record, applyRecord, createRecord, handleChange, createUndoLevel, - allowPrefixTransformations, - inputRule, - valueToFormat, - formatTypes, - markAutomaticChange, isSelected, onSelectionChange, setActiveFormats, @@ -334,26 +273,15 @@ export function useRichText( { } didMount.current = true; - }, [ placeholder, ...dependencies ] ), + }, [ placeholder, ...__unstableDependencies ] ), ] ); return { - isSelected, value: record.current, onChange: handleChange, onFocus: focus, ref: mergedRefs, hasActiveFormats: activeFormats.length, - removeEditorOnlyFormats, - children: isSelected && ( - - ), }; } diff --git a/packages/rich-text/src/component/use-input-and-selection.js b/packages/rich-text/src/component/use-input-and-selection.js index bd27a9338b488..dd440c55100a1 100644 --- a/packages/rich-text/src/component/use-input-and-selection.js +++ b/packages/rich-text/src/component/use-input-and-selection.js @@ -91,11 +91,6 @@ export function useInputAndSelection( props ) { createRecord, handleChange, createUndoLevel, - allowPrefixTransformations, - inputRule, - valueToFormat, - formatTypes, - markAutomaticChange, } = propsRef.current; // The browser formatted something or tried to insert HTML. @@ -129,35 +124,6 @@ export function useInputAndSelection( props ) { // Create an undo level when input stops for over a second. defaultView.clearTimeout( timeout ); timeout = defaultView.setTimeout( createUndoLevel, 1000 ); - - // Only run input rules when inserting text. - if ( inputType !== 'insertText' ) { - return; - } - - if ( allowPrefixTransformations && inputRule ) { - inputRule( change, valueToFormat ); - } - - const transformed = formatTypes.reduce( - ( accumlator, { __unstableInputRule } ) => { - if ( __unstableInputRule ) { - accumlator = __unstableInputRule( accumlator ); - } - - return accumlator; - }, - change - ); - - if ( transformed !== change ) { - createUndoLevel(); - handleChange( { - ...transformed, - activeFormats: oldActiveFormats, - } ); - markAutomaticChange(); - } } /** diff --git a/packages/rich-text/src/component/use-paste-handler.js b/packages/rich-text/src/component/use-paste-handler.js deleted file mode 100644 index d63bf59bed116..0000000000000 --- a/packages/rich-text/src/component/use-paste-handler.js +++ /dev/null @@ -1,112 +0,0 @@ -/** - * WordPress dependencies - */ -import { useRef } from '@wordpress/element'; -import { useRefEffect } from '@wordpress/compose'; -import { getFilesFromDataTransfer } from '@wordpress/dom'; - -/** - * Internal dependencies - */ -import { insert } from '../insert'; - -export function usePasteHandler( props ) { - const propsRef = useRef( props ); - propsRef.current = props; - return useRefEffect( ( element ) => { - function _onPaste( event ) { - const { - isSelected, - disableFormats, - handleChange, - record, - formatTypes, - onPaste, - removeEditorOnlyFormats, - activeFormats, - } = 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; - } - } - - 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 ) { - handleChange( insert( record.current, plainText ) ); - return; - } - - const transformed = formatTypes.reduce( - ( accumlator, { __unstablePasteRule } ) => { - // Only allow one transform. - if ( - __unstablePasteRule && - accumlator === record.current - ) { - accumlator = __unstablePasteRule( record.current, { - html, - plainText, - } ); - } - - return accumlator; - }, - record.current - ); - - if ( transformed !== record.current ) { - handleChange( transformed ); - return; - } - - if ( onPaste ) { - const files = getFilesFromDataTransfer( clipboardData ); - const isInternal = - clipboardData.getData( 'rich-text' ) === 'true'; - - onPaste( { - value: removeEditorOnlyFormats( record.current ), - onChange: handleChange, - html, - plainText, - isInternal, - files: [ ...files ], - activeFormats, - } ); - } - } - - element.addEventListener( 'paste', _onPaste ); - return () => { - element.removeEventListener( 'paste', _onPaste ); - }; - }, [] ); -}