@gechiui/block-editor
Version:
701 lines (602 loc) • 22.2 kB
JavaScript
;
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