UNPKG

@wordpress/editor

Version:
280 lines (279 loc) 7.7 kB
// packages/editor/src/components/collab-sidebar/rich-text-control/index.tsx import clsx from "clsx"; import { SlotFillProvider, privateApis as componentsPrivateApis, __unstableUseAutocompleteProps as useAutocompleteProps } from "@wordpress/components"; import { useMergeRefs, useRefEffect, __experimentalUseFocusOutside as useFocusOutside } from "@wordpress/compose"; import { useInsertionEffect, useMemo, useRef, useState } from "@wordpress/element"; import { withIgnoreIMEEvents } from "@wordpress/keycodes"; import { insert, privateApis as richTextPrivateApis } from "@wordpress/rich-text"; import { unlock } from "../../../lock-unlock.mjs"; import { getAllowedFormats } from "./utils.mjs"; import FormatEdit from "./format-edit.mjs"; import { jsx, jsxs } from "react/jsx-runtime"; var { ValidatedContentEditableControl: RichTextControlShell } = unlock( componentsPrivateApis ); var { useRichText, KeyboardShortcutContext, InputEventContext, shortcutsListener, inputEventsListener } = unlock(richTextPrivateApis); var EMPTY_COMPLETERS = []; function RichTextControl({ label, value: attrValue, onChange, placeholder, id, clientId, className, hideLabelFromVision, help, disabled, required, markWhenOptional, customValidity, allowedFormats, disableFormats, withoutInteractiveFormatting, preserveWhiteSpace, disableLineBreaks, focusOnMount, completers = EMPTY_COMPLETERS }) { const [selection, setSelection] = useState({ start: void 0, end: void 0 }); const [isSelected, setIsSelected] = useState(false); const anchorRef = useRef(void 0); const inputEvents = useRef(/* @__PURE__ */ new Set()); const keyboardShortcuts = useRef( /* @__PURE__ */ new Set() ); const focusOutside = useFocusOutside(() => setIsSelected(false)); const adjustedAllowedFormats = getAllowedFormats({ allowedFormats, disableFormats }); const { value, onChange: onRichTextChange, ref: richTextRef, formatTypes, getValue } = useRichText({ value: attrValue, onChange, selectionStart: selection.start, selectionEnd: selection.end, onSelectionChange: (start, end) => setSelection({ start, end }), __unstableIsSelected: isSelected, preserveWhiteSpace: !!preserveWhiteSpace, placeholder, __unstableDisableFormats: disableFormats, allowedFormats: adjustedAllowedFormats, withoutInteractiveFormatting, __unstableFormatTypeHandlerContext: useMemo( () => ({ richTextIdentifier: id, blockClientId: clientId }), [id, clientId] ) }); function onFocus() { anchorRef.current?.focus(); } const eventListenersPropsRef = useRef({ keyboardShortcuts, inputEvents }); const inputRulePropsRef = useRef({ formatTypes, getValue, onChange: onRichTextChange }); useInsertionEffect(() => { inputRulePropsRef.current = { formatTypes, getValue, onChange: onRichTextChange }; }); const enterRef = useRefEffect( (element) => { if (disabled) { return; } const onKeyDown = withIgnoreIMEEvents((event) => { if (event.key !== "Enter" || event.defaultPrevented || event.metaKey || event.ctrlKey) { return; } event.preventDefault(); if (disableLineBreaks) { return; } const { getValue: getCurrentValue, onChange: handleChange } = inputRulePropsRef.current; const current = getCurrentValue(); handleChange( insert( current, "\n", current.start ?? current.text.length, current.end ?? current.text.length ) ); }); element.addEventListener("keydown", onKeyDown); return () => element.removeEventListener("keydown", onKeyDown); }, [disableLineBreaks, disabled] ); const eventListenersRef = useRefEffect( (element) => { if (!isSelected) { return; } const cleanupShortcuts = shortcutsListener( eventListenersPropsRef )(element); const cleanupInputEvents = inputEventsListener( eventListenersPropsRef )(element); function onFormatInput(event) { if (event.inputType !== "insertText" && event.type !== "compositionend") { return; } const { formatTypes: types, getValue: getCurrentValue, onChange: handleChange } = inputRulePropsRef.current; const current = getCurrentValue(); const transformed = types.reduce( (accumulator, { __unstableInputRule }) => __unstableInputRule ? __unstableInputRule(accumulator) : accumulator, current ); if (transformed !== current) { handleChange({ ...transformed, activeFormats: current.activeFormats }); } } element.addEventListener("input", onFormatInput); element.addEventListener("compositionend", onFormatInput); return () => { cleanupShortcuts(); cleanupInputEvents(); element.removeEventListener("input", onFormatInput); element.removeEventListener("compositionend", onFormatInput); }; }, [isSelected] ); const { ref: autocompleteRef, ...autocompleteProps } = useAutocompleteProps( { completers, record: value, onChange: onRichTextChange, // This control's completers insert their completion into the value; // none replace the whole value, so the required `onReplace` is a // no-op here. onReplace: () => { } } ); const focusOnMountRef = useRefEffect( (element) => { if (focusOnMount && !disabled) { element.focus(); } }, [focusOnMount, disabled] ); const editableRef = useMergeRefs([ richTextRef, anchorRef, eventListenersRef, enterRef, focusOnMountRef, autocompleteRef ]); return ( // Focus boundary for the field's selection: `onFocus` selects on entry; // the spread `useFocusOutside` handlers deselect once focus leaves. /* @__PURE__ */ jsx( "div", { ...focusOutside, onFocus: (event) => { setIsSelected(true); focusOutside.onFocus(event); }, children: /* @__PURE__ */ jsxs(SlotFillProvider, { children: [ /* @__PURE__ */ jsx( RichTextControlShell, { label, id, className: clsx("editor-rich-text-control", className), placeholder, hideLabelFromVision, help, disabled, required, markWhenOptional, customValidity, value: value.text, "aria-multiline": !disableLineBreaks, ...autocompleteProps, ref: editableRef } ), isSelected && !disabled && /* @__PURE__ */ jsx( KeyboardShortcutContext.Provider, { value: keyboardShortcuts, children: /* @__PURE__ */ jsx(InputEventContext.Provider, { value: inputEvents, children: /* @__PURE__ */ jsx( FormatEdit, { value, onChange: onRichTextChange, onFocus, formatTypes, forwardedRef: anchorRef, isVisible: true } ) }) } ) ] }) } ) ); } export { RichTextControl as default }; //# sourceMappingURL=index.mjs.map