@wordpress/block-editor
Version:
467 lines (426 loc) • 12.3 kB
JavaScript
/**
* External dependencies
*/
import classnames from 'classnames';
/**
* WordPress dependencies
*/
import {
RawHTML,
useRef,
useCallback,
forwardRef,
createContext,
} from '@wordpress/element';
import { useDispatch, useSelect } from '@wordpress/data';
import { children as childrenSource } from '@wordpress/blocks';
import { useInstanceId, useMergeRefs } from '@wordpress/compose';
import {
__unstableUseRichText as useRichText,
__unstableCreateElement,
removeFormat,
} from '@wordpress/rich-text';
import deprecated from '@wordpress/deprecated';
import { Popover } from '@wordpress/components';
/**
* Internal dependencies
*/
import { useBlockEditorAutocompleteProps } from '../autocomplete';
import { useBlockEditContext } from '../block-edit';
import FormatToolbarContainer from './format-toolbar-container';
import { store as blockEditorStore } from '../../store';
import { useUndoAutomaticChange } from './use-undo-automatic-change';
import { useMarkPersistent } from './use-mark-persistent';
import { usePasteHandler } from './use-paste-handler';
import { useBeforeInputRules } from './use-before-input-rules';
import { useInputRules } from './use-input-rules';
import { useDelete } from './use-delete';
import { useEnter } from './use-enter';
import { useFormatTypes } from './use-format-types';
import { useRemoveBrowserShortcuts } from './use-remove-browser-shortcuts';
import { useShortcuts } from './use-shortcuts';
import { useInputEvents } from './use-input-events';
import { useInsertReplacementText } from './use-insert-replacement-text';
import { useFirefoxCompat } from './use-firefox-compat';
import FormatEdit from './format-edit';
import { getMultilineTag, getAllowedFormats } from './utils';
export const keyboardShortcutContext = createContext();
export const inputEventContext = createContext();
/**
* Removes props used for the native version of RichText so that they are not
* passed to the DOM element and log warnings.
*
* @param {Object} props Props to filter.
*
* @return {Object} Filtered props.
*/
function removeNativeProps( props ) {
const {
__unstableMobileNoFocusOnMount,
deleteEnter,
placeholderTextColor,
textAlign,
selectionColor,
tagsToEliminate,
disableEditingMenu,
fontSize,
fontFamily,
fontWeight,
fontStyle,
minWidth,
maxWidth,
setRef,
disableSuggestions,
disableAutocorrection,
...restProps
} = props;
return restProps;
}
function RichTextWrapper(
{
children,
tagName = 'div',
value: originalValue = '',
onChange: originalOnChange,
isSelected: originalIsSelected,
multiline,
inlineToolbar,
wrapperClassName,
autocompleters,
onReplace,
placeholder,
allowedFormats,
withoutInteractiveFormatting,
onRemove,
onMerge,
onSplit,
__unstableOnSplitAtEnd: onSplitAtEnd,
__unstableOnSplitMiddle: onSplitMiddle,
identifier,
preserveWhiteSpace,
__unstablePastePlainText: pastePlainText,
__unstableEmbedURLOnPaste,
__unstableDisableFormats: disableFormats,
disableLineBreaks,
__unstableAllowPrefixTransformations,
...props
},
forwardedRef
) {
if ( multiline ) {
deprecated( 'wp.blockEditor.RichText multiline prop', {
since: '6.1',
version: '6.3',
alternative: 'nested blocks (InnerBlocks)',
link: 'https://developer.wordpress.org/block-editor/how-to-guides/block-tutorial/nested-blocks-inner-blocks/',
} );
}
const instanceId = useInstanceId( RichTextWrapper );
identifier = identifier || instanceId;
props = removeNativeProps( props );
const anchorRef = useRef();
const { clientId } = useBlockEditContext();
const selector = ( select ) => {
const { getSelectionStart, getSelectionEnd } =
select( blockEditorStore );
const selectionStart = getSelectionStart();
const selectionEnd = getSelectionEnd();
let isSelected;
if ( originalIsSelected === undefined ) {
isSelected =
selectionStart.clientId === clientId &&
selectionEnd.clientId === clientId &&
selectionStart.attributeKey === identifier;
} else if ( originalIsSelected ) {
isSelected = selectionStart.clientId === clientId;
}
return {
selectionStart: isSelected ? selectionStart.offset : undefined,
selectionEnd: isSelected ? selectionEnd.offset : undefined,
isSelected,
};
};
// This selector must run on every render so the right selection state is
// retreived from the store on merge.
// To do: fix this somehow.
const { selectionStart, selectionEnd, isSelected } = useSelect( selector );
const { getSelectionStart, getSelectionEnd, getBlockRootClientId } =
useSelect( blockEditorStore );
const { selectionChange } = useDispatch( blockEditorStore );
const multilineTag = getMultilineTag( multiline );
const adjustedAllowedFormats = getAllowedFormats( {
allowedFormats,
disableFormats,
} );
const hasFormats =
! adjustedAllowedFormats || adjustedAllowedFormats.length > 0;
let adjustedValue = originalValue;
let adjustedOnChange = originalOnChange;
// Handle deprecated format.
if ( Array.isArray( originalValue ) ) {
deprecated( 'wp.blockEditor.RichText value prop as children type', {
since: '6.1',
version: '6.3',
alternative: 'value prop as string',
link: 'https://developer.wordpress.org/block-editor/how-to-guides/block-tutorial/introducing-attributes-and-editable-fields/',
} );
adjustedValue = childrenSource.toHTML( originalValue );
adjustedOnChange = ( newValue ) =>
originalOnChange(
childrenSource.fromDOM(
__unstableCreateElement( document, newValue ).childNodes
)
);
}
const onSelectionChange = useCallback(
( start, end ) => {
const selection = {};
const unset = start === undefined && end === undefined;
if ( typeof start === 'number' || unset ) {
// If we are only setting the start (or the end below), which
// means a partial selection, and we're not updating a selection
// with the same client ID, abort. This means the selected block
// is a parent block.
if (
end === undefined &&
getBlockRootClientId( clientId ) !==
getBlockRootClientId( getSelectionEnd().clientId )
) {
return;
}
selection.start = {
clientId,
attributeKey: identifier,
offset: start,
};
}
if ( typeof end === 'number' || unset ) {
if (
start === undefined &&
getBlockRootClientId( clientId ) !==
getBlockRootClientId( getSelectionStart().clientId )
) {
return;
}
selection.end = {
clientId,
attributeKey: identifier,
offset: end,
};
}
selectionChange( selection );
},
[ clientId, identifier ]
);
const {
formatTypes,
prepareHandlers,
valueHandlers,
changeHandlers,
dependencies,
} = useFormatTypes( {
clientId,
identifier,
withoutInteractiveFormatting,
allowedFormats: adjustedAllowedFormats,
} );
function addEditorOnlyFormats( value ) {
return valueHandlers.reduce(
( accumulator, fn ) => fn( accumulator, value.text ),
value.formats
);
}
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
);
}
} );
return value.formats;
}
function addInvisibleFormats( value ) {
return prepareHandlers.reduce(
( accumulator, fn ) => fn( accumulator, value.text ),
value.formats
);
}
const {
value,
getValue,
onChange,
ref: richTextRef,
} = useRichText( {
value: adjustedValue,
onChange( html, { __unstableFormats, __unstableText } ) {
adjustedOnChange( html );
Object.values( changeHandlers ).forEach( ( changeHandler ) => {
changeHandler( __unstableFormats, __unstableText );
} );
},
selectionStart,
selectionEnd,
onSelectionChange,
placeholder,
__unstableIsSelected: isSelected,
__unstableMultilineTag: multilineTag,
__unstableDisableFormats: disableFormats,
preserveWhiteSpace,
__unstableDependencies: [ ...dependencies, tagName ],
__unstableAfterParse: addEditorOnlyFormats,
__unstableBeforeSerialize: removeEditorOnlyFormats,
__unstableAddInvisibleFormats: addInvisibleFormats,
} );
const autocompleteProps = useBlockEditorAutocompleteProps( {
onReplace,
completers: autocompleters,
record: value,
onChange,
} );
useMarkPersistent( { html: adjustedValue, value } );
const keyboardShortcuts = useRef( new Set() );
const inputEvents = useRef( new Set() );
function onFocus() {
anchorRef.current?.focus();
}
const TagName = tagName;
return (
<>
{ isSelected && (
<keyboardShortcutContext.Provider value={ keyboardShortcuts }>
<inputEventContext.Provider value={ inputEvents }>
<Popover.__unstableSlotNameProvider value="__unstable-block-tools-after">
{ children &&
children( { value, onChange, onFocus } ) }
<FormatEdit
value={ value }
onChange={ onChange }
onFocus={ onFocus }
formatTypes={ formatTypes }
forwardedRef={ anchorRef }
/>
</Popover.__unstableSlotNameProvider>
</inputEventContext.Provider>
</keyboardShortcutContext.Provider>
) }
{ isSelected && hasFormats && (
<FormatToolbarContainer
inline={ inlineToolbar }
editableContentElement={ anchorRef.current }
value={ value }
/>
) }
<TagName
// Overridable props.
role="textbox"
aria-multiline={ ! disableLineBreaks }
aria-label={ placeholder }
{ ...props }
{ ...autocompleteProps }
ref={ useMergeRefs( [
forwardedRef,
autocompleteProps.ref,
props.ref,
richTextRef,
useBeforeInputRules( { value, onChange } ),
useInputRules( {
getValue,
onChange,
__unstableAllowPrefixTransformations,
formatTypes,
onReplace,
selectionChange,
} ),
useInsertReplacementText(),
useRemoveBrowserShortcuts(),
useShortcuts( keyboardShortcuts ),
useInputEvents( inputEvents ),
useUndoAutomaticChange(),
usePasteHandler( {
isSelected,
disableFormats,
onChange,
value,
formatTypes,
tagName,
onReplace,
onSplit,
onSplitMiddle,
__unstableEmbedURLOnPaste,
multilineTag,
preserveWhiteSpace,
pastePlainText,
} ),
useDelete( {
value,
onMerge,
onRemove,
} ),
useEnter( {
removeEditorOnlyFormats,
value,
onReplace,
onSplit,
onSplitMiddle,
multilineTag,
onChange,
disableLineBreaks,
onSplitAtEnd,
} ),
useFirefoxCompat( { value, onChange } ),
anchorRef,
] ) }
contentEditable={ true }
suppressContentEditableWarning={ true }
className={ classnames(
'block-editor-rich-text__editable',
props.className,
'rich-text'
) }
/>
</>
);
}
const ForwardedRichTextContainer = forwardRef( RichTextWrapper );
ForwardedRichTextContainer.Content = ( {
value,
tagName: Tag,
multiline,
...props
} ) => {
// Handle deprecated `children` and `node` sources.
if ( Array.isArray( value ) ) {
deprecated( 'wp.blockEditor.RichText value prop as children type', {
since: '6.1',
version: '6.3',
alternative: 'value prop as string',
link: 'https://developer.wordpress.org/block-editor/how-to-guides/block-tutorial/introducing-attributes-and-editable-fields/',
} );
value = childrenSource.toHTML( value );
}
const MultilineTag = getMultilineTag( multiline );
if ( ! value && MultilineTag ) {
value = `<${ MultilineTag }></${ MultilineTag }>`;
}
const content = <RawHTML>{ value }</RawHTML>;
if ( Tag ) {
const { format, ...restProps } = props;
return <Tag { ...restProps }>{ content }</Tag>;
}
return content;
};
ForwardedRichTextContainer.isEmpty = ( value ) => {
return ! value || value.length === 0;
};
/**
* @see https://github.com/WordPress/gutenberg/blob/HEAD/packages/block-editor/src/components/rich-text/README.md
*/
export default ForwardedRichTextContainer;
export { RichTextShortcut } from './shortcut';
export { RichTextToolbarButton } from './toolbar-button';
export { __unstableRichTextInputEvent } from './input-event';