@gechiui/block-editor
Version:
392 lines (350 loc) • 11.8 kB
JavaScript
import _extends from "@babel/runtime/helpers/esm/extends";
import { createElement, Fragment } from "@gechiui/element";
/**
* External dependencies
*/
import classnames from 'classnames';
import { omit } from 'lodash';
/**
* GeChiUI dependencies
*/
import { RawHTML, useRef, useCallback, forwardRef, createContext } from '@gechiui/element';
import { useDispatch, useSelect } from '@gechiui/data';
import { children as childrenSource } from '@gechiui/blocks';
import { useInstanceId, useMergeRefs } from '@gechiui/compose';
import { __unstableUseRichText as useRichText, __unstableCreateElement, isEmpty, isCollapsed, removeFormat } from '@gechiui/rich-text';
import deprecated from '@gechiui/deprecated';
import { BACKSPACE, DELETE } from '@gechiui/keycodes';
import { Popover } from '@gechiui/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 { useCaretInFormat } from './use-caret-in-format';
import { useMarkPersistent } from './use-mark-persistent';
import { usePasteHandler } from './use-paste-handler';
import { useInputRules } from './use-input-rules';
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 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) {
return omit(props, ['__unstableMobileNoFocusOnMount', 'deleteEnter', 'placeholderTextColor', 'textAlign', 'selectionColor', 'tagsToEliminate', 'rootTagsToEliminate', 'disableEditingMenu', 'fontSize', 'fontFamily', 'fontWeight', 'fontStyle', 'minWidth', 'maxWidth', 'setRef']);
}
function RichTextWrapper(_ref, forwardedRef) {
let {
children,
tagName = 'div',
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,
...props
} = _ref;
const instanceId = useInstanceId(RichTextWrapper);
identifier = identifier || instanceId;
props = removeNativeProps(props);
const anchorRef = useRef();
const {
clientId
} = useBlockEditContext();
const selector = select => {
const {
getSelectionStart,
getSelectionEnd,
isMultiSelecting,
hasMultiSelection
} = select(blockEditorStore);
const selectionStart = getSelectionStart();
const selectionEnd = getSelectionEnd();
let isSelected;
if (originalIsSelected === undefined) {
isSelected = selectionStart.clientId === clientId && selectionStart.attributeKey === identifier;
} else if (originalIsSelected) {
isSelected = selectionStart.clientId === clientId;
}
return {
selectionStart: isSelected ? selectionStart.offset : undefined,
selectionEnd: isSelected ? selectionEnd.offset : undefined,
isSelected,
disabled: isMultiSelecting() || hasMultiSelection()
};
}; // 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,
disabled
} = useSelect(selector);
const {
selectionChange
} = useDispatch(blockEditorStore);
const multilineTag = getMultilineTag(multiline);
const adjustedAllowedFormats = getAllowedFormats({
allowedFormats,
formattingControls,
disableFormats
});
const hasFormats = !adjustedAllowedFormats || adjustedAllowedFormats.length > 0;
let adjustedValue = originalValue;
let adjustedOnChange = originalOnChange; // Handle deprecated format.
if (Array.isArray(originalValue)) {
adjustedValue = childrenSource.toHTML(originalValue);
adjustedOnChange = newValue => originalOnChange(childrenSource.fromDOM(__unstableCreateElement(document, newValue).childNodes));
}
const onSelectionChange = useCallback((start, end) => {
selectionChange(clientId, identifier, start, end);
}, [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,
onChange,
ref: richTextRef
} = useRichText({
value: adjustedValue,
onChange(html, _ref2) {
let {
__unstableFormats,
__unstableText
} = _ref2;
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
});
useCaretInFormat({
value
});
useMarkPersistent({
html: adjustedValue,
value
});
const keyboardShortcuts = useRef(new Set());
const inputEvents = useRef(new Set());
function onKeyDown(event) {
const {
keyCode
} = event;
if (event.defaultPrevented) {
return;
}
if (keyCode === DELETE || keyCode === BACKSPACE) {
const {
start,
end,
text
} = value;
const isReverse = keyCode === BACKSPACE;
const hasActiveFormats = value.activeFormats && !!value.activeFormats.length; // Only process delete if the key press occurs at an uncollapsed edge.
if (!isCollapsed(value) || hasActiveFormats || isReverse && start !== 0 || !isReverse && end !== text.length) {
return;
}
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 && isEmpty(value) && isReverse) {
onRemove(!isReverse);
}
event.preventDefault();
}
}
function onFocus() {
anchorRef.current.focus();
}
const TagName = tagName;
const content = createElement(Fragment, null, isSelected && createElement(keyboardShortcutContext.Provider, {
value: keyboardShortcuts
}, createElement(inputEventContext.Provider, {
value: inputEvents
}, createElement(Popover.__unstableSlotNameProvider, {
value: "__unstable-block-tools-after"
}, children && children({
value,
onChange,
onFocus
}), createElement(FormatEdit, {
value: value,
onChange: onChange,
onFocus: onFocus,
formatTypes: formatTypes,
forwardedRef: anchorRef
})))), isSelected && hasFormats && createElement(FormatToolbarContainer, {
inline: inlineToolbar,
anchorRef: anchorRef.current
}), createElement(TagName // Overridable props.
, _extends({
role: "textbox",
"aria-multiline": true,
"aria-label": placeholder
}, props, autocompleteProps, {
ref: useMergeRefs([autocompleteProps.ref, props.ref, richTextRef, useInputRules({
value,
onChange,
__unstableAllowPrefixTransformations,
formatTypes,
onReplace
}), useRemoveBrowserShortcuts(), useShortcuts(keyboardShortcuts), useInputEvents(inputEvents), useUndoAutomaticChange(), usePasteHandler({
isSelected,
disableFormats,
onChange,
value,
formatTypes,
tagName,
onReplace,
onSplit,
onSplitMiddle,
__unstableEmbedURLOnPaste,
multilineTag,
preserveWhiteSpace,
pastePlainText
}), useEnter({
removeEditorOnlyFormats,
value,
onReplace,
onSplit,
onSplitMiddle,
multilineTag,
onChange,
disableLineBreaks,
onSplitAtEnd
}), anchorRef, forwardedRef]) // Do not set the attribute if disabled.
,
contentEditable: disabled ? undefined : true,
suppressContentEditableWarning: !disabled,
className: classnames('block-editor-rich-text__editable', props.className, 'rich-text'),
onFocus: unstableOnFocus,
onKeyDown: onKeyDown
})));
if (!wrapperClassName) {
return content;
}
deprecated('gc.blockEditor.RichText wrapperClassName prop', {
since: '5.4',
alternative: 'className prop or create your own wrapper div'
});
const className = classnames('block-editor-rich-text', wrapperClassName);
return createElement("div", {
className: className
}, content);
}
const ForwardedRichTextContainer = forwardRef(RichTextWrapper);
ForwardedRichTextContainer.Content = _ref3 => {
let {
value,
tagName: Tag,
multiline,
...props
} = _ref3;
// Handle deprecated `children` and `node` sources.
if (Array.isArray(value)) {
value = childrenSource.toHTML(value);
}
const MultilineTag = getMultilineTag(multiline);
if (!value && MultilineTag) {
value = `<${MultilineTag}></${MultilineTag}>`;
}
const content = createElement(RawHTML, null, value);
if (Tag) {
return createElement(Tag, omit(props, ['format']), content);
}
return content;
};
ForwardedRichTextContainer.isEmpty = value => {
return !value || value.length === 0;
};
/**
* @see https://github.com/GeChiUI/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';
//# sourceMappingURL=index.js.map