slate-react
Version:
Tools for building completely customizable richtext editors with React.
1,308 lines (1,284 loc) • 122 kB
JavaScript
import getDirection from 'direction';
import debounce from 'lodash/debounce';
import throttle from 'lodash/throttle';
import React, { createContext, useContext, useRef, useEffect, useLayoutEffect, useState, memo, forwardRef, useCallback, Component, useReducer, useMemo } from 'react';
import scrollIntoView from 'scroll-into-view-if-needed';
import { Editor, Range, Transforms, Node, Text as Text$1, Path, Point, Element as Element$1, Scrubber } from 'slate';
import { DOMEditor, EDITOR_TO_USER_MARKS, EDITOR_TO_PENDING_DIFFS, EDITOR_TO_PENDING_ACTION, EDITOR_TO_PENDING_INSERTION_MARKS, targetRange, verifyDiffState, EDITOR_TO_PENDING_SELECTION, IS_COMPOSING, IS_NODE_MAP_DIRTY, applyStringDiff, isDOMSelection, isTrackedMutation, EDITOR_TO_FORCE_RENDER, normalizeRange, normalizePoint, EDITOR_TO_PLACEHOLDER_ELEMENT, normalizeStringDiff, mergeStringDiffs, CAN_USE_DOM, IS_ANDROID, EDITOR_TO_SCHEDULE_FLUSH, MARK_PLACEHOLDER_SYMBOL, IS_IOS, PLACEHOLDER_SYMBOL, IS_WEBKIT, isTextDecorationsEqual, EDITOR_TO_KEY_TO_ELEMENT, NODE_TO_ELEMENT, ELEMENT_TO_NODE, isElementDecorationsEqual, NODE_TO_INDEX, NODE_TO_PARENT, IS_READ_ONLY, getActiveElement, getSelection, IS_FOCUSED, getDefaultView, EDITOR_TO_WINDOW, EDITOR_TO_ELEMENT, IS_FIREFOX, EDITOR_TO_USER_SELECTION, HAS_BEFORE_INPUT_SUPPORT, isDOMElement, isDOMNode, TRIPLE_CLICK, IS_FIREFOX_LEGACY, IS_WECHATBROWSER, IS_UC_MOBILE, Hotkeys, IS_CHROME, isPlainTextOnlyPaste, EDITOR_TO_ON_CHANGE, withDOM } from 'slate-dom';
export { NODE_TO_INDEX, NODE_TO_PARENT } from 'slate-dom';
import { ResizeObserver } from '@juggle/resize-observer';
import ReactDOM from 'react-dom';
function _objectWithoutPropertiesLoose(source, excluded) {
if (source == null) return {};
var target = {};
var sourceKeys = Object.keys(source);
var key, i;
for (i = 0; i < sourceKeys.length; i++) {
key = sourceKeys[i];
if (excluded.indexOf(key) >= 0) continue;
target[key] = source[key];
}
return target;
}
function _objectWithoutProperties(source, excluded) {
if (source == null) return {};
var target = _objectWithoutPropertiesLoose(source, excluded);
var key, i;
if (Object.getOwnPropertySymbols) {
var sourceSymbolKeys = Object.getOwnPropertySymbols(source);
for (i = 0; i < sourceSymbolKeys.length; i++) {
key = sourceSymbolKeys[i];
if (excluded.indexOf(key) >= 0) continue;
if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue;
target[key] = source[key];
}
}
return target;
}
function _typeof(o) {
"@babel/helpers - typeof";
return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) {
return typeof o;
} : function (o) {
return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
}, _typeof(o);
}
function _toPrimitive(input, hint) {
if (_typeof(input) !== "object" || input === null) return input;
var prim = input[Symbol.toPrimitive];
if (prim !== undefined) {
var res = prim.call(input, hint || "default");
if (_typeof(res) !== "object") return res;
throw new TypeError("@@toPrimitive must return a primitive value.");
}
return (hint === "string" ? String : Number)(input);
}
function _toPropertyKey(arg) {
var key = _toPrimitive(arg, "string");
return _typeof(key) === "symbol" ? key : String(key);
}
function _defineProperty(obj, key, value) {
key = _toPropertyKey(key);
if (key in obj) {
Object.defineProperty(obj, key, {
value: value,
enumerable: true,
configurable: true,
writable: true
});
} else {
obj[key] = value;
}
return obj;
}
/**
* A React context for sharing the editor object.
*/
var EditorContext = /*#__PURE__*/createContext(null);
/**
* Get the current editor object from the React context.
*/
var useSlateStatic = () => {
var editor = useContext(EditorContext);
if (!editor) {
throw new Error("The `useSlateStatic` hook must be used inside the <Slate> component's context.");
}
return editor;
};
// eslint-disable-next-line no-redeclare
var ReactEditor = DOMEditor;
function ownKeys$5(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread$5(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys$5(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys$5(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
// https://github.com/facebook/draft-js/blob/main/src/component/handlers/composition/DraftEditorCompositionHandler.js#L41
// When using keyboard English association function, conpositionEnd triggered too fast, resulting in after `insertText` still maintain association state.
var RESOLVE_DELAY = 25;
// Time with no user interaction before the current user action is considered as done.
var FLUSH_DELAY = 200;
// Replace with `const debug = console.log` to debug
var debug = function debug() {};
// Type guard to check if a value is a DataTransfer
var isDataTransfer = value => (value === null || value === void 0 ? void 0 : value.constructor.name) === 'DataTransfer';
function createAndroidInputManager(_ref) {
var {
editor,
scheduleOnDOMSelectionChange,
onDOMSelectionChange
} = _ref;
var flushing = false;
var compositionEndTimeoutId = null;
var flushTimeoutId = null;
var actionTimeoutId = null;
var idCounter = 0;
var insertPositionHint = false;
var applyPendingSelection = () => {
var pendingSelection = EDITOR_TO_PENDING_SELECTION.get(editor);
EDITOR_TO_PENDING_SELECTION.delete(editor);
if (pendingSelection) {
var {
selection
} = editor;
var normalized = normalizeRange(editor, pendingSelection);
if (normalized && (!selection || !Range.equals(normalized, selection))) {
Transforms.select(editor, normalized);
}
}
};
var performAction = () => {
var action = EDITOR_TO_PENDING_ACTION.get(editor);
EDITOR_TO_PENDING_ACTION.delete(editor);
if (!action) {
return;
}
if (action.at) {
var target = Point.isPoint(action.at) ? normalizePoint(editor, action.at) : normalizeRange(editor, action.at);
if (!target) {
return;
}
var _targetRange = Editor.range(editor, target);
if (!editor.selection || !Range.equals(editor.selection, _targetRange)) {
Transforms.select(editor, target);
}
}
action.run();
};
var flush = () => {
if (flushTimeoutId) {
clearTimeout(flushTimeoutId);
flushTimeoutId = null;
}
if (actionTimeoutId) {
clearTimeout(actionTimeoutId);
actionTimeoutId = null;
}
if (!hasPendingDiffs() && !hasPendingAction()) {
applyPendingSelection();
return;
}
if (!flushing) {
flushing = true;
setTimeout(() => flushing = false);
}
if (hasPendingAction()) {
flushing = 'action';
}
var selectionRef = editor.selection && Editor.rangeRef(editor, editor.selection, {
affinity: 'forward'
});
EDITOR_TO_USER_MARKS.set(editor, editor.marks);
debug('flush', EDITOR_TO_PENDING_ACTION.get(editor), EDITOR_TO_PENDING_DIFFS.get(editor));
var scheduleSelectionChange = hasPendingDiffs();
var diff;
while (diff = (_EDITOR_TO_PENDING_DI = EDITOR_TO_PENDING_DIFFS.get(editor)) === null || _EDITOR_TO_PENDING_DI === void 0 ? void 0 : _EDITOR_TO_PENDING_DI[0]) {
var _EDITOR_TO_PENDING_DI, _EDITOR_TO_PENDING_DI2;
var pendingMarks = EDITOR_TO_PENDING_INSERTION_MARKS.get(editor);
if (pendingMarks !== undefined) {
EDITOR_TO_PENDING_INSERTION_MARKS.delete(editor);
editor.marks = pendingMarks;
}
if (pendingMarks && insertPositionHint === false) {
insertPositionHint = null;
}
var range = targetRange(diff);
if (!editor.selection || !Range.equals(editor.selection, range)) {
Transforms.select(editor, range);
}
if (diff.diff.text) {
Editor.insertText(editor, diff.diff.text);
} else {
Editor.deleteFragment(editor);
}
// Remove diff only after we have applied it to account for it when transforming
// pending ranges.
EDITOR_TO_PENDING_DIFFS.set(editor, (_EDITOR_TO_PENDING_DI2 = EDITOR_TO_PENDING_DIFFS.get(editor)) === null || _EDITOR_TO_PENDING_DI2 === void 0 ? void 0 : _EDITOR_TO_PENDING_DI2.filter(_ref2 => {
var {
id
} = _ref2;
return id !== diff.id;
}));
if (!verifyDiffState(editor, diff)) {
scheduleSelectionChange = false;
EDITOR_TO_PENDING_ACTION.delete(editor);
EDITOR_TO_USER_MARKS.delete(editor);
flushing = 'action';
// Ensure we don't restore the pending user (dom) selection
// since the document and dom state do not match.
EDITOR_TO_PENDING_SELECTION.delete(editor);
scheduleOnDOMSelectionChange.cancel();
onDOMSelectionChange.cancel();
selectionRef === null || selectionRef === void 0 || selectionRef.unref();
}
}
var selection = selectionRef === null || selectionRef === void 0 ? void 0 : selectionRef.unref();
if (selection && !EDITOR_TO_PENDING_SELECTION.get(editor) && (!editor.selection || !Range.equals(selection, editor.selection))) {
Transforms.select(editor, selection);
}
if (hasPendingAction()) {
performAction();
return;
}
// COMPAT: The selectionChange event is fired after the action is performed,
// so we have to manually schedule it to ensure we don't 'throw away' the selection
// while rendering if we have pending changes.
if (scheduleSelectionChange) {
scheduleOnDOMSelectionChange();
}
scheduleOnDOMSelectionChange.flush();
onDOMSelectionChange.flush();
applyPendingSelection();
var userMarks = EDITOR_TO_USER_MARKS.get(editor);
EDITOR_TO_USER_MARKS.delete(editor);
if (userMarks !== undefined) {
editor.marks = userMarks;
editor.onChange();
}
};
var handleCompositionEnd = _event => {
if (compositionEndTimeoutId) {
clearTimeout(compositionEndTimeoutId);
}
compositionEndTimeoutId = setTimeout(() => {
IS_COMPOSING.set(editor, false);
flush();
}, RESOLVE_DELAY);
};
var handleCompositionStart = _event => {
IS_COMPOSING.set(editor, true);
if (compositionEndTimeoutId) {
clearTimeout(compositionEndTimeoutId);
compositionEndTimeoutId = null;
}
};
var updatePlaceholderVisibility = function updatePlaceholderVisibility() {
var forceHide = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
var placeholderElement = EDITOR_TO_PLACEHOLDER_ELEMENT.get(editor);
if (!placeholderElement) {
return;
}
if (hasPendingDiffs() || forceHide) {
placeholderElement.style.display = 'none';
return;
}
placeholderElement.style.removeProperty('display');
};
var storeDiff = (path, diff) => {
var _EDITOR_TO_PENDING_DI3;
var pendingDiffs = (_EDITOR_TO_PENDING_DI3 = EDITOR_TO_PENDING_DIFFS.get(editor)) !== null && _EDITOR_TO_PENDING_DI3 !== void 0 ? _EDITOR_TO_PENDING_DI3 : [];
EDITOR_TO_PENDING_DIFFS.set(editor, pendingDiffs);
var target = Node.leaf(editor, path);
var idx = pendingDiffs.findIndex(change => Path.equals(change.path, path));
if (idx < 0) {
var normalized = normalizeStringDiff(target.text, diff);
if (normalized) {
pendingDiffs.push({
path,
diff,
id: idCounter++
});
}
updatePlaceholderVisibility();
return;
}
var merged = mergeStringDiffs(target.text, pendingDiffs[idx].diff, diff);
if (!merged) {
pendingDiffs.splice(idx, 1);
updatePlaceholderVisibility();
return;
}
pendingDiffs[idx] = _objectSpread$5(_objectSpread$5({}, pendingDiffs[idx]), {}, {
diff: merged
});
};
var scheduleAction = function scheduleAction(run) {
var {
at
} = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
insertPositionHint = false;
EDITOR_TO_PENDING_SELECTION.delete(editor);
scheduleOnDOMSelectionChange.cancel();
onDOMSelectionChange.cancel();
if (hasPendingAction()) {
flush();
}
EDITOR_TO_PENDING_ACTION.set(editor, {
at,
run
});
// COMPAT: When deleting before a non-contenteditable element chrome only fires a beforeinput,
// (no input) and doesn't perform any dom mutations. Without a flush timeout we would never flush
// in this case and thus never actually perform the action.
actionTimeoutId = setTimeout(flush);
};
var handleDOMBeforeInput = event => {
var _targetRange2;
if (flushTimeoutId) {
clearTimeout(flushTimeoutId);
flushTimeoutId = null;
}
if (IS_NODE_MAP_DIRTY.get(editor)) {
return;
}
var {
inputType: type
} = event;
var targetRange = null;
var data = event.dataTransfer || event.data || undefined;
if (insertPositionHint !== false && type !== 'insertText' && type !== 'insertCompositionText') {
insertPositionHint = false;
}
var [nativeTargetRange] = event.getTargetRanges();
if (nativeTargetRange) {
targetRange = ReactEditor.toSlateRange(editor, nativeTargetRange, {
exactMatch: false,
suppressThrow: true
});
}
// COMPAT: SelectionChange event is fired after the action is performed, so we
// have to manually get the selection here to ensure it's up-to-date.
var window = ReactEditor.getWindow(editor);
var domSelection = window.getSelection();
if (!targetRange && domSelection) {
nativeTargetRange = domSelection;
targetRange = ReactEditor.toSlateRange(editor, domSelection, {
exactMatch: false,
suppressThrow: true
});
}
targetRange = (_targetRange2 = targetRange) !== null && _targetRange2 !== void 0 ? _targetRange2 : editor.selection;
if (!targetRange) {
return;
}
// By default, the input manager tries to store text diffs so that we can
// defer flushing them at a later point in time. We don't want to flush
// for every input event as this can be expensive. However, there are some
// scenarios where we cannot safely store the text diff and must instead
// schedule an action to let Slate normalize the editor state.
var canStoreDiff = true;
if (type.startsWith('delete')) {
if (Range.isExpanded(targetRange)) {
var [_start, _end] = Range.edges(targetRange);
var _leaf = Node.leaf(editor, _start.path);
if (_leaf.text.length === _start.offset && _end.offset === 0) {
var next = Editor.next(editor, {
at: _start.path,
match: Text$1.isText
});
if (next && Path.equals(next[1], _end.path)) {
targetRange = {
anchor: _end,
focus: _end
};
}
}
}
var direction = type.endsWith('Backward') ? 'backward' : 'forward';
var [start, end] = Range.edges(targetRange);
var [leaf, path] = Editor.leaf(editor, start.path);
var diff = {
text: '',
start: start.offset,
end: end.offset
};
var pendingDiffs = EDITOR_TO_PENDING_DIFFS.get(editor);
var relevantPendingDiffs = pendingDiffs === null || pendingDiffs === void 0 ? void 0 : pendingDiffs.find(change => Path.equals(change.path, path));
var diffs = relevantPendingDiffs ? [relevantPendingDiffs.diff, diff] : [diff];
var text = applyStringDiff(leaf.text, ...diffs);
if (text.length === 0) {
// Text leaf will be removed, so we need to schedule an
// action to remove it so that Slate can normalize instead
// of storing as a diff
canStoreDiff = false;
}
if (Range.isExpanded(targetRange)) {
if (canStoreDiff && Path.equals(targetRange.anchor.path, targetRange.focus.path)) {
var point = {
path: targetRange.anchor.path,
offset: start.offset
};
var range = Editor.range(editor, point, point);
handleUserSelect(range);
return storeDiff(targetRange.anchor.path, {
text: '',
end: end.offset,
start: start.offset
});
}
return scheduleAction(() => Editor.deleteFragment(editor, {
direction
}), {
at: targetRange
});
}
}
switch (type) {
case 'deleteByComposition':
case 'deleteByCut':
case 'deleteByDrag':
{
return scheduleAction(() => Editor.deleteFragment(editor), {
at: targetRange
});
}
case 'deleteContent':
case 'deleteContentForward':
{
var {
anchor
} = targetRange;
if (canStoreDiff && Range.isCollapsed(targetRange)) {
var targetNode = Node.leaf(editor, anchor.path);
if (anchor.offset < targetNode.text.length) {
return storeDiff(anchor.path, {
text: '',
start: anchor.offset,
end: anchor.offset + 1
});
}
}
return scheduleAction(() => Editor.deleteForward(editor), {
at: targetRange
});
}
case 'deleteContentBackward':
{
var _nativeTargetRange;
var {
anchor: _anchor
} = targetRange;
// If we have a mismatch between the native and slate selection being collapsed
// we are most likely deleting a zero-width placeholder and thus should perform it
// as an action to ensure correct behavior (mostly happens with mark placeholders)
var nativeCollapsed = isDOMSelection(nativeTargetRange) ? nativeTargetRange.isCollapsed : !!((_nativeTargetRange = nativeTargetRange) !== null && _nativeTargetRange !== void 0 && _nativeTargetRange.collapsed);
if (canStoreDiff && nativeCollapsed && Range.isCollapsed(targetRange) && _anchor.offset > 0) {
return storeDiff(_anchor.path, {
text: '',
start: _anchor.offset - 1,
end: _anchor.offset
});
}
return scheduleAction(() => Editor.deleteBackward(editor), {
at: targetRange
});
}
case 'deleteEntireSoftLine':
{
return scheduleAction(() => {
Editor.deleteBackward(editor, {
unit: 'line'
});
Editor.deleteForward(editor, {
unit: 'line'
});
}, {
at: targetRange
});
}
case 'deleteHardLineBackward':
{
return scheduleAction(() => Editor.deleteBackward(editor, {
unit: 'block'
}), {
at: targetRange
});
}
case 'deleteSoftLineBackward':
{
return scheduleAction(() => Editor.deleteBackward(editor, {
unit: 'line'
}), {
at: targetRange
});
}
case 'deleteHardLineForward':
{
return scheduleAction(() => Editor.deleteForward(editor, {
unit: 'block'
}), {
at: targetRange
});
}
case 'deleteSoftLineForward':
{
return scheduleAction(() => Editor.deleteForward(editor, {
unit: 'line'
}), {
at: targetRange
});
}
case 'deleteWordBackward':
{
return scheduleAction(() => Editor.deleteBackward(editor, {
unit: 'word'
}), {
at: targetRange
});
}
case 'deleteWordForward':
{
return scheduleAction(() => Editor.deleteForward(editor, {
unit: 'word'
}), {
at: targetRange
});
}
case 'insertLineBreak':
{
return scheduleAction(() => Editor.insertSoftBreak(editor), {
at: targetRange
});
}
case 'insertParagraph':
{
return scheduleAction(() => Editor.insertBreak(editor), {
at: targetRange
});
}
case 'insertCompositionText':
case 'deleteCompositionText':
case 'insertFromComposition':
case 'insertFromDrop':
case 'insertFromPaste':
case 'insertFromYank':
case 'insertReplacementText':
case 'insertText':
{
if (isDataTransfer(data)) {
return scheduleAction(() => ReactEditor.insertData(editor, data), {
at: targetRange
});
}
var _text = data !== null && data !== void 0 ? data : '';
// COMPAT: If we are writing inside a placeholder, the ime inserts the text inside
// the placeholder itself and thus includes the zero-width space inside edit events.
if (EDITOR_TO_PENDING_INSERTION_MARKS.get(editor)) {
_text = _text.replace('\uFEFF', '');
}
// Pastes from the Android clipboard will generate `insertText` events.
// If the copied text contains any newlines, Android will append an
// extra newline to the end of the copied text.
if (type === 'insertText' && /.*\n.*\n$/.test(_text)) {
_text = _text.slice(0, -1);
}
// If the text includes a newline, split it at newlines and paste each component
// string, with soft breaks in between each.
if (_text.includes('\n')) {
return scheduleAction(() => {
var parts = _text.split('\n');
parts.forEach((line, i) => {
if (line) {
Editor.insertText(editor, line);
}
if (i !== parts.length - 1) {
Editor.insertSoftBreak(editor);
}
});
}, {
at: targetRange
});
}
if (Path.equals(targetRange.anchor.path, targetRange.focus.path)) {
var [_start2, _end2] = Range.edges(targetRange);
var _diff = {
start: _start2.offset,
end: _end2.offset,
text: _text
};
// COMPAT: Swiftkey has a weird bug where the target range of the 2nd word
// inserted after a mark placeholder is inserted with an anchor offset off by 1.
// So writing 'some text' will result in 'some ttext'. Luckily all 'normal' insert
// text events are fired with the correct target ranges, only the final 'insertComposition'
// isn't, so we can adjust the target range start offset if we are confident this is the
// swiftkey insert causing the issue.
if (_text && insertPositionHint && type === 'insertCompositionText') {
var hintPosition = insertPositionHint.start + insertPositionHint.text.search(/\S|$/);
var diffPosition = _diff.start + _diff.text.search(/\S|$/);
if (diffPosition === hintPosition + 1 && _diff.end === insertPositionHint.start + insertPositionHint.text.length) {
_diff.start -= 1;
insertPositionHint = null;
scheduleFlush();
} else {
insertPositionHint = false;
}
} else if (type === 'insertText') {
if (insertPositionHint === null) {
insertPositionHint = _diff;
} else if (insertPositionHint && Range.isCollapsed(targetRange) && insertPositionHint.end + insertPositionHint.text.length === _start2.offset) {
insertPositionHint = _objectSpread$5(_objectSpread$5({}, insertPositionHint), {}, {
text: insertPositionHint.text + _text
});
} else {
insertPositionHint = false;
}
} else {
insertPositionHint = false;
}
if (canStoreDiff) {
storeDiff(_start2.path, _diff);
return;
}
}
return scheduleAction(() => Editor.insertText(editor, _text), {
at: targetRange
});
}
}
};
var hasPendingAction = () => {
return !!EDITOR_TO_PENDING_ACTION.get(editor);
};
var hasPendingDiffs = () => {
var _EDITOR_TO_PENDING_DI4;
return !!((_EDITOR_TO_PENDING_DI4 = EDITOR_TO_PENDING_DIFFS.get(editor)) !== null && _EDITOR_TO_PENDING_DI4 !== void 0 && _EDITOR_TO_PENDING_DI4.length);
};
var hasPendingChanges = () => {
return hasPendingAction() || hasPendingDiffs();
};
var isFlushing = () => {
return flushing;
};
var handleUserSelect = range => {
EDITOR_TO_PENDING_SELECTION.set(editor, range);
if (flushTimeoutId) {
clearTimeout(flushTimeoutId);
flushTimeoutId = null;
}
var {
selection
} = editor;
if (!range) {
return;
}
var pathChanged = !selection || !Path.equals(selection.anchor.path, range.anchor.path);
var parentPathChanged = !selection || !Path.equals(selection.anchor.path.slice(0, -1), range.anchor.path.slice(0, -1));
if (pathChanged && insertPositionHint || parentPathChanged) {
insertPositionHint = false;
}
if (pathChanged || hasPendingDiffs()) {
flushTimeoutId = setTimeout(flush, FLUSH_DELAY);
}
};
var handleInput = () => {
if (hasPendingAction() || !hasPendingDiffs()) {
flush();
}
};
var handleKeyDown = _ => {
// COMPAT: Swiftkey closes the keyboard when typing inside a empty node
// directly next to a non-contenteditable element (= the placeholder).
// The only event fired soon enough for us to allow hiding the placeholder
// without swiftkey picking it up is the keydown event, so we have to hide it
// here. See https://github.com/ianstormtaylor/slate/pull/4988#issuecomment-1201050535
if (!hasPendingDiffs()) {
updatePlaceholderVisibility(true);
setTimeout(updatePlaceholderVisibility);
}
};
var scheduleFlush = () => {
if (!hasPendingAction()) {
actionTimeoutId = setTimeout(flush);
}
};
var handleDomMutations = mutations => {
if (hasPendingDiffs() || hasPendingAction()) {
return;
}
if (mutations.some(mutation => isTrackedMutation(editor, mutation, mutations))) {
var _EDITOR_TO_FORCE_REND;
// Cause a re-render to restore the dom state if we encounter tracked mutations without
// a corresponding pending action.
(_EDITOR_TO_FORCE_REND = EDITOR_TO_FORCE_RENDER.get(editor)) === null || _EDITOR_TO_FORCE_REND === void 0 || _EDITOR_TO_FORCE_REND();
}
};
return {
flush,
scheduleFlush,
hasPendingDiffs,
hasPendingAction,
hasPendingChanges,
isFlushing,
handleUserSelect,
handleCompositionEnd,
handleCompositionStart,
handleDOMBeforeInput,
handleKeyDown,
handleDomMutations,
handleInput
};
}
function useIsMounted() {
var isMountedRef = useRef(false);
useEffect(() => {
isMountedRef.current = true;
return () => {
isMountedRef.current = false;
};
}, []);
return isMountedRef.current;
}
/**
* Prevent warning on SSR by falling back to useEffect when DOM isn't available
*/
var useIsomorphicLayoutEffect = CAN_USE_DOM ? useLayoutEffect : useEffect;
function useMutationObserver(node, callback, options) {
var [mutationObserver] = useState(() => new MutationObserver(callback));
useIsomorphicLayoutEffect(() => {
// Discard mutations caused during render phase. This works due to react calling
// useLayoutEffect synchronously after the render phase before the next tick.
mutationObserver.takeRecords();
});
useEffect(() => {
if (!node.current) {
throw new Error('Failed to attach MutationObserver, `node` is undefined');
}
mutationObserver.observe(node.current, options);
return () => mutationObserver.disconnect();
}, [mutationObserver, node, options]);
}
var _excluded$2 = ["node"];
function ownKeys$4(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread$4(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys$4(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys$4(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
var MUTATION_OBSERVER_CONFIG$1 = {
subtree: true,
childList: true,
characterData: true
};
var useAndroidInputManager = !IS_ANDROID ? () => null : _ref => {
var {
node
} = _ref,
options = _objectWithoutProperties(_ref, _excluded$2);
if (!IS_ANDROID) {
return null;
}
var editor = useSlateStatic();
var isMounted = useIsMounted();
var [inputManager] = useState(() => createAndroidInputManager(_objectSpread$4({
editor
}, options)));
useMutationObserver(node, inputManager.handleDomMutations, MUTATION_OBSERVER_CONFIG$1);
EDITOR_TO_SCHEDULE_FLUSH.set(editor, inputManager.scheduleFlush);
if (isMounted) {
inputManager.flush();
}
return inputManager;
};
function ownKeys$3(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread$3(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys$3(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys$3(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
/**
* Leaf content strings.
*/
var String$1 = props => {
var {
isLast,
leaf,
parent,
text
} = props;
var editor = useSlateStatic();
var path = ReactEditor.findPath(editor, text);
var parentPath = Path.parent(path);
var isMarkPlaceholder = Boolean(leaf[MARK_PLACEHOLDER_SYMBOL]);
// COMPAT: Render text inside void nodes with a zero-width space.
// So the node can contain selection but the text is not visible.
if (editor.isVoid(parent)) {
return /*#__PURE__*/React.createElement(ZeroWidthString, {
length: Node.string(parent).length
});
}
// COMPAT: If this is the last text node in an empty block, render a zero-
// width space that will convert into a line break when copying and pasting
// to support expected plain text.
if (leaf.text === '' && parent.children[parent.children.length - 1] === text && !editor.isInline(parent) && Editor.string(editor, parentPath) === '') {
return /*#__PURE__*/React.createElement(ZeroWidthString, {
isLineBreak: true,
isMarkPlaceholder: isMarkPlaceholder
});
}
// COMPAT: If the text is empty, it's because it's on the edge of an inline
// node, so we render a zero-width space so that the selection can be
// inserted next to it still.
if (leaf.text === '') {
return /*#__PURE__*/React.createElement(ZeroWidthString, {
isMarkPlaceholder: isMarkPlaceholder
});
}
// COMPAT: Browsers will collapse trailing new lines at the end of blocks,
// so we need to add an extra trailing new lines to prevent that.
if (isLast && leaf.text.slice(-1) === '\n') {
return /*#__PURE__*/React.createElement(TextString, {
isTrailing: true,
text: leaf.text
});
}
return /*#__PURE__*/React.createElement(TextString, {
text: leaf.text
});
};
/**
* Leaf strings with text in them.
*/
var TextString = props => {
var {
text,
isTrailing = false
} = props;
var ref = useRef(null);
var getTextContent = () => {
return "".concat(text !== null && text !== void 0 ? text : '').concat(isTrailing ? '\n' : '');
};
var [initialText] = useState(getTextContent);
// This is the actual text rendering boundary where we interface with the DOM
// The text is not rendered as part of the virtual DOM, as since we handle basic character insertions natively,
// updating the DOM is not a one way dataflow anymore. What we need here is not reconciliation and diffing
// with previous version of the virtual DOM, but rather diffing with the actual DOM element, and replace the DOM <span> content
// exactly if and only if its current content does not match our current virtual DOM.
// Otherwise the DOM TextNode would always be replaced by React as the user types, which interferes with native text features,
// eg makes native spellcheck opt out from checking the text node.
// useLayoutEffect: updating our span before browser paint
useIsomorphicLayoutEffect(() => {
// null coalescing text to make sure we're not outputing "null" as a string in the extreme case it is nullish at runtime
var textWithTrailing = getTextContent();
if (ref.current && ref.current.textContent !== textWithTrailing) {
ref.current.textContent = textWithTrailing;
}
// intentionally not specifying dependencies, so that this effect runs on every render
// as this effectively replaces "specifying the text in the virtual DOM under the <span> below" on each render
});
// We intentionally render a memoized <span> that only receives the initial text content when the component is mounted.
// We defer to the layout effect above to update the `textContent` of the span element when needed.
return /*#__PURE__*/React.createElement(MemoizedText$1, {
ref: ref
}, initialText);
};
var MemoizedText$1 = /*#__PURE__*/memo( /*#__PURE__*/forwardRef((props, ref) => {
return /*#__PURE__*/React.createElement("span", {
"data-slate-string": true,
ref: ref
}, props.children);
}));
/**
* Leaf strings without text, render as zero-width strings.
*/
var ZeroWidthString = props => {
var {
length = 0,
isLineBreak = false,
isMarkPlaceholder = false
} = props;
var attributes = {
'data-slate-zero-width': isLineBreak ? 'n' : 'z',
'data-slate-length': length
};
if (isMarkPlaceholder) {
attributes['data-slate-mark-placeholder'] = true;
}
return /*#__PURE__*/React.createElement("span", _objectSpread$3({}, attributes), !(IS_ANDROID || IS_IOS) || !isLineBreak ? '\uFEFF' : null, isLineBreak ? /*#__PURE__*/React.createElement("br", null) : null);
};
function ownKeys$2(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread$2(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys$2(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys$2(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
// Delay the placeholder on Android to prevent the keyboard from closing.
// (https://github.com/ianstormtaylor/slate/pull/5368)
var PLACEHOLDER_DELAY = IS_ANDROID ? 300 : 0;
function disconnectPlaceholderResizeObserver(placeholderResizeObserver, releaseObserver) {
if (placeholderResizeObserver.current) {
placeholderResizeObserver.current.disconnect();
if (releaseObserver) {
placeholderResizeObserver.current = null;
}
}
}
function clearTimeoutRef(timeoutRef) {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
}
/**
* Individual leaves in a text node with unique formatting.
*/
var Leaf = props => {
var {
leaf,
isLast,
text,
parent,
renderPlaceholder,
renderLeaf = props => /*#__PURE__*/React.createElement(DefaultLeaf, _objectSpread$2({}, props))
} = props;
var editor = useSlateStatic();
var placeholderResizeObserver = useRef(null);
var placeholderRef = useRef(null);
var [showPlaceholder, setShowPlaceholder] = useState(false);
var showPlaceholderTimeoutRef = useRef(null);
var callbackPlaceholderRef = useCallback(placeholderEl => {
disconnectPlaceholderResizeObserver(placeholderResizeObserver, placeholderEl == null);
if (placeholderEl == null) {
var _leaf$onPlaceholderRe;
EDITOR_TO_PLACEHOLDER_ELEMENT.delete(editor);
(_leaf$onPlaceholderRe = leaf.onPlaceholderResize) === null || _leaf$onPlaceholderRe === void 0 || _leaf$onPlaceholderRe.call(leaf, null);
} else {
EDITOR_TO_PLACEHOLDER_ELEMENT.set(editor, placeholderEl);
if (!placeholderResizeObserver.current) {
// Create a new observer and observe the placeholder element.
var ResizeObserver$1 = window.ResizeObserver || ResizeObserver;
placeholderResizeObserver.current = new ResizeObserver$1(() => {
var _leaf$onPlaceholderRe2;
(_leaf$onPlaceholderRe2 = leaf.onPlaceholderResize) === null || _leaf$onPlaceholderRe2 === void 0 || _leaf$onPlaceholderRe2.call(leaf, placeholderEl);
});
}
placeholderResizeObserver.current.observe(placeholderEl);
placeholderRef.current = placeholderEl;
}
}, [placeholderRef, leaf, editor]);
var children = /*#__PURE__*/React.createElement(String$1, {
isLast: isLast,
leaf: leaf,
parent: parent,
text: text
});
var leafIsPlaceholder = Boolean(leaf[PLACEHOLDER_SYMBOL]);
useEffect(() => {
if (leafIsPlaceholder) {
if (!showPlaceholderTimeoutRef.current) {
// Delay the placeholder, so it will not render in a selection
showPlaceholderTimeoutRef.current = setTimeout(() => {
setShowPlaceholder(true);
showPlaceholderTimeoutRef.current = null;
}, PLACEHOLDER_DELAY);
}
} else {
clearTimeoutRef(showPlaceholderTimeoutRef);
setShowPlaceholder(false);
}
return () => clearTimeoutRef(showPlaceholderTimeoutRef);
}, [leafIsPlaceholder, setShowPlaceholder]);
if (leafIsPlaceholder && showPlaceholder) {
var placeholderProps = {
children: leaf.placeholder,
attributes: {
'data-slate-placeholder': true,
style: {
position: 'absolute',
top: 0,
pointerEvents: 'none',
width: '100%',
maxWidth: '100%',
display: 'block',
opacity: '0.333',
userSelect: 'none',
textDecoration: 'none',
// Fixes https://github.com/udecode/plate/issues/2315
WebkitUserModify: IS_WEBKIT ? 'inherit' : undefined
},
contentEditable: false,
ref: callbackPlaceholderRef
}
};
children = /*#__PURE__*/React.createElement(React.Fragment, null, renderPlaceholder(placeholderProps), children);
}
// COMPAT: Having the `data-` attributes on these leaf elements ensures that
// in certain misbehaving browsers they aren't weirdly cloned/destroyed by
// contenteditable behaviors. (2019/05/08)
var attributes = {
'data-slate-leaf': true
};
return renderLeaf({
attributes,
children,
leaf,
text
});
};
var MemoizedLeaf = /*#__PURE__*/React.memo(Leaf, (prev, next) => {
return next.parent === prev.parent && next.isLast === prev.isLast && next.renderLeaf === prev.renderLeaf && next.renderPlaceholder === prev.renderPlaceholder && next.text === prev.text && Text$1.equals(next.leaf, prev.leaf) && next.leaf[PLACEHOLDER_SYMBOL] === prev.leaf[PLACEHOLDER_SYMBOL];
});
var DefaultLeaf = props => {
var {
attributes,
children
} = props;
return /*#__PURE__*/React.createElement("span", _objectSpread$2({}, attributes), children);
};
/**
* Text.
*/
var Text = props => {
var {
decorations,
isLast,
parent,
renderPlaceholder,
renderLeaf,
text
} = props;
var editor = useSlateStatic();
var ref = useRef(null);
var leaves = Text$1.decorations(text, decorations);
var key = ReactEditor.findKey(editor, text);
var children = [];
for (var i = 0; i < leaves.length; i++) {
var leaf = leaves[i];
children.push( /*#__PURE__*/React.createElement(MemoizedLeaf, {
isLast: isLast && i === leaves.length - 1,
key: "".concat(key.id, "-").concat(i),
renderPlaceholder: renderPlaceholder,
leaf: leaf,
text: text,
parent: parent,
renderLeaf: renderLeaf
}));
}
// Update element-related weak maps with the DOM element ref.
var callbackRef = useCallback(span => {
var KEY_TO_ELEMENT = EDITOR_TO_KEY_TO_ELEMENT.get(editor);
if (span) {
KEY_TO_ELEMENT === null || KEY_TO_ELEMENT === void 0 || KEY_TO_ELEMENT.set(key, span);
NODE_TO_ELEMENT.set(text, span);
ELEMENT_TO_NODE.set(span, text);
} else {
KEY_TO_ELEMENT === null || KEY_TO_ELEMENT === void 0 || KEY_TO_ELEMENT.delete(key);
NODE_TO_ELEMENT.delete(text);
if (ref.current) {
ELEMENT_TO_NODE.delete(ref.current);
}
}
ref.current = span;
}, [ref, editor, key, text]);
return /*#__PURE__*/React.createElement("span", {
"data-slate-node": "text",
ref: callbackRef
}, children);
};
var MemoizedText = /*#__PURE__*/React.memo(Text, (prev, next) => {
return next.parent === prev.parent && next.isLast === prev.isLast && next.renderLeaf === prev.renderLeaf && next.renderPlaceholder === prev.renderPlaceholder && next.text === prev.text && isTextDecorationsEqual(next.decorations, prev.decorations);
});
function ownKeys$1(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread$1(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys$1(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys$1(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
/**
* Element.
*/
var Element = props => {
var {
decorations,
element,
renderElement = p => /*#__PURE__*/React.createElement(DefaultElement, _objectSpread$1({}, p)),
renderPlaceholder,
renderLeaf,
selection
} = props;
var editor = useSlateStatic();
var readOnly = useReadOnly();
var isInline = editor.isInline(element);
var key = ReactEditor.findKey(editor, element);
var ref = useCallback(ref => {
// Update element-related weak maps with the DOM element ref.
var KEY_TO_ELEMENT = EDITOR_TO_KEY_TO_ELEMENT.get(editor);
if (ref) {
KEY_TO_ELEMENT === null || KEY_TO_ELEMENT === void 0 || KEY_TO_ELEMENT.set(key, ref);
NODE_TO_ELEMENT.set(element, ref);
ELEMENT_TO_NODE.set(ref, element);
} else {
KEY_TO_ELEMENT === null || KEY_TO_ELEMENT === void 0 || KEY_TO_ELEMENT.delete(key);
NODE_TO_ELEMENT.delete(element);
}
}, [editor, key, element]);
var children = useChildren({
decorations,
node: element,
renderElement,
renderPlaceholder,
renderLeaf,
selection
});
// Attributes that the developer must mix into the element in their
// custom node renderer component.
var attributes = {
'data-slate-node': 'element',
ref
};
if (isInline) {
attributes['data-slate-inline'] = true;
}
// If it's a block node with inline children, add the proper `dir` attribute
// for text direction.
if (!isInline && Editor.hasInlines(editor, element)) {
var text = Node.string(element);
var dir = getDirection(text);
if (dir === 'rtl') {
attributes.dir = dir;
}
}
// If it's a void node, wrap the children in extra void-specific elements.
if (Editor.isVoid(editor, element)) {
attributes['data-slate-void'] = true;
if (!readOnly && isInline) {
attributes.contentEditable = false;
}
var Tag = isInline ? 'span' : 'div';
var [[_text]] = Node.texts(element);
children = /*#__PURE__*/React.createElement(Tag, {
"data-slate-spacer": true,
style: {
height: '0',
color: 'transparent',
outline: 'none',
position: 'absolute'
}
}, /*#__PURE__*/React.createElement(MemoizedText, {
renderPlaceholder: renderPlaceholder,
decorations: [],
isLast: false,
parent: element,
text: _text
}));
NODE_TO_INDEX.set(_text, 0);
NODE_TO_PARENT.set(_text, element);
}
return renderElement({
attributes,
children,
element
});
};
var MemoizedElement = /*#__PURE__*/React.memo(Element, (prev, next) => {
return prev.element === next.element && prev.renderElement === next.renderElement && prev.renderLeaf === next.renderLeaf && prev.renderPlaceholder === next.renderPlaceholder && isElementDecorationsEqual(prev.decorations, next.decorations) && (prev.selection === next.selection || !!prev.selection && !!next.selection && Range.equals(prev.selection, next.selection));
});
/**
* The default element renderer.
*/
var DefaultElement = props => {
var {
attributes,
children,
element
} = props;
var editor = useSlateStatic();
var Tag = editor.isInline(element) ? 'span' : 'div';
return /*#__PURE__*/React.createElement(Tag, _objectSpread$1(_objectSpread$1({}, attributes), {}, {
style: {
position: 'relative'
}
}), children);
};
/**
* A React context for sharing the `decorate` prop of the editable.
*/
var DecorateContext = /*#__PURE__*/createContext(() => []);
/**
* Get the current `decorate` prop of the editable.
*/
var useDecorate = () => {
return useContext(DecorateContext);
};
/**
* A React context for sharing the `selected` state of an element.
*/
var SelectedContext = /*#__PURE__*/createContext(false);
/**
* Get the current `selected` state of an element.
*/
var useSelected = () => {
return useContext(SelectedContext);
};
/**
* Children.
*/
var useChildren = props => {
var {
decorations,
node,
renderElement,
renderPlaceholder,
renderLeaf,
selection
} = props;
var decorate = useDecorate();
var editor = useSlateStatic();
IS_NODE_MAP_DIRTY.set(editor, false);
var path = ReactEditor.findPath(editor, node);
var children = [];
var isLeafBlock = Element$1.isElement(node) && !editor.isInline(node) && Editor.hasInlines(editor, node);
for (var i = 0; i < node.children.length; i++) {
var p = path.concat(i);
var n = node.children[i];
var key = ReactEditor.findKey(editor, n);
var range = Editor.range(editor, p);
var sel = selection && Range.intersection(range, selection);
var ds = decorate([n, p]);
for (var dec of decorations) {
var d = Range.intersection(dec, range);
if (d) {
ds.push(d);
}
}
if (Element$1.isElement(n)) {
children.push( /*#__PURE__*/React.createElement(SelectedContext.Provider, {
key: "provider-".concat(key.id),
value: !!sel
}, /*#__PURE__*/React.createElement(MemoizedElement, {
decorations: ds,
element: n,
key: key.id,
renderElement: renderElement,
renderPlaceholder: renderPlaceholder,
renderLeaf: renderLeaf,
selection: sel
})));
} else {
children.push( /*#__PURE__*/React.createElement(MemoizedText, {
decorations: ds,
key: key.id,
isLast: isLeafBlock && i === node.children.length - 1,
parent: node,
renderPlaceholder: renderPlaceholder,
renderLeaf: renderLeaf,
text: n
}));
}
NODE_TO_INDEX.set(n, i);
NODE_TO_PARENT.set(n, node);
}
return children;
};
/**
* A React context for sharing the `readOnly` state of the editor.
*/
var ReadOnlyContext = /*#__PURE__*/createContext(false);
/**
* Get the current `readOnly` state of the editor.
*/
var useReadOnly = () => {
return useContext(ReadOnlyContext);
};
var SlateContext = /*#__PURE__*/createContext(null);
/**
* Get the current editor object from the React context.
*/
var useSlate = () => {
var context = useContext(SlateContext);
if (!context) {
throw new Error("The `useSlate` hook must be used inside the <Slate> component's context.");
}
var {
editor
} = context;
return editor;
};
var useSlateWithV = () => {
var context = useContext(SlateContext);
if (!context) {
throw new Error("The `useSlate` hook must be used inside the <Slate> component's context.");
}
return context;
};
function useTrackUserInput() {
var editor = useSlateStatic();
var receivedUserInput = useRef(false);
var animationFrameIdRef = useRef(0);
var onUserInput = useCallback(() => {
if (receivedUserInput.current) {
return;
}
receivedUserInput.current = true;
var window = ReactEditor.getWindow(editor);
window.cancelAnimationFrame(animationFrameIdRef.current);
animationFram