slate-dom
Version:
Tools for building completely customizable richtext editors with React.
1,151 lines (1,109 loc) • 104 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('slate')) :
typeof define === 'function' && define.amd ? define(['exports', 'slate'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.SlateDom = {}, global.Slate));
})(this, (function (exports, slate) { 'use strict';
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 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.__esModule = true, module.exports["default"] = module.exports;
});
unwrapExports(arrayLikeToArray);
var arrayWithoutHoles = createCommonjsModule(function (module) {
function _arrayWithoutHoles(arr) {
if (Array.isArray(arr)) return arrayLikeToArray(arr);
}
module.exports = _arrayWithoutHoles, module.exports.__esModule = true, module.exports["default"] = module.exports;
});
unwrapExports(arrayWithoutHoles);
var iterableToArray = createCommonjsModule(function (module) {
function _iterableToArray(iter) {
if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter);
}
module.exports = _iterableToArray, module.exports.__esModule = true, module.exports["default"] = module.exports;
});
unwrapExports(iterableToArray);
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.__esModule = true, module.exports["default"] = module.exports;
});
unwrapExports(unsupportedIterableToArray);
var nonIterableSpread = createCommonjsModule(function (module) {
function _nonIterableSpread() {
throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}
module.exports = _nonIterableSpread, module.exports.__esModule = true, module.exports["default"] = module.exports;
});
unwrapExports(nonIterableSpread);
var toConsumableArray = createCommonjsModule(function (module) {
function _toConsumableArray(arr) {
return arrayWithoutHoles(arr) || iterableToArray(arr) || unsupportedIterableToArray(arr) || nonIterableSpread();
}
module.exports = _toConsumableArray, module.exports.__esModule = true, module.exports["default"] = module.exports;
});
var _toConsumableArray = unwrapExports(toConsumableArray);
var arrayWithHoles = createCommonjsModule(function (module) {
function _arrayWithHoles(arr) {
if (Array.isArray(arr)) return arr;
}
module.exports = _arrayWithHoles, module.exports.__esModule = true, module.exports["default"] = module.exports;
});
unwrapExports(arrayWithHoles);
var iterableToArrayLimit = createCommonjsModule(function (module) {
function _iterableToArrayLimit(r, l) {
var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"];
if (null != t) {
var e,
n,
i,
u,
a = [],
f = true,
o = false;
try {
if (i = (t = t.call(r)).next, 0 === l) {
if (Object(t) !== t) return;
f = !1;
} else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0);
} catch (r) {
o = true, n = r;
} finally {
try {
if (!f && null != t["return"] && (u = t["return"](), Object(u) !== u)) return;
} finally {
if (o) throw n;
}
}
return a;
}
}
module.exports = _iterableToArrayLimit, module.exports.__esModule = true, module.exports["default"] = module.exports;
});
unwrapExports(iterableToArrayLimit);
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.__esModule = true, module.exports["default"] = module.exports;
});
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.__esModule = true, module.exports["default"] = module.exports;
});
var _slicedToArray = unwrapExports(slicedToArray);
function _createForOfIteratorHelper$2(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray$2(o)) || allowArrayLike) { 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 = it.call(o); }, 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$2(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray$2(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$2(o, minLen); }
function _arrayLikeToArray$2(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; }
/**
* Types.
*/
// COMPAT: This is required to prevent TypeScript aliases from doing some very
// weird things for Slate's types with the same name as globals. (2019/11/27)
// https://github.com/microsoft/TypeScript/issues/35002
var DOMNode = globalThis.Node;
var DOMElement = globalThis.Element;
var DOMText = globalThis.Text;
var DOMRange = globalThis.Range;
var DOMSelection = globalThis.Selection;
var DOMStaticRange = globalThis.StaticRange;
/**
* 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;
// If the editable child found is in front of input offset, we instead seek to its end
var _getEditableChildAndI = getEditableChildAndIndex(node, index, isLast ? 'backward' : 'forward');
var _getEditableChildAndI2 = _slicedToArray(_getEditableChildAndI, 2);
node = _getEditableChildAndI2[0];
index = _getEditableChildAndI2[1];
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 whether the active element is nested within a shadowRoot
*/
var hasShadowRoot = function hasShadowRoot(node) {
var parent = node && node.parentNode;
while (parent) {
if (parent.toString() === '[object ShadowRoot]') {
return true;
}
parent = parent.parentNode;
}
return false;
};
/**
* Get the nearest editable child and index at `index` in a `parent`, preferring
* `direction`.
*/
var getEditableChildAndIndex = function getEditableChildAndIndex(parent, index, direction) {
if (typeof index !== 'number') {
throw new Error('Expected index to be a number');
}
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;
};
/**
* Get x-slate-fragment attribute from data-slate-fragment
*/
var catchSlateFragment = /data-slate-fragment="(.+?)"/m;
var getSlateFragmentAttribute = function getSlateFragmentAttribute(dataTransfer) {
var htmlData = dataTransfer.getData('text/html');
var _ref = htmlData.match(catchSlateFragment) || [],
_ref2 = _slicedToArray(_ref, 2),
fragment = _ref2[1];
return fragment;
};
/**
* Get the dom selection from Shadow Root if possible, otherwise from the document
*/
var getSelection = function getSelection(root) {
if (root.getSelection != null) {
return root.getSelection();
}
return document.getSelection();
};
/**
* Check whether a mutation originates from a editable element inside the editor.
*/
var isTrackedMutation = function isTrackedMutation(editor, mutation, batch) {
var target = mutation.target;
if (isDOMElement(target) && target.matches('[contentEditable="false"]')) {
return false;
}
var _DOMEditor$getWindow = DOMEditor.getWindow(editor),
document = _DOMEditor$getWindow.document;
if (containsShadowAware(document, target)) {
return DOMEditor.hasDOMNode(editor, target, {
editable: true
});
}
var parentMutation = batch.find(function (_ref3) {
var addedNodes = _ref3.addedNodes,
removedNodes = _ref3.removedNodes;
var _iterator = _createForOfIteratorHelper$2(addedNodes),
_step;
try {
for (_iterator.s(); !(_step = _iterator.n()).done;) {
var node = _step.value;
if (node === target || containsShadowAware(node, target)) {
return true;
}
}
} catch (err) {
_iterator.e(err);
} finally {
_iterator.f();
}
var _iterator2 = _createForOfIteratorHelper$2(removedNodes),
_step2;
try {
for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
var _node = _step2.value;
if (_node === target || containsShadowAware(_node, target)) {
return true;
}
}
} catch (err) {
_iterator2.e(err);
} finally {
_iterator2.f();
}
});
if (!parentMutation || parentMutation === mutation) {
return false;
}
// Target add/remove is tracked. Track the mutation if we track the parent mutation.
return isTrackedMutation(editor, parentMutation, batch);
};
/**
* Retrieves the deepest active element in the DOM, considering nested shadow DOMs.
*/
var getActiveElement = function getActiveElement() {
var activeElement = document.activeElement;
while ((_activeElement = activeElement) !== null && _activeElement !== void 0 && _activeElement.shadowRoot && (_activeElement$shadow = activeElement.shadowRoot) !== null && _activeElement$shadow !== void 0 && _activeElement$shadow.activeElement) {
var _activeElement, _activeElement$shadow, _activeElement2;
activeElement = (_activeElement2 = activeElement) === null || _activeElement2 === void 0 || (_activeElement2 = _activeElement2.shadowRoot) === null || _activeElement2 === void 0 ? void 0 : _activeElement2.activeElement;
}
return activeElement;
};
/**
* @returns `true` if `otherNode` is before `node` in the document; otherwise, `false`.
*/
var isBefore = function isBefore(node, otherNode) {
return Boolean(node.compareDocumentPosition(otherNode) & DOMNode.DOCUMENT_POSITION_PRECEDING);
};
/**
* @returns `true` if `otherNode` is after `node` in the document; otherwise, `false`.
*/
var isAfter = function isAfter(node, otherNode) {
return Boolean(node.compareDocumentPosition(otherNode) & DOMNode.DOCUMENT_POSITION_FOLLOWING);
};
/**
* Shadow DOM-aware version of Element.closest()
* Traverses up the DOM tree, crossing shadow DOM boundaries
*/
var closestShadowAware = function closestShadowAware(element, selector) {
if (!element) {
return null;
}
var current = element;
while (current) {
if (current.matches && current.matches(selector)) {
return current;
}
if (current.parentElement) {
current = current.parentElement;
} else if (current.parentNode && 'host' in current.parentNode) {
current = current.parentNode.host;
} else {
return null;
}
}
return null;
};
/**
* Shadow DOM-aware version of Node.contains()
* Checks if a node contains another node, crossing shadow DOM boundaries
*/
var containsShadowAware = function containsShadowAware(parent, child) {
if (!parent || !child) {
return false;
}
if (parent.contains(child)) {
return true;
}
var current = child;
while (current) {
if (current === parent) {
return true;
}
if (current.parentNode) {
if ('host' in current.parentNode) {
current = current.parentNode.host;
} else {
current = current.parentNode;
}
} else {
return false;
}
}
return false;
};
var _navigator$userAgent$, _navigator$userAgent$2;
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_WEBKIT = typeof navigator !== 'undefined' && /AppleWebKit(?!.*Chrome)/i.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);
var IS_ANDROID_CHROME_LEGACY = IS_ANDROID && typeof navigator !== 'undefined' && /Chrome?\/(?:[0-5]?\d)(?:\.)/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);
// UC mobile browser
var IS_UC_MOBILE = typeof navigator !== 'undefined' && /.*UCBrowser/.test(navigator.userAgent);
// Wechat browser (not including mac wechat)
var IS_WECHATBROWSER = typeof navigator !== 'undefined' && /.*Wechat/.test(navigator.userAgent) && !/.*MacWechat/.test(navigator.userAgent) && (
// avoid lookbehind (buggy in safari < 16.4)
!IS_CHROME || IS_CHROME_LEGACY); // wechat and low chrome is real wechat
// 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');
// Check if the browser is Safari and older than 17
typeof navigator !== 'undefined' && /Safari/.test(navigator.userAgent) && /Version\/(\d+)/.test(navigator.userAgent) && ((_navigator$userAgent$ = navigator.userAgent.match(/Version\/(\d+)/)) !== null && _navigator$userAgent$ !== void 0 && _navigator$userAgent$[1] ? parseInt((_navigator$userAgent$2 = navigator.userAgent.match(/Version\/(\d+)/)) === null || _navigator$userAgent$2 === void 0 ? void 0 : _navigator$userAgent$2[1], 10) < 17 : false);
// 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_ANDROID_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';
var _typeof_1 = createCommonjsModule(function (module) {
function _typeof(o) {
"@babel/helpers - typeof";
return (module.exports = _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;
}, module.exports.__esModule = true, module.exports["default"] = module.exports), _typeof(o);
}
module.exports = _typeof, module.exports.__esModule = true, module.exports["default"] = module.exports;
});
unwrapExports(_typeof_1);
var toPrimitive = createCommonjsModule(function (module) {
var _typeof = _typeof_1["default"];
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);
}
module.exports = _toPrimitive, module.exports.__esModule = true, module.exports["default"] = module.exports;
});
unwrapExports(toPrimitive);
var toPropertyKey = createCommonjsModule(function (module) {
var _typeof = _typeof_1["default"];
function _toPropertyKey(arg) {
var key = toPrimitive(arg, "string");
return _typeof(key) === "symbol" ? key : String(key);
}
module.exports = _toPropertyKey, module.exports.__esModule = true, module.exports["default"] = module.exports;
});
unwrapExports(toPropertyKey);
var createClass = createCommonjsModule(function (module) {
function _defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor) descriptor.writable = true;
Object.defineProperty(target, toPropertyKey(descriptor.key), descriptor);
}
}
function _createClass(Constructor, protoProps, staticProps) {
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
if (staticProps) _defineProperties(Constructor, staticProps);
Object.defineProperty(Constructor, "prototype", {
writable: false
});
return Constructor;
}
module.exports = _createClass, module.exports.__esModule = true, module.exports["default"] = module.exports;
});
var _createClass = unwrapExports(createClass);
var classCallCheck = createCommonjsModule(function (module) {
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
module.exports = _classCallCheck, module.exports.__esModule = true, module.exports["default"] = module.exports;
});
var _classCallCheck = unwrapExports(classCallCheck);
var defineProperty = createCommonjsModule(function (module) {
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;
}
module.exports = _defineProperty, module.exports.__esModule = true, module.exports["default"] = module.exports;
});
var _defineProperty = unwrapExports(defineProperty);
/**
* An auto-incrementing identifier for keys.
*/
var n = 0;
/**
* A class that keeps track of a key string. We use a full class here because we
* want to be able to use them as keys in `WeakMap` objects.
*/
var Key = /*#__PURE__*/_createClass(function Key() {
_classCallCheck(this, Key);
_defineProperty(this, "id", void 0);
this.id = "".concat(n++);
});
/**
* 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 IS_NODE_MAP_DIRTY = new WeakMap();
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 EDITOR_TO_PLACEHOLDER_ELEMENT = new WeakMap();
var ELEMENT_TO_NODE = new WeakMap();
var NODE_TO_ELEMENT = new WeakMap();
var NODE_TO_KEY = new WeakMap();
var EDITOR_TO_KEY_TO_ELEMENT = new WeakMap();
/**
* Weak maps for storing editor-related state.
*/
var IS_READ_ONLY = new WeakMap();
var IS_FOCUSED = new WeakMap();
var IS_COMPOSING = new WeakMap();
var EDITOR_TO_USER_SELECTION = new WeakMap();
/**
* Weak map for associating the context `onChange` context with the plugin.
*/
var EDITOR_TO_ON_CHANGE = new WeakMap();
/**
* Weak maps for saving pending state on composition stage.
*/
var EDITOR_TO_SCHEDULE_FLUSH = new WeakMap();
var EDITOR_TO_PENDING_INSERTION_MARKS = new WeakMap();
var EDITOR_TO_USER_MARKS = new WeakMap();
/**
* Android input handling specific weak-maps
*/
var EDITOR_TO_PENDING_DIFFS = new WeakMap();
var EDITOR_TO_PENDING_ACTION = new WeakMap();
var EDITOR_TO_PENDING_SELECTION = new WeakMap();
var EDITOR_TO_FORCE_RENDER = new WeakMap();
/**
* Symbols.
*/
var PLACEHOLDER_SYMBOL = Symbol('placeholder');
var MARK_PLACEHOLDER_SYMBOL = Symbol('mark-placeholder');
// eslint-disable-next-line no-redeclare
var DOMEditor = {
androidPendingDiffs: function androidPendingDiffs(editor) {
return EDITOR_TO_PENDING_DIFFS.get(editor);
},
androidScheduleFlush: function androidScheduleFlush(editor) {
var _EDITOR_TO_SCHEDULE_F;
(_EDITOR_TO_SCHEDULE_F = EDITOR_TO_SCHEDULE_FLUSH.get(editor)) === null || _EDITOR_TO_SCHEDULE_F === void 0 || _EDITOR_TO_SCHEDULE_F();
},
blur: function blur(editor) {
var el = DOMEditor.toDOMNode(editor, editor);
var root = DOMEditor.findDocumentOrShadowRoot(editor);
IS_FOCUSED.set(editor, false);
if (root.activeElement === el) {
el.blur();
}
},
deselect: function deselect(editor) {
var selection = editor.selection;
var root = DOMEditor.findDocumentOrShadowRoot(editor);
var domSelection = getSelection(root);
if (domSelection && domSelection.rangeCount > 0) {
domSelection.removeAllRanges();
}
if (selection) {
slate.Transforms.deselect(editor);
}
},
findDocumentOrShadowRoot: function findDocumentOrShadowRoot(editor) {
var el = DOMEditor.toDOMNode(editor, editor);
var root = el.getRootNode();
if (root instanceof Document || root instanceof ShadowRoot) {
return root;
}
return el.ownerDocument;
},
findEventRange: function findEventRange(editor, event) {
if ('nativeEvent' in event) {
event = event.nativeEvent;
}
var _event = event,
x = _event.clientX,
y = _event.clientY,
target = _event.target;
if (x == null || y == null) {
throw new Error("Cannot resolve a Slate range from a DOM event: ".concat(event));
}
var node = DOMEditor.toSlateNode(editor, event.target);
var path = DOMEditor.findPath(editor, node);
// If the drop target is inside a void node, move it into either the
// next or previous node, depending on which side the `x` and `y`
// coordinates are closest to.
if (slate.Node.isElement(node) && slate.Editor.isVoid(editor, node)) {
var rect = target.getBoundingClientRect();
var isPrev = editor.isInline(node) ? x - rect.left < rect.left + rect.width - x : y - rect.top < rect.top + rect.height - y;
var edge = slate.Editor.point(editor, path, {
edge: isPrev ? 'start' : 'end'
});
var point = isPrev ? slate.Editor.before(editor, edge) : slate.Editor.after(editor, edge);
if (point) {
var _range = slate.Editor.range(editor, point);
return _range;
}
}
// Else resolve a range from the caret position where the drop occured.
var domRange;
var _DOMEditor$getWindow = DOMEditor.getWindow(editor),
document = _DOMEditor$getWindow.document;
// COMPAT: In Firefox, `caretRangeFromPoint` doesn't exist. (2016/07/25)
if (document.caretRangeFromPoint) {
domRange = document.caretRangeFromPoint(x, y);
} else {
var position = document.caretPositionFromPoint(x, y);
if (position) {
domRange = document.createRange();
domRange.setStart(position.offsetNode, position.offset);
domRange.setEnd(position.offsetNode, position.offset);
}
}
if (!domRange) {
throw new Error("Cannot resolve a Slate range from a DOM event: ".concat(event));
}
// Resolve a Slate range from the DOM range.
var range = DOMEditor.toSlateRange(editor, domRange, {
exactMatch: false,
suppressThrow: false
});
return range;
},
findKey: function findKey(editor, node) {
var key = NODE_TO_KEY.get(node);
if (!key) {
key = new Key();
NODE_TO_KEY.set(node, key);
}
return key;
},
findPath: function findPath(editor, node) {
var path = [];
var child = node;
while (true) {
var parent = NODE_TO_PARENT.get(child);
if (parent == null) {
if (child === editor) {
return path;
} else {
break;
}
}
var i = NODE_TO_INDEX.get(child);
if (i == null) {
break;
}
path.unshift(i);
child = parent;
}
throw new Error("Unable to find the path for Slate node: ".concat(slate.Scrubber.stringify(node)));
},
focus: function focus(editor) {
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {
retries: 5
};
// Return if already focused
if (IS_FOCUSED.get(editor)) {
return;
}
// Return if no dom node is associated with the editor, which means the editor is not yet mounted
// or has been unmounted. This can happen especially, while retrying to focus the editor.
if (!EDITOR_TO_ELEMENT.get(editor)) {
return;
}
// Retry setting focus if the editor has pending operations.
// The DOM (selection) is unstable while changes are applied.
// Retry until retries are exhausted or editor is focused.
if (options.retries <= 0) {
throw new Error('Could not set focus, editor seems stuck with pending operations');
}
if (editor.operations.length > 0) {
setTimeout(function () {
DOMEditor.focus(editor, {
retries: options.retries - 1
});
}, 10);
return;
}
var el = DOMEditor.toDOMNode(editor, editor);
var root = DOMEditor.findDocumentOrShadowRoot(editor);
if (root.activeElement !== el) {
// Ensure that the DOM selection state is set to the editor's selection
if (editor.selection && root instanceof Document) {
var domSelection = getSelection(root);
var domRange = DOMEditor.toDOMRange(editor, editor.selection);
domSelection === null || domSelection === void 0 || domSelection.removeAllRanges();
domSelection === null || domSelection === void 0 || domSelection.addRange(domRange);
}
// Create a new selection in the top of the document if missing
if (!editor.selection) {
slate.Transforms.select(editor, slate.Editor.start(editor, []));
}
// IS_FOCUSED should be set before calling el.focus() to ensure that
// FocusedContext is updated to the correct value
IS_FOCUSED.set(editor, true);
el.focus({
preventScroll: true
});
}
},
getWindow: function getWindow(editor) {
var window = EDITOR_TO_WINDOW.get(editor);
if (!window) {
throw new Error('Unable to find a host window element for this editor');
}
return window;
},
hasDOMNode: function hasDOMNode(editor, target) {
var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
var _options$editable = options.editable,
editable = _options$editable === void 0 ? false : _options$editable;
var editorEl = DOMEditor.toDOMNode(editor, editor);
var targetEl;
// COMPAT: In Firefox, reading `target.nodeType` will throw an error if
// target is originating from an internal "restricted" element (e.g. a
// stepper arrow on a number input). (2018/05/04)
// https://github.com/ianstormtaylor/slate/issues/1819
try {
targetEl = isDOMElement(target) ? target : target.parentElement;
} catch (err) {
if (err instanceof Error && !err.message.includes('Permission denied to access property "nodeType"')) {
throw err;
}
}
if (!targetEl) {
return false;
}
return closestShadowAware(targetEl, "[data-slate-editor]") === editorEl && (!editable || targetEl.isContentEditable ? true : typeof targetEl.isContentEditable === 'boolean' &&
// isContentEditable exists only on HTMLElement, and on other nodes it will be undefined
// this is the core logic that lets you know you got the right editor.selection instead of null when editor is contenteditable="false"(readOnly)
closestShadowAware(targetEl, '[contenteditable="false"]') === editorEl || !!targetEl.getAttribute('data-slate-zero-width'));
},
hasEditableTarget: function hasEditableTarget(editor, target) {
return isDOMNode(target) && DOMEditor.hasDOMNode(editor, target, {
editable: true
});
},
hasRange: function hasRange(editor, range) {
var anchor = range.anchor,
focus = range.focus;
return slate.Editor.hasPath(editor, anchor.path) && slate.Editor.hasPath(editor, focus.path);
},
hasSelectableTarget: function hasSelectableTarget(editor, target) {
return DOMEditor.hasEditableTarget(editor, target) || DOMEditor.isTargetInsideNonReadonlyVoid(editor, target);
},
hasTarget: function hasTarget(editor, target) {
return isDOMNode(target) && DOMEditor.hasDOMNode(editor, target);
},
insertData: function insertData(editor, data) {
editor.insertData(data);
},
insertFragmentData: function insertFragmentData(editor, data) {
return editor.insertFragmentData(data);
},
insertTextData: function insertTextData(editor, data) {
return editor.insertTextData(data);
},
isComposing: function isComposing(editor) {
return !!IS_COMPOSING.get(editor);
},
isFocused: function isFocused(editor) {
return !!IS_FOCUSED.get(editor);
},
isReadOnly: function isReadOnly(editor) {
return !!IS_READ_ONLY.get(editor);
},
isTargetInsideNonReadonlyVoid: function isTargetInsideNonReadonlyVoid(editor, target) {
if (IS_READ_ONLY.get(editor)) return false;
if (!DOMEditor.hasTarget(editor, target)) return false;
var slateNode = DOMEditor.toSlateNode(editor, target);
return slate.Node.isElement(slateNode) && slate.Editor.isVoid(editor, slateNode);
},
setFragmentData: function setFragmentData(editor, data, originEvent) {
return editor.setFragmentData(data, originEvent);
},
toDOMNode: function toDOMNode(editor, node) {
var _EDITOR_TO_KEY_TO_ELE;
var domNode = node === editor ? EDITOR_TO_ELEMENT.get(editor) : (_EDITOR_TO_KEY_TO_ELE = EDITOR_TO_KEY_TO_ELEMENT.get(editor)) === null || _EDITOR_TO_KEY_TO_ELE === void 0 ? void 0 : _EDITOR_TO_KEY_TO_ELE.get(DOMEditor.findKey(editor, node));
if (!domNode) {
throw new Error("Cannot resolve a DOM node from Slate node: ".concat(slate.Scrubber.stringify(node)));
}
return domNode;
},
toDOMPoint: function toDOMPoint(editor, point) {
var _Editor$node = slate.Editor.node(editor, point.path),
_Editor$node2 = _slicedToArray(_Editor$node, 1),
node = _Editor$node2[0];
var el = DOMEditor.toDOMNode(editor, node);
var domPoint;
// If we're inside a void node, force the offset to 0, otherwise the zero
// width spacing character will result in an incorrect offset of 1
if (slate.Editor["void"](editor, {
at: point
})) {
point = {
path: point.path,
offset: 0
};
}
// For each leaf, we need to isolate its content, which means filtering
// to its direct text and zero-width spans. (We have to filter out any
// other siblings that may have been rendered alongside them.)
var selector = "[data-slate-string], [data-slate-zero-width]";
var texts = Array.from(el.querySelectorAll(selector));
var start = 0;
for (var i = 0; i < texts.length; i++) {
var text = texts[i];
var domNode = text.childNodes[0];
if (domNode == null || domNode.textContent == null) {
continue;
}
var length = domNode.textContent.length;
var attr = text.getAttribute('data-slate-length');
var trueLength = attr == null ? length : parseInt(attr, 10);
var end = start + trueLength;
// Prefer putting the selection inside the mark placeholder to ensure
// composed text is displayed with the correct marks.
var nextText = texts[i + 1];
if (point.offset === end && nextText !== null && nextText !== void 0 && nextText.hasAttribute('data-slate-mark-placeholder')) {
var _nextText$textContent;
var domText = nextText.childNodes[0];
domPoint = [
// COMPAT: If we don't explicity set the dom point to be on the actual
// dom text element, chrome will put the selection behind the actual dom
// text element, causing domRange.getBoundingClientRect() calls on a collapsed
// selection to return incorrect zero values (https://bugs.chromium.org/p/chromium/issues/detail?id=435438)
// which will cause issues when scrolling to it.
domText instanceof DOMText ? domText : nextText, (_nextText$textContent = nextText.textContent) !== null && _nextText$textContent !== void 0 && _nextText$textContent.startsWith("\uFEFF") ? 1 : 0];
break;
}
if (point.offset <= end) {
var offset = Math.min(length, Math.max(0, point.offset - start));
domPoint = [domNode, offset];
break;
}
start = end;
}
if (!domPoint) {
throw new Error("Cannot resolve a DOM point from Slate point: ".concat(slate.Scrubber.stringify(point)));
}
return domPoint;
},
toDOMRange: function toDOMRange(editor, range) {
var anchor = range.anchor,
focus = range.focus;
var isBackward = slate.Range.isBackward(range);
var domAnchor = DOMEditor.toDOMPoint(editor, anchor);
var domFocus = slate.Range.isCollapsed(range) ? domAnchor : DOMEditor.toDOMPoint(editor, focus);
var window = DOMEditor.getWindow(editor);
var domRange = window.document.createRange();
var _ref = isBackward ? domFocus : domAnchor,
_ref2 = _slicedToArray(_ref, 2),
startNode = _ref2[0],
startOffset = _ref2[1];
var _ref3 = isBackward ? domAnchor : domFocus,
_ref4 = _slicedToArray(_ref3, 2),
endNode = _ref4[0],
endOffset = _ref4[1];
// A slate Point at zero-width Leaf always has an offset of 0 but a native DOM selection at
// zero-width node has an offset of 1 so we have to check if we are in a zero-width node and
// adjust the offset accordingly.
var startEl = isDOMElement(startNode) ? startNode : startNode.parentElement;
var isStartAtZeroWidth = !!startEl.getAttribute('data-slate-zero-width');
var endEl = isDOMElement(endNode) ? endNode : endNode.parentElement;
var isEndAtZeroWidth = !!endEl.getAttribute('data-slate-zero-width');
domRange.setStart(startNode, isStartAtZeroWidth ? 1 : startOffset);
domRange.setEnd(endNode, isEndAtZeroWidth ? 1 : endOffset);
return domRange;
},
toSlateNode: function toSlateNode(editor, domNode) {
var domEl = isDOMElement(domNode) ? domNode : domNode.parentElement;
if (domEl && !domEl.hasAttribute('data-slate-node')) {
domEl = domEl.closest("[data-slate-node]");
}
var node = domEl ? ELEMENT_TO_NODE.get(domEl) : null;
if (!node) {
throw new Error("Cannot resolve a Slate node from DOM node: ".concat(domEl));
}
return node;
},
toSlatePoint: function toSlatePoint(editor, domPoint, options) {
var exactMatch = options.exactMatch,
suppressThrow = options.suppressThrow;
var _ref5 = exactMatch ? domPoint : normalizeDOMPoint(domPoint),
_ref6 = _slicedToArray(_ref5, 2),
nearestNode = _ref6[0],
nearestOffset = _ref6[1];
var parentNode = nearestNode.parentNode;
var searchDirection = options.searchDirection;
var textNode = null;
var offset = 0;
if (parentNode) {
var _domNode$textContent, _domNode$textContent2;
var editorEl = DOMEditor.toDOMNode(editor, editor);
var potentialVoidNode = parentNode.closest('[data-slate-void="true"]');
// Need to ensure that the closest void node is actually a void node
// within this editor, and not a void node within some parent editor. This can happen
// if this editor is within a void node of another editor ("nested editors", like in
// the "Editable Voids" example on the docs site).
var voidNode = potentialVoidNode && containsShadowAware(editorEl, potentialVoidNode) ? potentialVoidNode : null;
var potentialNonEditableNode = parentNode.closest('[contenteditable="false"]');
var nonEditableNode = potentialNonEditableNode && containsShadowAware(editorEl, potentialNonEditableNode) ? potentialNonEditableNode : null;
var leafNode = parentNode.closest('[data-slate-leaf]');
var domNode = null;
// Calculate how far into the text node the `nearestNode` is, so that we
// can determine what the offset relative to the text node is.
if (leafNode) {
textNode = leafNode.closest('[data-slate-node="text"]');
if (textNode) {
var window = DOMEditor.getWindow(editor);
var range = window.document.createRange();
range.setStart(textNode, 0);
range.setEnd(nearestNode, nearestOffset);
var contents = range.cloneContents();
var removals = [].concat(_toConsumableArray(Array.prototype.slice.call(contents.querySelectorAll('[data-slate-zero-width]'))), _toConsumableArray(Array.prototype.slice.call(contents.querySelectorAll('[contenteditable=false]'))));
removals.forEach(function (el) {
// COMPAT: While composing at the start of a text node, some keyboards put
// the text content inside the zero width space.
if (IS_ANDROID && !exactMatch && el.hasAttribute('data-slate-zero-width') && el.textContent.length > 0 && el.textContext !== "\uFEFF") {
if (el.textContent.startsWith("\uFEFF")) {
el.textContent = el.textContent.slice(1);
}
return;
}
el.parentNode.removeChild(el);
});
// COMPAT: Edge has a bug where Range.prototype.toString() will
// convert \n into \r\n. The bug causes a loop when slate-dom
// attempts to reposition its cursor to match the native position. Use
// textContent.length instead.
// https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/10291116/
offset = contents.textContent.length;
domNode = textNode;
}
} else if (voidNode) {
// For void nodes, the element with the offset key will be a cousin, not an
// ancestor, so find it by going down from the nearest void parent and taking the
// first one that isn't inside a nested editor.
var leafNodes = voidNode.querySelectorAll('[data-slate-leaf]');
for (var index = 0; index < leafNodes.length; index++) {
var current = leafNodes[index];
if (DOMEditor.hasDOMNode(editor, current)) {
leafNode = current;
break;
}
}
// COMPAT: In read-only editors the leaf is not rendered.
if (!leafNode) {
offset = 1;
} else {
textNode = leafNode.closest('[data-slate-node="text"]');
domNode = leafNode;
offset = domNode.textContent.length;
domNode.querySelectorAll('[data-slate-zero-width]').forEach(function (el) {
offset -= el.textContent.length;
});
}
} else if (nonEditableNode) {
// Find the edge of the nearest leaf in `searchDirection`
var getLeafNodes = function getLeafNodes(node) {
return node ? node.querySelectorAll(
// Exclude leaf nodes in nested editors
'[data-slate-leaf]:not(:scope [data-slate-editor] [data-slate-leaf])') : [];
};
var elementNode = nonEditableNode.closest('[data-slate-node="element"]');
if (searchDirection === 'backward' || !searchDirection) {
var _leafNodes$findLast;
var _leafNodes = [].concat(_toConsumableArray(getLeafNodes(elementNode === null || elementNode === void 0 ? void 0 : elementNode.previousElementSibling)), _toConsumableArray(getLeafNodes(elementNode)));
leafNode = (_leafNodes$findLast = _leafNodes.findLast(function (leaf) {
return isBefore(nonEditableNode, leaf);
})) !== null && _leafNodes$findLast !== void 0 ? _leafNodes$findLast : null;
if (leafNode) {
searchDirection = 'backward';
}
}
if (searchDirection === 'forward' || !searchDirection) {
var _leafNodes2$find;
var _leafNodes2 = [].concat(_toConsumableArray(getLeafNodes(elementNode)), _toConsumableArray(getLeafNodes(elementNode === null || elementNode === void 0 ? void 0 : elementNode.nextElementSibling)));
leafNode = (_leafNodes2$find = _leafNodes2.find(function (leaf) {
return isAfter(nonEditableNode, leaf);
})) !== null && _leafNodes2$find !== void 0 ? _leafNodes2$find : null;
if (leafNode) {
searchDirection = 'forward';
}
}
if (leafNode) {
textNode = leafNode.closest('[data-slate-node="text"]');
domNode = leafNode;
if (searchDirection === 'forward') {
offset = 0;
} else {
offset = domNode.textContent.length;
domNode.querySelectorAll('[data-slate-zero-width]').forEach(function (el) {
offset -= el.textContent.length;
});
}
}
}
if (domNode && offset === domNode.textContent.length &&
// COMPAT: Android IMEs might remove the zero width space while composing,
// and we don't add it for line-breaks.
IS_ANDROID && domNode.getAttribute('data-slate-zero-width') === 'z' && (_domNode$textContent = domNode.textContent) !== null && _domNode$textContent !== void 0 && _domNode$textContent.startsWith("\uFEFF") && (
// COMPAT: If the parent node is a Slate zero-width space, editor is
// because the text node should have no characters. However, during IME
// composition the ASCII characters will be prepended to the zero-width
// space, so subtract 1 from the offset to account for the zero-width
// space character.
parentNode.hasAttribute('data-slate-zero-width') ||
// COMPAT: In Firefox, `range.cloneContents()` returns an extra trailing '\n'
// when the document ends with a new-line character. This results in the offset
// length being off by one, so we need to subtract one to account for this.
IS_FIREFOX && (_domNode$textContent2 = domNode.textContent) !== null && _domNode$textContent2 !== void 0 && _domNode$textContent2.endsWith('\n\n'))) {
offset--;
}
}
if (IS_ANDROID && !textNode && !exactMatch) {
var node = parentNode.hasAttribute('data-slate-node') ? parentNode : parentNode.closest('[data-slate-node]');
if (node && DOMEditor.hasDOMNode(editor, node, {
editable: true
})) {
var _slateNode = DOMEditor.toSlateNode(editor, node);
var nodePath;
try {
nodePath = DOMEditor.findPath(editor, _slateNode);
} catch (e) {
if (suppressThrow) {
return null;
}
throw e;
}
var _Editor$start = slate.Editor.start(editor, nodePath),
_path = _Editor$start.path,
_offset = _Editor$start.offset;
if (!node.querySelector('[data-slate-leaf]')) {