UNPKG

@gechiui/block-editor

Version:
701 lines (602 loc) 22.2 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "RichTextShortcut", { enumerable: true, get: function () { return _shortcut.RichTextShortcut; } }); Object.defineProperty(exports, "RichTextToolbarButton", { enumerable: true, get: function () { return _toolbarButton.RichTextToolbarButton; } }); Object.defineProperty(exports, "__unstableRichTextInputEvent", { enumerable: true, get: function () { return _inputEvent.__unstableRichTextInputEvent; } }); exports.default = void 0; var _element = require("@gechiui/element"); var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends")); var _classnames = _interopRequireDefault(require("classnames")); var _lodash = require("lodash"); var _data = require("@gechiui/data"); var _blocks = require("@gechiui/blocks"); var _compose = require("@gechiui/compose"); var _richText = require("@gechiui/rich-text"); var _deprecated = _interopRequireDefault(require("@gechiui/deprecated")); var _url = require("@gechiui/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 _useNativeProps = require("./use-native-props"); var _store = require("../../store"); var _utils = require("./utils"); var _embedHandlerPicker = _interopRequireDefault(require("./embed-handler-picker")); var _shortcut = require("./shortcut"); var _toolbarButton = require("./toolbar-button"); var _inputEvent = require("./input-event"); /** * External dependencies */ /** * GeChiUI dependencies */ /** * Internal dependencies */ const wrapperClasses = 'block-editor-rich-text'; const classes = 'block-editor-rich-text__editable'; function RichTextWrapper(_ref, forwardedRef) { let { children, tagName, value: originalValue, onChange: originalOnChange, isSelected: originalIsSelected, multiline, inlineToolbar, wrapperClassName, autocompleters, onReplace, placeholder, allowedFormats, formattingControls, withoutInteractiveFormatting, onRemove, onMerge, onSplit, __unstableOnSplitAtEnd: onSplitAtEnd, __unstableOnSplitMiddle: onSplitMiddle, identifier, preserveWhiteSpace, __unstablePastePlainText: pastePlainText, __unstableEmbedURLOnPaste, __unstableDisableFormats: disableFormats, disableLineBreaks, unstableOnFocus, __unstableAllowPrefixTransformations, __unstableMultilineRootTag, // Native props. __unstableMobileNoFocusOnMount, deleteEnter, placeholderTextColor, textAlign, selectionColor, tagsToEliminate, rootTagsToEliminate, disableEditingMenu, fontSize, fontFamily, fontWeight, fontStyle, minWidth, maxWidth, onBlur, setRef, ...props } = _ref; const instanceId = (0, _compose.useInstanceId)(RichTextWrapper); identifier = identifier || instanceId; const fallbackRef = (0, _element.useRef)(); const { clientId, isSelected: blockIsSelected } = (0, _blockEdit.useBlockEditContext)(); const nativeProps = (0, _useNativeProps.useNativeProps)(); const embedHandlerPickerRef = (0, _element.useRef)(); const selector = select => { const { isCaretWithinFormattedText, getSelectionStart, getSelectionEnd, getSettings, didAutomaticChange, getBlock, isMultiSelecting, hasMultiSelection } = 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/gechiui-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 { isCaretWithinFormattedText: isCaretWithinFormattedText(), selectionStart: isSelected ? selectionStart.offset : undefined, selectionEnd: isSelected ? selectionEnd.offset : undefined, isSelected, didAutomaticChange: didAutomaticChange(), disabled: isMultiSelecting() || hasMultiSelection(), undo, ...extraProps }; }; // 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 { isCaretWithinFormattedText, selectionStart, selectionEnd, isSelected, didAutomaticChange, disabled, undo, shouldBlurOnUnmount } = (0, _data.useSelect)(selector); const { __unstableMarkLastChangeAsPersistent, enterFormattedText, exitFormattedText, selectionChange, __unstableMarkAutomaticChange } = (0, _data.useDispatch)(_store.store); const multilineTag = (0, _utils.getMultilineTag)(multiline); const adjustedAllowedFormats = (0, _utils.getAllowedFormats)({ allowedFormats, formattingControls, 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)((start, end) => { selectionChange(clientId, identifier, start, end); }, [clientId, identifier]); const onDelete = (0, _element.useCallback)(_ref2 => { let { value, isReverse } = _ref2; 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). 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)(function (record) { let pastedBlocks = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : []; 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, multilineTag }), !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, multilineTag }), 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, multilineTag, onSplitMiddle]); const onEnter = (0, _element.useCallback)(_ref3 => { let { value, onChange, shiftKey } = _ref3; const canSplit = onReplace && onSplit; if (onReplace) { const transforms = (0, _blocks.getBlockTransforms)('from').filter(_ref4 => { let { type } = _ref4; return type === 'enter'; }); const transformation = (0, _blocks.findTransform)(transforms, item => { return item.regExp.test(value.text); }); if (transformation) { onReplace([transformation.transform({ content: value.text })]); __unstableMarkAutomaticChange(); } } if (multiline) { if (shiftKey) { if (!disableLineBreaks) { onChange((0, _richText.insert)(value, '\n')); } } else if (canSplit && (0, _richText.__unstableIsEmptyLine)(value)) { splitValue(value); } else { onChange((0, _richText.__unstableInsertLineSeparator)(value)); } } else { const { text, start, end } = value; const canSplitAtEnd = onSplitAtEnd && start === end && end === text.length; if (shiftKey || !canSplit && !canSplitAtEnd) { if (!disableLineBreaks) { onChange((0, _richText.insert)(value, '\n')); } } else if (!canSplit && canSplitAtEnd) { onSplitAtEnd(); } else if (canSplit) { splitValue(value); } } }, [onReplace, onSplit, __unstableMarkAutomaticChange, multiline, splitValue, onSplitAtEnd]); const onPaste = (0, _element.useCallback)(_ref5 => { let { value, onChange, html, plainText, isInternal, files, activeFormats } = _ref5; // 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, multilineTag, multilineWrapperTags: multilineTag === 'li' ? ['ul', 'ol'] : undefined, 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'; // 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' && (0, _richText.isEmpty)(value) && (0, _utils.isShortcode)(plainText)) { mode = 'BLOCKS'; } const isPastedURL = (0, _url.isURL)(plainText.trim()); const presentEmbedHandlerPicker = () => { var _embedHandlerPickerRe; return (_embedHandlerPickerRe = embedHandlerPickerRef.current) === null || _embedHandlerPickerRe === void 0 ? void 0 : _embedHandlerPickerRe.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') { let valueToInsert = (0, _richText.create)({ html: content }); (0, _utils.addActiveFormats)(valueToInsert, activeFormats); // If the content should be multiline, we should process text // separated by a line break as separate lines. if (multilineTag) { valueToInsert = (0, _richText.replace)(valueToInsert, /\n+/g, _richText.__UNSTABLE_LINE_SEPARATOR); } 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); } else { if (canPasteEmbed) { onChange((0, _richText.insert)(value, (0, _richText.create)({ text: plainText }))); return; } splitValue(value, content); } } }, [tagName, onReplace, onSplit, splitValue, __unstableEmbedURLOnPaste, multilineTag, preserveWhiteSpace, pastePlainText]); const inputRule = (0, _element.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 trimmedTextBefore = text.slice(0, start).trim(); const prefixTransforms = (0, _blocks.getBlockTransforms)('from').filter(_ref6 => { let { type } = _ref6; return type === 'prefix'; }); const transformation = (0, _blocks.findTransform)(prefixTransforms, _ref7 => { let { prefix } = _ref7; return trimmedTextBefore === prefix; }); if (!transformation) { return; } const content = valueToFormat((0, _richText.slice)(value, start, text.length)); const block = transformation.transform(content); onReplace([block]); __unstableMarkAutomaticChange(); }, [onReplace, __unstableMarkAutomaticChange]); const mergedRef = (0, _compose.useMergeRefs)([forwardedRef, fallbackRef]); const content = (0, _element.createElement)(_richText.__experimentalRichText, (0, _extends2.default)({ clientId: clientId, identifier: identifier, ref: mergedRef, value: adjustedValue, onChange: adjustedOnChange, selectionStart: selectionStart, selectionEnd: selectionEnd, onSelectionChange: onSelectionChange, tagName: tagName, placeholder: placeholder, allowedFormats: adjustedAllowedFormats, withoutInteractiveFormatting: withoutInteractiveFormatting, onEnter: onEnter, onDelete: onDelete, onPaste: onPaste, __unstableIsSelected: isSelected, __unstableInputRule: inputRule, __unstableMultilineTag: multilineTag, __unstableIsCaretWithinFormattedText: isCaretWithinFormattedText, __unstableOnEnterFormattedText: enterFormattedText, __unstableOnExitFormattedText: exitFormattedText, __unstableOnCreateUndoLevel: __unstableMarkLastChangeAsPersistent, __unstableMarkAutomaticChange: __unstableMarkAutomaticChange, __unstableDidAutomaticChange: didAutomaticChange, __unstableUndo: undo, __unstableDisableFormats: disableFormats, preserveWhiteSpace: preserveWhiteSpace, disabled: disabled, unstableOnFocus: unstableOnFocus, __unstableAllowPrefixTransformations: __unstableAllowPrefixTransformations, __unstableMultilineRootTag: __unstableMultilineRootTag // Native props. }, nativeProps, { blockIsSelected: originalIsSelected !== undefined ? originalIsSelected : blockIsSelected, shouldBlurOnUnmount: shouldBlurOnUnmount, __unstableMobileNoFocusOnMount: __unstableMobileNoFocusOnMount, deleteEnter: deleteEnter, placeholderTextColor: placeholderTextColor, textAlign: textAlign, selectionColor: selectionColor, tagsToEliminate: tagsToEliminate, rootTagsToEliminate: rootTagsToEliminate, disableEditingMenu: disableEditingMenu, fontSize: fontSize, fontFamily: fontFamily, fontWeight: fontWeight, fontStyle: fontStyle, minWidth: minWidth, maxWidth: maxWidth, onBlur: onBlur, setRef: setRef // 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 }), _ref8 => { let { isSelected: nestedIsSelected, value, onChange, onFocus, editableProps, editableTagName: TagName } = _ref8; return (0, _element.createElement)(_element.Fragment, null, children && children({ value, onChange, onFocus }), nestedIsSelected && hasFormats && (0, _element.createElement)(_formatToolbarContainer.default, { inline: inlineToolbar, anchorRef: fallbackRef.current }), nestedIsSelected && (0, _element.createElement)(_removeBrowserShortcuts.RemoveBrowserShortcuts, null), (0, _element.createElement)(_autocomplete.default, { onReplace: onReplace, completers: autocompleters, record: value, onChange: onChange, isSelected: nestedIsSelected, contentRef: fallbackRef }, _ref9 => { let { listBoxId, activeId, onKeyDown } = _ref9; return (0, _element.createElement)(TagName, (0, _extends2.default)({}, editableProps, props, { style: props.style ? { ...props.style, ...editableProps.style } : editableProps.style, className: (0, _classnames.default)(classes, props.className, editableProps.className), "aria-autocomplete": listBoxId ? 'list' : undefined, "aria-owns": listBoxId, "aria-activedescendant": activeId, onKeyDown: event => { onKeyDown(event); editableProps.onKeyDown(event); } })); }), (0, _element.createElement)(_embedHandlerPicker.default, { ref: embedHandlerPickerRef })); }); if (!wrapperClassName) { return content; } (0, _deprecated.default)('gc.blockEditor.RichText wrapperClassName prop', { since: '5.4', alternative: 'className prop or create your own wrapper div' }); return (0, _element.createElement)("div", { className: (0, _classnames.default)(wrapperClasses, wrapperClassName) }, content); } const ForwardedRichTextContainer = (0, _element.forwardRef)(RichTextWrapper); ForwardedRichTextContainer.Content = _ref10 => { let { value, tagName: Tag, multiline, ...props } = _ref10; // Handle deprecated `children` and `node` sources. if (Array.isArray(value)) { value = _blocks.children.toHTML(value); } const MultilineTag = (0, _utils.getMultilineTag)(multiline); if (!value && MultilineTag) { value = `<${MultilineTag}></${MultilineTag}>`; } const content = (0, _element.createElement)(_element.RawHTML, null, value); if (Tag) { return (0, _element.createElement)(Tag, (0, _lodash.omit)(props, ['format']), content); } return content; }; ForwardedRichTextContainer.isEmpty = value => { return !value || value.length === 0; }; ForwardedRichTextContainer.Content.defaultProps = { format: 'string', value: '' }; /** * @see https://github.com/GeChiUI/gutenberg/blob/HEAD/packages/block-editor/src/components/rich-text/README.md */ var _default = ForwardedRichTextContainer; exports.default = _default; //# sourceMappingURL=index.native.js.map