UNPKG

slate-react

Version:

Tools for building completely customizable richtext editors with React.

1,308 lines (1,284 loc) • 122 kB
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