UNPKG

slate-react

Version:

Tools for building completely customizable richtext editors with React.

1,438 lines (1,196 loc) • 143 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var React = require('react'); var slate = require('slate'); var getDirection = require('direction'); var throttle = require('lodash/throttle'); var scrollIntoView = require('scroll-into-view-if-needed'); var isHotkey = require('is-hotkey'); var invariant = require('tiny-invariant'); var ReactDOM = require('react-dom'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var React__default = /*#__PURE__*/_interopDefaultLegacy(React); var getDirection__default = /*#__PURE__*/_interopDefaultLegacy(getDirection); var throttle__default = /*#__PURE__*/_interopDefaultLegacy(throttle); var scrollIntoView__default = /*#__PURE__*/_interopDefaultLegacy(scrollIntoView); var invariant__default = /*#__PURE__*/_interopDefaultLegacy(invariant); var ReactDOM__default = /*#__PURE__*/_interopDefaultLegacy(ReactDOM); function unwrapExports (x) { return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; } function createCommonjsModule(fn, module) { return module = { exports: {} }, fn(module, module.exports), module.exports; } var defineProperty = createCommonjsModule(function (module) { function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } module.exports = _defineProperty; module.exports["default"] = module.exports, module.exports.__esModule = true; }); var _defineProperty = unwrapExports(defineProperty); var arrayWithHoles = createCommonjsModule(function (module) { function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } module.exports = _arrayWithHoles; module.exports["default"] = module.exports, module.exports.__esModule = true; }); unwrapExports(arrayWithHoles); var iterableToArrayLimit = createCommonjsModule(function (module) { function _iterableToArrayLimit(arr, i) { if (typeof Symbol === "undefined" || !(Symbol.iterator in Object(arr))) return; var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } module.exports = _iterableToArrayLimit; module.exports["default"] = module.exports, module.exports.__esModule = true; }); unwrapExports(iterableToArrayLimit); var arrayLikeToArray = createCommonjsModule(function (module) { function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } module.exports = _arrayLikeToArray; module.exports["default"] = module.exports, module.exports.__esModule = true; }); unwrapExports(arrayLikeToArray); var unsupportedIterableToArray = createCommonjsModule(function (module) { function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return arrayLikeToArray(o, minLen); } module.exports = _unsupportedIterableToArray; module.exports["default"] = module.exports, module.exports.__esModule = true; }); unwrapExports(unsupportedIterableToArray); var nonIterableRest = createCommonjsModule(function (module) { function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } module.exports = _nonIterableRest; module.exports["default"] = module.exports, module.exports.__esModule = true; }); unwrapExports(nonIterableRest); var slicedToArray = createCommonjsModule(function (module) { function _slicedToArray(arr, i) { return arrayWithHoles(arr) || iterableToArrayLimit(arr, i) || unsupportedIterableToArray(arr, i) || nonIterableRest(); } module.exports = _slicedToArray; module.exports["default"] = module.exports, module.exports.__esModule = true; }); var _slicedToArray = unwrapExports(slicedToArray); var objectWithoutPropertiesLoose = createCommonjsModule(function (module) { 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; } module.exports = _objectWithoutPropertiesLoose; module.exports["default"] = module.exports, module.exports.__esModule = true; }); unwrapExports(objectWithoutPropertiesLoose); var objectWithoutProperties = createCommonjsModule(function (module) { 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; } module.exports = _objectWithoutProperties; module.exports["default"] = module.exports, module.exports.__esModule = true; }); var _objectWithoutProperties = unwrapExports(objectWithoutProperties); /** * Leaf content strings. */ var String = function String(props) { var isLast = props.isLast, leaf = props.leaf, parent = props.parent, text = props.text; var editor = useSlateStatic(); var path = ReactEditor.findPath(editor, text); var parentPath = slate.Path.parent(path); // 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__default['default'].createElement(ZeroWidthString, { length: slate.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) && slate.Editor.string(editor, parentPath) === '') { return /*#__PURE__*/React__default['default'].createElement(ZeroWidthString, { isLineBreak: true }); } // 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__default['default'].createElement(ZeroWidthString, null); } // 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__default['default'].createElement(TextString, { isTrailing: true, text: leaf.text }); } return /*#__PURE__*/React__default['default'].createElement(TextString, { text: leaf.text }); }; /** * Leaf strings with text in them. */ var TextString = function TextString(props) { var text = props.text, _props$isTrailing = props.isTrailing, isTrailing = _props$isTrailing === void 0 ? false : _props$isTrailing; return /*#__PURE__*/React__default['default'].createElement("span", { "data-slate-string": true }, text, isTrailing ? '\n' : null); }; /** * Leaf strings without text, render as zero-width strings. */ var ZeroWidthString = function ZeroWidthString(props) { var _props$length = props.length, length = _props$length === void 0 ? 0 : _props$length, _props$isLineBreak = props.isLineBreak, isLineBreak = _props$isLineBreak === void 0 ? false : _props$isLineBreak; return /*#__PURE__*/React__default['default'].createElement("span", { "data-slate-zero-width": isLineBreak ? 'n' : 'z', "data-slate-length": length }, "\uFEFF", isLineBreak ? /*#__PURE__*/React__default['default'].createElement("br", null) : null); }; /** * Two weak maps that allow us rebuild a path given a node. They are populated * at render time such that after a render occurs we can always backtrack. */ var NODE_TO_INDEX = new WeakMap(); var NODE_TO_PARENT = new WeakMap(); /** * Weak maps that allow us to go between Slate nodes and DOM nodes. These * are used to resolve DOM event-related logic into Slate actions. */ var EDITOR_TO_WINDOW = new WeakMap(); var EDITOR_TO_ELEMENT = new WeakMap(); var ELEMENT_TO_NODE = new WeakMap(); var KEY_TO_ELEMENT = new WeakMap(); var NODE_TO_ELEMENT = new WeakMap(); var NODE_TO_KEY = new WeakMap(); /** * Weak maps for storing editor-related state. */ var IS_READ_ONLY = new WeakMap(); var IS_FOCUSED = new WeakMap(); /** * Weak map for associating the context `onChange` context with the plugin. */ var EDITOR_TO_ON_CHANGE = new WeakMap(); var EDITOR_TO_RESTORE_DOM = new WeakMap(); /** * Symbols. */ var PLACEHOLDER_SYMBOL = Symbol('placeholder'); // prevent inconsistent rendering by React with IME input var keyForString = 0; /** * Individual leaves in a text node with unique formatting. */ var Leaf = function Leaf(props) { var leaf = props.leaf, isLast = props.isLast, text = props.text, parent = props.parent, renderPlaceholder = props.renderPlaceholder, _props$renderLeaf = props.renderLeaf, renderLeaf = _props$renderLeaf === void 0 ? function (props) { return /*#__PURE__*/React__default['default'].createElement(DefaultLeaf, Object.assign({}, props)); } : _props$renderLeaf; var placeholderRef = React.useRef(null); React.useEffect(function () { var placeholderEl = placeholderRef === null || placeholderRef === void 0 ? void 0 : placeholderRef.current; var editorEl = document.querySelector('[data-slate-editor="true"]'); if (!placeholderEl || !editorEl) { return; } editorEl.style.minHeight = "".concat(placeholderEl.clientHeight, "px"); return function () { editorEl.style.minHeight = 'auto'; }; }, [placeholderRef, leaf]); var children = /*#__PURE__*/React__default['default'].createElement(String, { key: keyForString++, isLast: isLast, leaf: leaf, parent: parent, text: text }); if (leaf[PLACEHOLDER_SYMBOL]) { var placeholderProps = { children: leaf.placeholder, attributes: { 'data-slate-placeholder': true, style: { position: 'absolute', pointerEvents: 'none', width: '100%', maxWidth: '100%', display: 'block', opacity: '0.333', userSelect: 'none', textDecoration: 'none' }, contentEditable: false, ref: placeholderRef } }; children = /*#__PURE__*/React__default['default'].createElement(React__default['default'].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: attributes, children: children, leaf: leaf, text: text }); }; var MemoizedLeaf = /*#__PURE__*/React__default['default'].memo(Leaf, function (prev, next) { return next.parent === prev.parent && next.isLast === prev.isLast && next.renderLeaf === prev.renderLeaf && next.renderPlaceholder === prev.renderPlaceholder && next.text === prev.text && next.leaf.text === prev.leaf.text && slate.Text.matches(next.leaf, prev.leaf) && next.leaf[PLACEHOLDER_SYMBOL] === prev.leaf[PLACEHOLDER_SYMBOL]; }); var DefaultLeaf = function DefaultLeaf(props) { var attributes = props.attributes, children = props.children; return /*#__PURE__*/React__default['default'].createElement("span", Object.assign({}, attributes), children); }; var IS_IOS = typeof navigator !== 'undefined' && typeof window !== 'undefined' && /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; var IS_APPLE = typeof navigator !== 'undefined' && /Mac OS X/.test(navigator.userAgent); var IS_ANDROID = typeof navigator !== 'undefined' && /Android/.test(navigator.userAgent); var IS_FIREFOX = typeof navigator !== 'undefined' && /^(?!.*Seamonkey)(?=.*Firefox).*/i.test(navigator.userAgent); var IS_SAFARI = typeof navigator !== 'undefined' && /Version\/[\d\.]+.*Safari/.test(navigator.userAgent); // "modern" Edge was released at 79.x var IS_EDGE_LEGACY = typeof navigator !== 'undefined' && /Edge?\/(?:[0-6][0-9]|[0-7][0-8])/i.test(navigator.userAgent); var IS_CHROME = typeof navigator !== 'undefined' && /Chrome/i.test(navigator.userAgent); // Native `beforeInput` events don't work well with react on Chrome 75 // and older, Chrome 76+ can use `beforeInput` though. var IS_CHROME_LEGACY = typeof navigator !== 'undefined' && /Chrome?\/(?:[0-7][0-5]|[0-6][0-9])/i.test(navigator.userAgent); // Firefox did not support `beforeInput` until `v87`. var IS_FIREFOX_LEGACY = typeof navigator !== 'undefined' && /^(?!.*Seamonkey)(?=.*Firefox\/(?:[0-7][0-9]|[0-8][0-6])).*/i.test(navigator.userAgent); // Check if DOM is available as React does internally. // https://github.com/facebook/react/blob/master/packages/shared/ExecutionEnvironment.js var CAN_USE_DOM = !!(typeof window !== 'undefined' && typeof window.document !== 'undefined' && typeof window.document.createElement !== 'undefined'); // COMPAT: Firefox/Edge Legacy don't support the `beforeinput` event // Chrome Legacy doesn't support `beforeinput` correctly var HAS_BEFORE_INPUT_SUPPORT = !IS_CHROME_LEGACY && !IS_EDGE_LEGACY && // globalThis is undefined in older browsers typeof globalThis !== 'undefined' && globalThis.InputEvent && // @ts-ignore The `getTargetRanges` property isn't recognized. typeof globalThis.InputEvent.prototype.getTargetRanges === 'function'; /** * Prevent warning on SSR by falling back to useEffect when DOM isn't available */ var useIsomorphicLayoutEffect = CAN_USE_DOM ? React.useLayoutEffect : React.useEffect; var shallowCompare = function shallowCompare(obj1, obj2) { return Object.keys(obj1).length === Object.keys(obj2).length && Object.keys(obj1).every(function (key) { return obj2.hasOwnProperty(key) && obj1[key] === obj2[key]; }); }; /** * Check if a list of decorator ranges are equal to another. * * PERF: this requires the two lists to also have the ranges inside them in the * same order, but this is an okay constraint for us since decorations are * kept in order, and the odd case where they aren't is okay to re-render for. */ var isDecoratorRangeListEqual = function isDecoratorRangeListEqual(list, another) { if (list.length !== another.length) { return false; } for (var i = 0; i < list.length; i++) { var range = list[i]; var other = another[i]; var rangeAnchor = range.anchor, rangeFocus = range.focus, rangeOwnProps = _objectWithoutProperties(range, ["anchor", "focus"]); var otherAnchor = other.anchor, otherFocus = other.focus, otherOwnProps = _objectWithoutProperties(other, ["anchor", "focus"]); if (!slate.Range.equals(range, other) || range[PLACEHOLDER_SYMBOL] !== other[PLACEHOLDER_SYMBOL] || !shallowCompare(rangeOwnProps, otherOwnProps)) { return false; } } return true; }; /** * Text. */ var Text = function Text(props) { var decorations = props.decorations, isLast = props.isLast, parent = props.parent, renderPlaceholder = props.renderPlaceholder, renderLeaf = props.renderLeaf, text = props.text; var editor = useSlateStatic(); var ref = React.useRef(null); var leaves = slate.Text.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__default['default'].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. useIsomorphicLayoutEffect(function () { if (ref.current) { KEY_TO_ELEMENT.set(key, ref.current); NODE_TO_ELEMENT.set(text, ref.current); ELEMENT_TO_NODE.set(ref.current, text); } else { KEY_TO_ELEMENT["delete"](key); NODE_TO_ELEMENT["delete"](text); } }); return /*#__PURE__*/React__default['default'].createElement("span", { "data-slate-node": "text", ref: ref }, children); }; var MemoizedText = /*#__PURE__*/React__default['default'].memo(Text, function (prev, next) { return next.parent === prev.parent && next.isLast === prev.isLast && next.renderLeaf === prev.renderLeaf && next.text === prev.text && isDecoratorRangeListEqual(next.decorations, prev.decorations); }); /** * A React context for sharing the `selected` state of an element. */ var SelectedContext = /*#__PURE__*/React.createContext(false); /** * Get the current `selected` state of an element. */ var useSelected = function useSelected() { return React.useContext(SelectedContext); }; /** * Element. */ var Element = function Element(props) { var decorations = props.decorations, element = props.element, _props$renderElement = props.renderElement, renderElement = _props$renderElement === void 0 ? function (p) { return /*#__PURE__*/React__default['default'].createElement(DefaultElement, Object.assign({}, p)); } : _props$renderElement, renderPlaceholder = props.renderPlaceholder, renderLeaf = props.renderLeaf, selection = props.selection; var ref = React.useRef(null); var editor = useSlateStatic(); var readOnly = useReadOnly(); var isInline = editor.isInline(element); var key = ReactEditor.findKey(editor, element); var children = useChildren({ decorations: decorations, node: element, renderElement: renderElement, renderPlaceholder: renderPlaceholder, renderLeaf: renderLeaf, selection: selection }); // Attributes that the developer must mix into the element in their // custom node renderer component. var attributes = { 'data-slate-node': 'element', ref: 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 && slate.Editor.hasInlines(editor, element)) { var text = slate.Node.string(element); var dir = getDirection__default['default'](text); if (dir === 'rtl') { attributes.dir = dir; } } // If it's a void node, wrap the children in extra void-specific elements. if (slate.Editor.isVoid(editor, element)) { attributes['data-slate-void'] = true; if (!readOnly && isInline) { attributes.contentEditable = false; } var Tag = isInline ? 'span' : 'div'; var _Node$texts = slate.Node.texts(element), _Node$texts2 = _slicedToArray(_Node$texts, 1), _Node$texts2$ = _slicedToArray(_Node$texts2[0], 1), _text = _Node$texts2$[0]; children = readOnly ? null : /*#__PURE__*/React__default['default'].createElement(Tag, { "data-slate-spacer": true, style: { height: '0', color: 'transparent', outline: 'none', position: 'absolute' } }, /*#__PURE__*/React__default['default'].createElement(MemoizedText, { renderPlaceholder: renderPlaceholder, decorations: [], isLast: false, parent: element, text: _text })); NODE_TO_INDEX.set(_text, 0); NODE_TO_PARENT.set(_text, element); } // Update element-related weak maps with the DOM element ref. useIsomorphicLayoutEffect(function () { if (ref.current) { KEY_TO_ELEMENT.set(key, ref.current); NODE_TO_ELEMENT.set(element, ref.current); ELEMENT_TO_NODE.set(ref.current, element); } else { KEY_TO_ELEMENT["delete"](key); NODE_TO_ELEMENT["delete"](element); } }); return /*#__PURE__*/React__default['default'].createElement(SelectedContext.Provider, { value: !!selection }, renderElement({ attributes: attributes, children: children, element: element })); }; var MemoizedElement = /*#__PURE__*/React__default['default'].memo(Element, function (prev, next) { return prev.element === next.element && prev.renderElement === next.renderElement && prev.renderLeaf === next.renderLeaf && isDecoratorRangeListEqual(prev.decorations, next.decorations) && (prev.selection === next.selection || !!prev.selection && !!next.selection && slate.Range.equals(prev.selection, next.selection)); }); /** * The default element renderer. */ var DefaultElement = function DefaultElement(props) { var attributes = props.attributes, children = props.children, element = props.element; var editor = useSlateStatic(); var Tag = editor.isInline(element) ? 'span' : 'div'; return /*#__PURE__*/React__default['default'].createElement(Tag, Object.assign({}, attributes, { style: { position: 'relative' } }), children); }; /** * A React context for sharing the editor object. */ var EditorContext = /*#__PURE__*/React.createContext(null); /** * Get the current editor object from the React context. */ var useSlateStatic = function useSlateStatic() { var editor = React.useContext(EditorContext); if (!editor) { throw new Error("The `useSlateStatic` hook must be used inside the <Slate> component's context."); } return editor; }; /** * A React context for sharing the `decorate` prop of the editable. */ var DecorateContext = /*#__PURE__*/React.createContext(function () { return []; }); /** * Get the current `decorate` prop of the editable. */ var useDecorate = function useDecorate() { return React.useContext(DecorateContext); }; function _createForOfIteratorHelper(o, allowArrayLike) { var it; if (typeof Symbol === "undefined" || o[Symbol.iterator] == null) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = o[Symbol.iterator](); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; } function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } /** * Children. */ var useChildren = function useChildren(props) { var decorations = props.decorations, node = props.node, renderElement = props.renderElement, renderPlaceholder = props.renderPlaceholder, renderLeaf = props.renderLeaf, selection = props.selection; var decorate = useDecorate(); var editor = useSlateStatic(); var path = ReactEditor.findPath(editor, node); var children = []; var isLeafBlock = slate.Element.isElement(node) && !editor.isInline(node) && slate.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 = slate.Editor.range(editor, p); var sel = selection && slate.Range.intersection(range, selection); var ds = decorate([n, p]); var _iterator = _createForOfIteratorHelper(decorations), _step; try { for (_iterator.s(); !(_step = _iterator.n()).done;) { var dec = _step.value; var d = slate.Range.intersection(dec, range); if (d) { ds.push(d); } } } catch (err) { _iterator.e(err); } finally { _iterator.f(); } if (slate.Element.isElement(n)) { children.push( /*#__PURE__*/React__default['default'].createElement(MemoizedElement, { decorations: ds, element: n, key: key.id, renderElement: renderElement, renderPlaceholder: renderPlaceholder, renderLeaf: renderLeaf, selection: sel })); } else { children.push( /*#__PURE__*/React__default['default'].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; }; /** * Hotkey mappings for each platform. */ var HOTKEYS = { bold: 'mod+b', compose: ['down', 'left', 'right', 'up', 'backspace', 'enter'], moveBackward: 'left', moveForward: 'right', moveWordBackward: 'ctrl+left', moveWordForward: 'ctrl+right', deleteBackward: 'shift?+backspace', deleteForward: 'shift?+delete', extendBackward: 'shift+left', extendForward: 'shift+right', italic: 'mod+i', splitBlock: 'shift?+enter', undo: 'mod+z' }; var APPLE_HOTKEYS = { moveLineBackward: 'opt+up', moveLineForward: 'opt+down', moveWordBackward: 'opt+left', moveWordForward: 'opt+right', deleteBackward: ['ctrl+backspace', 'ctrl+h'], deleteForward: ['ctrl+delete', 'ctrl+d'], deleteLineBackward: 'cmd+shift?+backspace', deleteLineForward: ['cmd+shift?+delete', 'ctrl+k'], deleteWordBackward: 'opt+shift?+backspace', deleteWordForward: 'opt+shift?+delete', extendLineBackward: 'opt+shift+up', extendLineForward: 'opt+shift+down', redo: 'cmd+shift+z', transposeCharacter: 'ctrl+t' }; var WINDOWS_HOTKEYS = { deleteWordBackward: 'ctrl+shift?+backspace', deleteWordForward: 'ctrl+shift?+delete', redo: ['ctrl+y', 'ctrl+shift+z'] }; /** * Create a platform-aware hotkey checker. */ var create = function create(key) { var generic = HOTKEYS[key]; var apple = APPLE_HOTKEYS[key]; var windows = WINDOWS_HOTKEYS[key]; var isGeneric = generic && isHotkey.isKeyHotkey(generic); var isApple = apple && isHotkey.isKeyHotkey(apple); var isWindows = windows && isHotkey.isKeyHotkey(windows); return function (event) { if (isGeneric && isGeneric(event)) return true; if (IS_APPLE && isApple && isApple(event)) return true; if (!IS_APPLE && isWindows && isWindows(event)) return true; return false; }; }; /** * Hotkeys. */ var Hotkeys = { isBold: create('bold'), isCompose: create('compose'), isMoveBackward: create('moveBackward'), isMoveForward: create('moveForward'), isDeleteBackward: create('deleteBackward'), isDeleteForward: create('deleteForward'), isDeleteLineBackward: create('deleteLineBackward'), isDeleteLineForward: create('deleteLineForward'), isDeleteWordBackward: create('deleteWordBackward'), isDeleteWordForward: create('deleteWordForward'), isExtendBackward: create('extendBackward'), isExtendForward: create('extendForward'), isExtendLineBackward: create('extendLineBackward'), isExtendLineForward: create('extendLineForward'), isItalic: create('italic'), isMoveLineBackward: create('moveLineBackward'), isMoveLineForward: create('moveLineForward'), isMoveWordBackward: create('moveWordBackward'), isMoveWordForward: create('moveWordForward'), isRedo: create('redo'), isSplitBlock: create('splitBlock'), isTransposeCharacter: create('transposeCharacter'), isUndo: create('undo') }; /** * A React context for sharing the `readOnly` state of the editor. */ var ReadOnlyContext = /*#__PURE__*/React.createContext(false); /** * Get the current `readOnly` state of the editor. */ var useReadOnly = function useReadOnly() { return React.useContext(ReadOnlyContext); }; /** * A React context for sharing the editor object, in a way that re-renders the * context whenever changes occur. */ var SlateContext = /*#__PURE__*/React.createContext(null); /** * Get the current editor object from the React context. */ var useSlate = function useSlate() { var context = React.useContext(SlateContext); if (!context) { throw new Error("The `useSlate` hook must be used inside the <SlateProvider> component's context."); } var _context = _slicedToArray(context, 1), editor = _context[0]; return editor; }; /** * Returns the host window of a DOM node */ var getDefaultView = function getDefaultView(value) { return value && value.ownerDocument && value.ownerDocument.defaultView || null; }; /** * Check if a DOM node is a comment node. */ var isDOMComment = function isDOMComment(value) { return isDOMNode(value) && value.nodeType === 8; }; /** * Check if a DOM node is an element node. */ var isDOMElement = function isDOMElement(value) { return isDOMNode(value) && value.nodeType === 1; }; /** * Check if a value is a DOM node. */ var isDOMNode = function isDOMNode(value) { var window = getDefaultView(value); return !!window && value instanceof window.Node; }; /** * Check if a value is a DOM selection. */ var isDOMSelection = function isDOMSelection(value) { var window = value && value.anchorNode && getDefaultView(value.anchorNode); return !!window && value instanceof window.Selection; }; /** * Check if a DOM node is an element node. */ var isDOMText = function isDOMText(value) { return isDOMNode(value) && value.nodeType === 3; }; /** * Checks whether a paste event is a plaintext-only event. */ var isPlainTextOnlyPaste = function isPlainTextOnlyPaste(event) { return event.clipboardData && event.clipboardData.getData('text/plain') !== '' && event.clipboardData.types.length === 1; }; /** * Normalize a DOM point so that it always refers to a text node. */ var normalizeDOMPoint = function normalizeDOMPoint(domPoint) { var _domPoint = _slicedToArray(domPoint, 2), node = _domPoint[0], offset = _domPoint[1]; // If it's an element node, its offset refers to the index of its children // including comment nodes, so try to find the right text child node. if (isDOMElement(node) && node.childNodes.length) { var isLast = offset === node.childNodes.length; var index = isLast ? offset - 1 : offset; var _getEditableChildAndI = getEditableChildAndIndex(node, index, isLast ? 'backward' : 'forward'); var _getEditableChildAndI2 = _slicedToArray(_getEditableChildAndI, 2); node = _getEditableChildAndI2[0]; index = _getEditableChildAndI2[1]; // If the editable child found is in front of input offset, we instead seek to its end isLast = index < offset; // If the node has children, traverse until we have a leaf node. Leaf nodes // can be either text nodes, or other void DOM nodes. while (isDOMElement(node) && node.childNodes.length) { var i = isLast ? node.childNodes.length - 1 : 0; node = getEditableChild(node, i, isLast ? 'backward' : 'forward'); } // Determine the new offset inside the text node. offset = isLast && node.textContent != null ? node.textContent.length : 0; } // Return the node and offset. return [node, offset]; }; /** * Determines wether the active element is nested within a shadowRoot */ var hasShadowRoot = function hasShadowRoot() { return !!(window.document.activeElement && window.document.activeElement.shadowRoot); }; /** * Get the nearest editable child and index at `index` in a `parent`, preferring * `direction`. */ var getEditableChildAndIndex = function getEditableChildAndIndex(parent, index, direction) { var childNodes = parent.childNodes; var child = childNodes[index]; var i = index; var triedForward = false; var triedBackward = false; // While the child is a comment node, or an element node with no children, // keep iterating to find a sibling non-void, non-comment node. while (isDOMComment(child) || isDOMElement(child) && child.childNodes.length === 0 || isDOMElement(child) && child.getAttribute('contenteditable') === 'false') { if (triedForward && triedBackward) { break; } if (i >= childNodes.length) { triedForward = true; i = index - 1; direction = 'backward'; continue; } if (i < 0) { triedBackward = true; i = index + 1; direction = 'forward'; continue; } child = childNodes[i]; index = i; i += direction === 'forward' ? 1 : -1; } return [child, index]; }; /** * Get the nearest editable child at `index` in a `parent`, preferring * `direction`. */ var getEditableChild = function getEditableChild(parent, index, direction) { var _getEditableChildAndI3 = getEditableChildAndIndex(parent, index, direction), _getEditableChildAndI4 = _slicedToArray(_getEditableChildAndI3, 1), child = _getEditableChildAndI4[0]; return child; }; /** * Get a plaintext representation of the content of a node, accounting for block * elements which get a newline appended. * * The domNode must be attached to the DOM. */ var getPlainText = function getPlainText(domNode) { var text = ''; if (isDOMText(domNode) && domNode.nodeValue) { return domNode.nodeValue; } if (isDOMElement(domNode)) { for (var _i = 0, _Array$from = Array.from(domNode.childNodes); _i < _Array$from.length; _i++) { var childNode = _Array$from[_i]; text += getPlainText(childNode); } var display = getComputedStyle(domNode).getPropertyValue('display'); if (display === 'block' || display === 'list' || domNode.tagName === 'BR') { text += '\n'; } } return text; }; function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } /** * Editable. */ var Editable = function Editable(props) { var autoFocus = props.autoFocus, _props$decorate = props.decorate, decorate = _props$decorate === void 0 ? defaultDecorate : _props$decorate, propsOnDOMBeforeInput = props.onDOMBeforeInput, placeholder = props.placeholder, _props$readOnly = props.readOnly, readOnly = _props$readOnly === void 0 ? false : _props$readOnly, renderElement = props.renderElement, renderLeaf = props.renderLeaf, _props$renderPlacehol = props.renderPlaceholder, renderPlaceholder = _props$renderPlacehol === void 0 ? function (props) { return /*#__PURE__*/React__default['default'].createElement(DefaultPlaceholder, Object.assign({}, props)); } : _props$renderPlacehol, _props$style = props.style, style = _props$style === void 0 ? {} : _props$style, _props$as = props.as, Component = _props$as === void 0 ? 'div' : _props$as, attributes = _objectWithoutProperties(props, ["autoFocus", "decorate", "onDOMBeforeInput", "placeholder", "readOnly", "renderElement", "renderLeaf", "renderPlaceholder", "style", "as"]); var editor = useSlate(); var ref = React.useRef(null); // Update internal state on each render. IS_READ_ONLY.set(editor, readOnly); // Keep track of some state for the event handler logic. var state = React.useMemo(function () { return { isComposing: false, isDraggingInternally: false, isUpdatingSelection: false, latestElement: null }; }, []); // Whenever the editor updates... useIsomorphicLayoutEffect(function () { // Update element-related weak maps with the DOM element ref. var window; if (ref.current && (window = getDefaultView(ref.current))) { EDITOR_TO_WINDOW.set(editor, window); EDITOR_TO_ELEMENT.set(editor, ref.current); NODE_TO_ELEMENT.set(editor, ref.current); ELEMENT_TO_NODE.set(ref.current, editor); } else { NODE_TO_ELEMENT["delete"](editor); } // Make sure the DOM selection state is in sync. var selection = editor.selection; var root = ReactEditor.findDocumentOrShadowRoot(editor); var domSelection = root.getSelection(); if (state.isComposing || !domSelection || !ReactEditor.isFocused(editor)) { return; } var hasDomSelection = domSelection.type !== 'None'; // If the DOM selection is properly unset, we're done. if (!selection && !hasDomSelection) { return; } // verify that the dom selection is in the editor var editorElement = EDITOR_TO_ELEMENT.get(editor); var hasDomSelectionInEditor = false; if (editorElement.contains(domSelection.anchorNode) && editorElement.contains(domSelection.focusNode)) { hasDomSelectionInEditor = true; } // If the DOM selection is in the editor and the editor selection is already correct, we're done. if (hasDomSelection && hasDomSelectionInEditor && selection) { var slateRange = ReactEditor.toSlateRange(editor, domSelection, { exactMatch: true }); if (slateRange && slate.Range.equals(slateRange, selection)) { return; } } // when <Editable/> is being controlled through external value // then its children might just change - DOM responds to it on its own // but Slate's value is not being updated through any operation // and thus it doesn't transform selection on its own if (selection && !ReactEditor.hasRange(editor, selection)) { editor.selection = ReactEditor.toSlateRange(editor, domSelection, { exactMatch: false }); return; } // Otherwise the DOM selection is out of sync, so update it. var el = ReactEditor.toDOMNode(editor, editor); state.isUpdatingSelection = true; var newDomRange = selection && ReactEditor.toDOMRange(editor, selection); if (newDomRange) { if (slate.Range.isBackward(selection)) { domSelection.setBaseAndExtent(newDomRange.endContainer, newDomRange.endOffset, newDomRange.startContainer, newDomRange.startOffset); } else { domSelection.setBaseAndExtent(newDomRange.startContainer, newDomRange.startOffset, newDomRange.endContainer, newDomRange.endOffset); } var leafEl = newDomRange.startContainer.parentElement; leafEl.getBoundingClientRect = newDomRange.getBoundingClientRect.bind(newDomRange); scrollIntoView__default['default'](leafEl, { scrollMode: 'if-needed', boundary: el }); // @ts-ignore delete leafEl.getBoundingClientRect; } else { domSelection.removeAllRanges(); } setTimeout(function () { // COMPAT: In Firefox, it's not enough to create a range, you also need // to focus the contenteditable element too. (2016/11/16) if (newDomRange && IS_FIREFOX) { el.focus(); } state.isUpdatingSelection = false; }); }); // The autoFocus TextareaHTMLAttribute doesn't do anything on a div, so it // needs to be manually focused. React.useEffect(function () { if (ref.current && autoFocus) { ref.current.focus(); } }, [autoFocus]); // Listen on the native `beforeinput` event to get real "Level 2" events. This // is required because React's `beforeinput` is fake and never really attaches // to the real event sadly. (2019/11/01) // https://github.com/facebook/react/issues/11211 var onDOMBeforeInput = React.useCallback(function (event) { if (!readOnly && hasEditableTarget(editor, event.target) && !isDOMEventHandled(event, propsOnDOMBeforeInput)) { var selection = editor.selection; var type = event.inputType; var data = event.dataTransfer || event.data || undefined; // These two types occur while a user is composing text and can't be // cancelled. Let them through and wait for the composition to end. if (type === 'insertCompositionText' || type === 'deleteCompositionText') { return; } event.preventDefault(); // COMPAT: For the deleting forward/backward input types we don't want // to change the selection because it is the range that will be deleted, // and those commands determine that for themselves. if (!type.startsWith('delete') || type.startsWith('deleteBy')) { var _event$getTargetRange = event.getTargetRanges(), _event$getTargetRange2 = _slicedToArray(_event$getTargetRange, 1), targetRange = _event$getTargetRange2[0]; if (targetRange) { var range = ReactEditor.toSlateRange(editor, targetRange, { exactMatch: false }); if (!selection || !slate.Range.equals(selection, range)) { slate.Transforms.select(editor, range); } } } // COMPAT: If the selection is expanded, even if the command seems like // a delete forward/backward command it should delete the selection. if (selection && slate.Range.isExpanded(selection) && type.startsWith('delete')) { var direction = type.endsWith('Backward') ? 'backward' : 'forward'; slate.Editor.deleteFragment(editor, { direction: direction }); return; } switch (type) { case 'deleteByComposition': case 'deleteByCut': case 'deleteByDrag': { slate.Editor.deleteFragment(editor); break; } case 'deleteContent': case 'deleteContentForward': { slate.Editor.deleteForward(editor); break; } case 'deleteContentBackward': { slate.Editor.deleteBackward(editor); break; } case 'deleteEntireSoftLine': { slate.Editor.deleteBackward(editor, { unit: 'line' }); slate.Editor.deleteForward(editor, { unit: 'line' }); break; } case 'deleteHardLineBackward': { slate.Editor.deleteBackward(editor, { unit: 'block' }); break; } case 'deleteSoftLineBackward': { slate.Editor.deleteBackward(editor, { unit: 'line' }); break; } case 'deleteHardLineForward': { slate.Editor.deleteForward(editor, { unit: 'block' }); break; } case 'deleteSoftLineForward': { slate.Editor.deleteForward(editor, { unit: 'line' }); break; } case 'deleteWordBackward': { slate.Editor.deleteBackward(editor, { unit: 'word' }); break; } case 'deleteWordForward': { slate.Editor.deleteForward(editor, { unit: 'word' }); break; } case 'insertLineBreak': case 'insertParagraph': { slate.Editor.insertBreak(editor); break; } case 'insertFromComposition': case 'insertFromDrop': case 'insertFromPaste': case 'insertFromYank': case 'insertReplacementText': case 'insertText': { if (type === 'insertFromComposition') { // COMPAT: in Safari, `compositionend` is dispatched after the // `beforeinput` for "insertFromComposition". But if we wait for it // then we will abort because we're still composing and the selection // won't be updated properly. // https://www.w3.org/TR/input-events-2/ state.isComposing = false; } var window = ReactEditor.getWindow(editor); if (data instanceof window.DataTransfer) { ReactEditor.insertData(editor, data); } else if (typeof data === 'string') { slate.Editor.insertText(editor, data); } break; } } } }, [readOnly, propsOnDOMBeforeInput]); // Attach a native DOM event handler for `beforeinput` events, because React's // built-in `onBeforeInput` is actually a leaky polyfill that doesn't expose // real `beforeinput` events sadly... (2019/11/04) // https://github.com/facebook/react/issues/11211 useIsomorphicLayoutEffect(function () { if (ref.current && HAS_BEFORE_INPUT_SUPPORT) { // @ts-ignore The `beforeinput` event isn't recognized. ref.current.addEventListener('beforeinput', onDOMBeforeInput); } return function () { if (ref.current && HAS_BEFORE_INPUT_SUPPORT) { // @ts-ignore The `beforeinput` event isn't recognized. ref.current.removeEventListener('beforeinput', onDOMBeforeInput); } }; }, [onDOMBeforeInput]); // Listen on the native `selectionchange` event to be able to update any time // the selection changes. This is required because React's `onSelect` is leaky // and non-standard so it doesn't fire until after a selection has been // released. This causes issues in situations where another change happens // while a selection is being dragged. var onDOMSelectionChange = React.useCallback(throttle__default['default'](function () { if (!readOnly && !state.isComposing && !state.isUpdatingSelection && !state.isDraggingInternally) { var root = ReactEditor.findDocumentOrShadowRoot(editor); var activeElement = root.activeElement; var el = ReactEditor.toDOMNode(editor, editor); var domSelection = root.getSelection(); if (activeElement === el) { state.latestElement = activeElement; IS_FOCUSED.set(editor, true); } else { IS_FOCUSED["delete"](editor); } if (!domSelection) { return slate.Transforms.deselect(editor); } var anchorNode = domSelection.anchorNode, focusNode = domSelection.focusNode; var anchorNodeSelectable = hasEditableTarget(editor, anchorNode) || isTargetInsideVoid(editor, anchorNode); var focusNodeSelectable = hasEditableTarget(editor, focusNode) || isTargetInsideVoid(editor, focusNode); if (anchorNodeSelectable && focusNodeSelectable) { var range = ReactEditor.toSlateRange(editor, domSelection, { exactMatch: false }); slate.Transforms.select(editor, range); } else { slate.Transforms.deselect(editor); } } }, 100), [readOnly]); // Attach a native DOM event handler for `selectionchange`, because React's // built-in `onSelect` handler doesn't fire for all selection changes. It's a // leaky polyfill that only fires on keypresses or clicks. Instead, we want to // fire for any change to the selection inside the editor. (2019/11/04) // https://github.com/facebook/react/issues/5785 useIsomorphicLayoutEffect(function () { var window = ReactEditor.getWindow(editor); window.document.addEventListener('selectionchange', onDOMSelectionChange); return function () { window.document.removeEventListener('selectionchange', onDOMSelectionChange); }; }, [onDOMSelectionChange]); var decorations = decorate([editor, []]); if (placeholder && editor.children.length === 1 && Array.from(slate.Node.texts(editor)).length === 1 && slate.Node.string(editor) === '') { var _decorations$push; var start = slate.Editor.start(editor, []); decorations.push((_decorations$push = {}, _definePro