UNPKG

@wordpress/block-editor

Version:
601 lines (584 loc) 21.2 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.PrivateRichText = void 0; Object.defineProperty(exports, "RichTextShortcut", { enumerable: true, get: function () { return _shortcut.RichTextShortcut; } }); Object.defineProperty(exports, "RichTextToolbarButton", { enumerable: true, get: function () { return _toolbarButton.RichTextToolbarButton; } }); exports.RichTextWrapper = RichTextWrapper; Object.defineProperty(exports, "__unstableRichTextInputEvent", { enumerable: true, get: function () { return _inputEvent.__unstableRichTextInputEvent; } }); exports.default = void 0; var _clsx = _interopRequireDefault(require("clsx")); var _element = require("@wordpress/element"); var _data = require("@wordpress/data"); var _blocks = require("@wordpress/blocks"); var _compose = require("@wordpress/compose"); var _richText = require("@wordpress/rich-text"); var _url = require("@wordpress/url"); var _autocomplete = _interopRequireDefault(require("../autocomplete")); var _blockEdit = require("../block-edit"); var _removeBrowserShortcuts = require("./remove-browser-shortcuts"); var _filePasteHandler = require("./file-paste-handler"); var _formatToolbarContainer = _interopRequireDefault(require("./format-toolbar-container")); var _store = require("../../store"); var _utils = require("./utils"); var _embedHandlerPicker = _interopRequireDefault(require("./embed-handler-picker")); var _content = require("./content"); var _native = _interopRequireDefault(require("./native")); var _withDeprecations = require("./with-deprecations"); var _inputRules = require("./event-listeners/input-rules"); var _selection = require("../../utils/selection"); var _jsxRuntime = require("react/jsx-runtime"); var _shortcut = require("./shortcut"); var _toolbarButton = require("./toolbar-button"); var _inputEvent = require("./input-event"); /** * External dependencies */ /** * WordPress dependencies */ /** * Internal dependencies */ const classes = 'block-editor-rich-text__editable'; function RichTextWrapper({ children, tagName, start, reversed, value: originalValue, onChange: originalOnChange, isSelected: originalIsSelected, inlineToolbar, wrapperClassName, autocompleters, onReplace, placeholder, allowedFormats, withoutInteractiveFormatting, onRemove, onMerge, onSplit, __unstableOnSplitAtEnd: onSplitAtEnd, __unstableOnSplitMiddle: onSplitMiddle, __unstableOnSplitAtDoubleLineEnd: onSplitAtDoubleLineEnd, identifier, preserveWhiteSpace, __unstablePastePlainText: pastePlainText, __unstableEmbedURLOnPaste, __unstableDisableFormats: disableFormats, disableLineBreaks, unstableOnFocus, __unstableAllowPrefixTransformations, // Native props. __unstableUseSplitSelection, __unstableMobileNoFocusOnMount, deleteEnter, placeholderTextColor, textAlign, selectionColor, tagsToEliminate, disableEditingMenu, fontSize, fontFamily, fontWeight, fontStyle, minWidth, maxWidth, onBlur, disableSuggestions, disableAutocorrection, containerWidth, onEnter: onCustomEnter, ...props }, providedRef) { const instanceId = (0, _compose.useInstanceId)(RichTextWrapper); identifier = identifier || instanceId; const fallbackRef = (0, _element.useRef)(); const { clientId, isSelected: blockIsSelected } = (0, _blockEdit.useBlockEditContext)(); const embedHandlerPickerRef = (0, _element.useRef)(); const selector = select => { const { getSelectionStart, getSelectionEnd, getSettings, didAutomaticChange, getBlock, isMultiSelecting, hasMultiSelection, getSelectedBlockClientId } = select(_store.store); const selectionStart = getSelectionStart(); const selectionEnd = getSelectionEnd(); const { __experimentalUndo: undo } = getSettings(); let isSelected; if (originalIsSelected === undefined) { isSelected = selectionStart.clientId === clientId && selectionStart.attributeKey === identifier; } else if (originalIsSelected) { isSelected = selectionStart.clientId === clientId; } let extraProps = {}; if (_element.Platform.OS === 'native') { // If the block of this RichText is unmodified then it's a candidate for replacing when adding a new block. // In order to fix https://github.com/wordpress-mobile/gutenberg-mobile/issues/1126, let's blur on unmount in that case. // This apparently assumes functionality the BlockHlder actually. const block = clientId && getBlock(clientId); const shouldBlurOnUnmount = block && isSelected && (0, _blocks.isUnmodifiedDefaultBlock)(block); extraProps = { shouldBlurOnUnmount }; } return { selectionStart: isSelected ? selectionStart.offset : undefined, selectionEnd: isSelected ? selectionEnd.offset : undefined, isSelected, didAutomaticChange: didAutomaticChange(), disabled: isMultiSelecting() || hasMultiSelection(), undo, getSelectedBlockClientId, ...extraProps }; }; // This selector must run on every render so the right selection state is // retrieved from the store on merge. // To do: fix this somehow. const { selectionStart, selectionEnd, isSelected, getSelectedBlockClientId, didAutomaticChange, disabled, undo, shouldBlurOnUnmount } = (0, _data.useSelect)(selector); const { __unstableMarkLastChangeAsPersistent, enterFormattedText, exitFormattedText, selectionChange, __unstableMarkAutomaticChange, __unstableSplitSelection, clearSelectedBlock } = (0, _data.useDispatch)(_store.store); const adjustedAllowedFormats = (0, _utils.getAllowedFormats)({ allowedFormats, disableFormats }); const hasFormats = !adjustedAllowedFormats || adjustedAllowedFormats.length > 0; let adjustedValue = originalValue; let adjustedOnChange = originalOnChange; // Handle deprecated format. if (Array.isArray(originalValue)) { adjustedValue = _blocks.children.toHTML(originalValue); adjustedOnChange = newValue => originalOnChange(_blocks.children.fromDOM((0, _richText.__unstableCreateElement)(document, newValue).childNodes)); } const onSelectionChange = (0, _element.useCallback)((selectionChangeStart, selectionChangeEnd) => { selectionChange(clientId, identifier, selectionChangeStart, selectionChangeEnd); }, [clientId, identifier]); const clearCurrentSelectionOnUnmount = (0, _element.useCallback)(() => { if (getSelectedBlockClientId() === clientId) { clearSelectedBlock(); } }, [clearSelectedBlock, clientId, getSelectedBlockClientId]); const onDelete = (0, _element.useCallback)(({ value, isReverse }) => { if (onMerge) { onMerge(!isReverse); } // Only handle remove on Backspace. This serves dual-purpose of being // an intentional user interaction distinguishing between Backspace and // Delete to remove the empty field, but also to avoid merge & remove // causing destruction of two fields (merge, then removed merged). else if (onRemove && (0, _richText.isEmpty)(value) && isReverse) { onRemove(!isReverse); } }, [onMerge, onRemove]); /** * Signals to the RichText owner that the block can be replaced with two * blocks as a result of splitting the block by pressing enter, or with * blocks as a result of splitting the block by pasting block content in the * instance. * * @param {Object} record The rich text value to split. * @param {Array} pastedBlocks The pasted blocks to insert, if any. */ const splitValue = (0, _element.useCallback)((record, pastedBlocks = []) => { if (!onReplace || !onSplit) { return; } const blocks = []; const [before, after] = (0, _richText.split)(record); const hasPastedBlocks = pastedBlocks.length > 0; let lastPastedBlockIndex = -1; // Consider the after value to be the original it is not empty and // the before value *is* empty. const isAfterOriginal = (0, _richText.isEmpty)(before) && !(0, _richText.isEmpty)(after); // Create a block with the content before the caret if there's no pasted // blocks, or if there are pasted blocks and the value is not empty. // We do not want a leading empty block on paste, but we do if split // with e.g. the enter key. if (!hasPastedBlocks || !(0, _richText.isEmpty)(before)) { blocks.push(onSplit((0, _richText.toHTMLString)({ value: before }), !isAfterOriginal)); lastPastedBlockIndex += 1; } if (hasPastedBlocks) { blocks.push(...pastedBlocks); lastPastedBlockIndex += pastedBlocks.length; } else if (onSplitMiddle) { blocks.push(onSplitMiddle()); } // If there's pasted blocks, append a block with non empty content // after the caret. Otherwise, do append an empty block if there // is no `onSplitMiddle` prop, but if there is and the content is // empty, the middle block is enough to set focus in. if (hasPastedBlocks ? !(0, _richText.isEmpty)(after) : !onSplitMiddle || !(0, _richText.isEmpty)(after)) { blocks.push(onSplit((0, _richText.toHTMLString)({ value: after }), isAfterOriginal)); } // If there are pasted blocks, set the selection to the last one. // Otherwise, set the selection to the second block. const indexToSelect = hasPastedBlocks ? lastPastedBlockIndex : 1; // If there are pasted blocks, move the caret to the end of the selected block // Otherwise, retain the default value. const initialPosition = hasPastedBlocks ? -1 : 0; onReplace(blocks, indexToSelect, initialPosition); }, [onReplace, onSplit, onSplitMiddle]); const onEnter = (0, _element.useCallback)(({ value, onChange, shiftKey }) => { const canSplit = onReplace && onSplit; if (onReplace) { const transforms = (0, _blocks.getBlockTransforms)('from').filter(({ type }) => type === 'enter'); const transformation = (0, _blocks.findTransform)(transforms, item => { return item.regExp.test(value.text); }); if (transformation) { onReplace([transformation.transform({ content: value.text })]); __unstableMarkAutomaticChange(); return; } } if (onCustomEnter) { onCustomEnter(); } const { text, start: splitStart, end: splitEnd } = value; const canSplitAtEnd = onSplitAtEnd && splitStart === splitEnd && splitEnd === text.length; if (shiftKey) { if (!disableLineBreaks) { onChange((0, _richText.insert)(value, '\n')); } } else if (canSplit) { splitValue(value); } else if (__unstableUseSplitSelection) { __unstableSplitSelection(); } else if (canSplitAtEnd) { onSplitAtEnd(); } else if ( // For some blocks it's desirable to split at the end of the // block when there are two line breaks at the end of the // block, so triple Enter exits the block. onSplitAtDoubleLineEnd && splitStart === splitEnd && splitEnd === text.length && text.slice(-2) === '\n\n') { value.start = value.end - 2; onChange((0, _richText.remove)(value)); onSplitAtDoubleLineEnd(); } else if (!disableLineBreaks) { onChange((0, _richText.insert)(value, '\n')); } }, [onReplace, onSplit, __unstableMarkAutomaticChange, splitValue, onSplitAtEnd]); const onPaste = (0, _element.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 = (0, _richText.create)({ html, preserveWhiteSpace }); (0, _utils.addActiveFormats)(pastedValue, activeFormats); onChange((0, _richText.insert)(value, pastedValue)); return; } if (pastePlainText) { onChange((0, _richText.insert)(value, (0, _richText.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 = (0, _blocks.pasteHandler)({ HTML: (0, _filePasteHandler.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 && (0, _richText.isEmpty)(value)) { onReplace(content); } else { splitValue(value, content); } return; } let mode = onReplace && onSplit ? 'AUTO' : 'INLINE'; const isPastedURL = (0, _url.isURL)(plainText.trim()); const presentEmbedHandlerPicker = () => embedHandlerPickerRef.current?.presentPicker({ createEmbed: () => onReplace(content, content.length - 1, -1), createLink: () => (0, _utils.createLinkInParagraph)(plainText.trim(), onReplace) }); if (__unstableEmbedURLOnPaste && (0, _richText.isEmpty)(value) && isPastedURL) { mode = 'BLOCKS'; } const content = (0, _blocks.pasteHandler)({ HTML: html, plainText, mode, tagName, preserveWhiteSpace }); if (typeof content === 'string') { const valueToInsert = (0, _richText.create)({ html: content }); (0, _utils.addActiveFormats)(valueToInsert, activeFormats); onChange((0, _richText.insert)(value, valueToInsert)); } else if (content.length > 0) { // When an URL is pasted in an empty paragraph then the EmbedHandlerPicker should showcase options allowing the transformation of that URL // into either an Embed block or a link within the target paragraph. If the paragraph is non-empty, the URL is pasted as text. const canPasteEmbed = isPastedURL && content.length === 1 && content[0].name === 'core/embed'; if (onReplace && (0, _richText.isEmpty)(value)) { if (canPasteEmbed) { onChange((0, _richText.insert)(value, (0, _richText.create)({ text: plainText }))); if (__unstableEmbedURLOnPaste) { presentEmbedHandlerPicker(); } return; } onReplace(content, content.length - 1, -1, { source: 'clipboard' }); } else { if (canPasteEmbed) { onChange((0, _richText.insert)(value, (0, _richText.create)({ text: plainText }))); return; } splitValue(value, content); } } }, [tagName, onReplace, onSplit, splitValue, __unstableEmbedURLOnPaste, preserveWhiteSpace, pastePlainText]); const inputRule = (0, _element.useCallback)(value => { if (!onReplace) { return; } const { start: startPosition, text } = value; const characterBefore = text.slice(startPosition - 1, startPosition); // The character right before the caret must be a plain space. if (characterBefore !== ' ') { return; } const trimmedTextBefore = text.slice(0, start).trim(); const prefixTransforms = (0, _blocks.getBlockTransforms)('from').filter(({ type }) => type === 'prefix'); const transformation = (0, _blocks.findTransform)(prefixTransforms, ({ prefix }) => { return trimmedTextBefore === prefix; }); if (!transformation) { return; } const content = (0, _richText.toHTMLString)({ value: (0, _richText.insert)(value, _selection.START_OF_SELECTED_AREA, 0, start) }); const block = transformation.transform(content); const currentSelection = (0, _inputRules.findSelection)([block]); onReplace([block]); selectionChange(...currentSelection); __unstableMarkAutomaticChange(); }, [onReplace, start, selectionChange, __unstableMarkAutomaticChange]); const mergedRef = (0, _compose.useMergeRefs)([providedRef, fallbackRef]); return /*#__PURE__*/(0, _jsxRuntime.jsx)(_native.default, { clientId: clientId, identifier: identifier, nativeEditorRef: mergedRef, value: adjustedValue, onChange: adjustedOnChange, selectionStart: selectionStart, selectionEnd: selectionEnd, onSelectionChange: onSelectionChange, tagName: tagName, start: start, reversed: reversed, placeholder: placeholder, allowedFormats: adjustedAllowedFormats, withoutInteractiveFormatting: withoutInteractiveFormatting, onEnter: onEnter, onDelete: onDelete, onPaste: onPaste, __unstableIsSelected: isSelected, __unstableInputRule: inputRule, __unstableOnEnterFormattedText: enterFormattedText, __unstableOnExitFormattedText: exitFormattedText, __unstableOnCreateUndoLevel: __unstableMarkLastChangeAsPersistent, __unstableMarkAutomaticChange: __unstableMarkAutomaticChange, __unstableDidAutomaticChange: didAutomaticChange, __unstableUndo: undo, __unstableDisableFormats: disableFormats, preserveWhiteSpace: preserveWhiteSpace, disabled: disabled, unstableOnFocus: unstableOnFocus, __unstableAllowPrefixTransformations: __unstableAllowPrefixTransformations // Native props. , blockIsSelected: originalIsSelected !== undefined ? originalIsSelected : blockIsSelected, shouldBlurOnUnmount: shouldBlurOnUnmount, __unstableMobileNoFocusOnMount: __unstableMobileNoFocusOnMount, deleteEnter: deleteEnter, placeholderTextColor: placeholderTextColor, textAlign: textAlign, selectionColor: selectionColor, tagsToEliminate: tagsToEliminate, disableEditingMenu: disableEditingMenu, fontSize: fontSize, fontFamily: fontFamily, fontWeight: fontWeight, fontStyle: fontStyle, minWidth: minWidth, maxWidth: maxWidth, onBlur: onBlur, disableSuggestions: disableSuggestions, disableAutocorrection: disableAutocorrection, containerWidth: containerWidth, clearCurrentSelectionOnUnmount: clearCurrentSelectionOnUnmount // Props to be set on the editable container are destructured on the // element itself for web (see below), but passed through rich text // for native. , id: props.id, style: props.style, children: ({ isSelected: nestedIsSelected, value, onChange, onFocus, editableProps, editableTagName: TagName }) => /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, { children: [children && children({ value, onChange, onFocus }), nestedIsSelected && hasFormats && /*#__PURE__*/(0, _jsxRuntime.jsx)(_formatToolbarContainer.default, { inline: inlineToolbar, anchorRef: fallbackRef.current }), nestedIsSelected && /*#__PURE__*/(0, _jsxRuntime.jsx)(_removeBrowserShortcuts.RemoveBrowserShortcuts, {}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_autocomplete.default, { onReplace: onReplace, completers: autocompleters, record: value, onChange: onChange, isSelected: nestedIsSelected, contentRef: fallbackRef, children: ({ listBoxId, activeId, onKeyDown }) => /*#__PURE__*/(0, _jsxRuntime.jsx)(TagName, { ...editableProps, ...props, style: props.style ? { ...props.style, ...editableProps.style } : editableProps.style, className: (0, _clsx.default)(classes, props.className, editableProps.className), "aria-autocomplete": listBoxId ? 'list' : undefined, "aria-owns": listBoxId, "aria-activedescendant": activeId, onKeyDown: event => { onKeyDown(event); editableProps.onKeyDown(event); } }) }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_embedHandlerPicker.default, { ref: embedHandlerPickerRef })] }) }); } // This export does not actually implement a private API, but was exported // under this name for interoperability with the web version of the RichText // component. const PrivateRichText = exports.PrivateRichText = (0, _withDeprecations.withDeprecations)((0, _element.forwardRef)(RichTextWrapper)); PrivateRichText.Content = _content.Content; PrivateRichText.isEmpty = value => { return !value || value.length === 0; }; PrivateRichText.Content.defaultProps = { format: 'string', value: '' }; PrivateRichText.Raw = (0, _element.forwardRef)((props, ref) => /*#__PURE__*/(0, _jsxRuntime.jsx)(_native.default, { ...props, nativeEditorRef: ref })); /** * @see https://github.com/WordPress/gutenberg/blob/HEAD/packages/block-editor/src/components/rich-text/README.md */ var _default = exports.default = PrivateRichText; //# sourceMappingURL=index.native.js.map