slate-react
Version:
Tools for building completely customizable richtext editors with React.
1,438 lines (1,196 loc) • 143 kB
JavaScript
'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