UNPKG

@grafana/slate-react

Version:

A set of React components for building completely customizable rich-text editors.

1,777 lines (1,445 loc) 180 kB
import React, { useRef, useLayoutEffect } from 'react'; import Types from 'prop-types'; import SlateTypes from 'slate-prop-types'; import ImmutableTypes from 'react-immutable-proptypes'; import Debug from 'debug'; import warning from 'tiny-warning'; import { PathUtils, Node, Value, Editor } from 'slate'; import getWindow from 'get-window'; import isBackward from 'selection-is-backward'; import { IS_SAFARI, IS_IOS, IS_IE, IS_ANDROID, IS_FIREFOX, HAS_INPUT_EVENTS_LEVEL_2, IS_EDGE } from 'slate-dev-environment'; import throttle from 'lodash/throttle'; import omit from 'lodash/omit'; import { List } from 'immutable'; import Hotkeys from 'slate-hotkeys'; import ReactDOM from 'react-dom'; import Base64 from 'slate-base64-serializer'; import Plain from 'slate-plain-serializer'; import invariant from 'tiny-invariant'; import PlaceholderPlugin from 'slate-react-placeholder'; import memoizeOne from 'memoize-one'; /** * Event handlers used by Slate plugins. * * @type {Array} */ var EVENT_HANDLERS = ['onBeforeInput', 'onBlur', 'onClick', 'onContextMenu', 'onCompositionEnd', 'onCompositionStart', 'onCopy', 'onCut', 'onDragEnd', 'onDragEnter', 'onDragExit', 'onDragLeave', 'onDragOver', 'onDragStart', 'onDrop', 'onInput', 'onFocus', 'onKeyDown', 'onKeyUp', 'onMouseDown', 'onMouseUp', 'onPaste', 'onSelect']; /** * Other handlers used by Slate plugins. * * @type {Array} */ var OTHER_HANDLERS = ['decorateNode', 'renderAnnotation', 'renderBlock', 'renderDecoration', 'renderDocument', 'renderEditor', 'renderInline', 'renderMark']; /** * DOM data attribute strings that refer to Slate concepts. * * @type {String} */ var DATA_ATTRS = { EDITOR: 'data-slate-editor', FRAGMENT: 'data-slate-fragment', KEY: 'data-key', LEAF: 'data-slate-leaf', LENGTH: 'data-slate-length', OBJECT: 'data-slate-object', OFFSET_KEY: 'data-offset-key', SPACER: 'data-slate-spacer', STRING: 'data-slate-string', TEXT: 'data-slate-object', VOID: 'data-slate-void', ZERO_WIDTH: 'data-slate-zero-width' }; /** * DOM selector strings that refer to Slate concepts. * * @type {String} */ var SELECTORS = { BLOCK: '[' + DATA_ATTRS.OBJECT + '="block"]', EDITOR: '[' + DATA_ATTRS.EDITOR + ']', INLINE: '[' + DATA_ATTRS.OBJECT + '="inline"]', KEY: '[' + DATA_ATTRS.KEY + ']', LEAF: '[' + DATA_ATTRS.LEAF + ']', OBJECT: '[' + DATA_ATTRS.OBJECT + ']', STRING: '[' + DATA_ATTRS.STRING + ']', TEXT: '[' + DATA_ATTRS.OBJECT + '="text"]', VOID: '[' + DATA_ATTRS.VOID + ']', ZERO_WIDTH: '[' + DATA_ATTRS.ZERO_WIDTH + ']' }; var classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; var createClass = function () { 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, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); var defineProperty = function (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; }; var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; var inherits = function (subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }; var possibleConstructorReturn = function (self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }; var slicedToArray = function () { function sliceIterator(arr, i) { 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"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); var toConsumableArray = function (arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) arr2[i] = arr[i]; return arr2; } else { return Array.from(arr); } }; /** * Offset key parser regex. * * @type {RegExp} */ var PARSER = /^([\w-]+)(?::(\d+))?$/; /** * Parse an offset key `string`. * * @param {String} string * @return {Object} */ function parse(string) { var matches = PARSER.exec(string); if (!matches) { throw new Error("Invalid offset key string \"" + string + "\"."); } var _matches = slicedToArray(matches, 3), original = _matches[0], key = _matches[1], index = _matches[2]; // eslint-disable-line no-unused-vars return { key: key, index: parseInt(index, 10) }; } /** * Stringify an offset key `object`. * * @param {Object} object * @property {String} key * @property {Number} index * @return {String} */ function stringify(object) { return object.key + ":" + object.index; } /** * Export. * * @type {Object} */ var OffsetKey = { parse: parse, stringify: stringify }; /** * Leaf strings with text in them. * * @type {Component} */ var TextString = function TextString(_ref) { var _ref$text = _ref.text, text = _ref$text === undefined ? '' : _ref$text, _ref$isTrailing = _ref.isTrailing, isTrailing = _ref$isTrailing === undefined ? false : _ref$isTrailing; var ref = useRef(null); useLayoutEffect(function () { if (ref.current && ref.current.innerText !== text) { ref.current.innerText = text; } }); return React.createElement( 'span', _extends({}, defineProperty({}, DATA_ATTRS.STRING, true), { ref: ref }), text, isTrailing ? '\n' : null ); }; /** * Leaf strings without text, render as zero-width strings. * * @type {Component} */ var ZeroWidthString = function ZeroWidthString(_ref3) { var _ref4; var _ref3$length = _ref3.length, length = _ref3$length === undefined ? 0 : _ref3$length, _ref3$isLineBreak = _ref3.isLineBreak, isLineBreak = _ref3$isLineBreak === undefined ? false : _ref3$isLineBreak; return React.createElement( 'span', (_ref4 = {}, defineProperty(_ref4, DATA_ATTRS.ZERO_WIDTH, isLineBreak ? 'n' : 'z'), defineProperty(_ref4, DATA_ATTRS.LENGTH, length), _ref4), '\uFEFF', isLineBreak ? React.createElement('br', null) : null ); }; /** * Individual leaves in a text node with unique formatting. * * @type {Component} */ var Leaf = function Leaf(props) { var _attrs; var marks = props.marks, annotations = props.annotations, decorations = props.decorations, node = props.node, index = props.index, offset = props.offset, text = props.text, editor = props.editor, parent = props.parent, block = props.block, leaves = props.leaves; var offsetKey = OffsetKey.stringify({ key: node.key, index: index }); var children = void 0; if (editor.query('isVoid', parent)) { // COMPAT: Render text inside void nodes with a zero-width space. // So the node can contain selection but the text is not visible. children = React.createElement(ZeroWidthString, { length: parent.text.length }); } else if (text === '' && parent.object === 'block' && parent.text === '' && parent.nodes.last() === node) { // 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. children = React.createElement(ZeroWidthString, { isLineBreak: true }); } else if (text === '') { // 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. children = React.createElement(ZeroWidthString, null); } else { // 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. var lastText = block.getLastText(); var lastChar = text.charAt(text.length - 1); var isLastText = node === lastText; var isLastLeaf = index === leaves.size - 1; if (isLastText && isLastLeaf && lastChar === '\n') { children = React.createElement(TextString, { isTrailing: true, text: text }); } else { children = React.createElement(TextString, { text: text }); } } var renderProps = { editor: editor, marks: marks, annotations: annotations, decorations: decorations, node: node, offset: offset, text: text // 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 _iteratorNormalCompletion = true; var _didIteratorError = false; var _iteratorError = undefined; try { for (var _iterator = marks[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { var mark = _step.value; var ret = editor.run('renderMark', _extends({}, renderProps, { mark: mark, children: children, attributes: defineProperty({}, DATA_ATTRS.OBJECT, 'mark') })); if (ret) { children = ret; } } } catch (err) { _didIteratorError = true; _iteratorError = err; } finally { try { if (!_iteratorNormalCompletion && _iterator.return) { _iterator.return(); } } finally { if (_didIteratorError) { throw _iteratorError; } } } var _iteratorNormalCompletion2 = true; var _didIteratorError2 = false; var _iteratorError2 = undefined; try { for (var _iterator2 = decorations[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { var decoration = _step2.value; var ret = editor.run('renderDecoration', _extends({}, renderProps, { decoration: decoration, children: children, attributes: defineProperty({}, DATA_ATTRS.OBJECT, 'decoration') })); if (ret) { children = ret; } } } catch (err) { _didIteratorError2 = true; _iteratorError2 = err; } finally { try { if (!_iteratorNormalCompletion2 && _iterator2.return) { _iterator2.return(); } } finally { if (_didIteratorError2) { throw _iteratorError2; } } } var _iteratorNormalCompletion3 = true; var _didIteratorError3 = false; var _iteratorError3 = undefined; try { for (var _iterator3 = annotations[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) { var annotation = _step3.value; var ret = editor.run('renderAnnotation', _extends({}, renderProps, { annotation: annotation, children: children, attributes: defineProperty({}, DATA_ATTRS.OBJECT, 'annotation') })); if (ret) { children = ret; } } } catch (err) { _didIteratorError3 = true; _iteratorError3 = err; } finally { try { if (!_iteratorNormalCompletion3 && _iterator3.return) { _iterator3.return(); } } finally { if (_didIteratorError3) { throw _iteratorError3; } } } var attrs = (_attrs = {}, defineProperty(_attrs, DATA_ATTRS.LEAF, true), defineProperty(_attrs, DATA_ATTRS.OFFSET_KEY, offsetKey), _attrs); return React.createElement( 'span', attrs, children ); }; /** * Prop types. * * @type {Object} */ Leaf.propTypes = { annotations: ImmutableTypes.list.isRequired, block: SlateTypes.block.isRequired, decorations: ImmutableTypes.list.isRequired, editor: Types.object.isRequired, index: Types.number.isRequired, leaves: Types.object.isRequired, marks: SlateTypes.marks.isRequired, node: SlateTypes.node.isRequired, offset: Types.number.isRequired, parent: SlateTypes.node.isRequired, text: Types.string.isRequired /** * A memoized version of `Leaf` that updates less frequently. * * @type {Component} */ };var MemoizedLeaf = React.memo(Leaf, function (prev, next) { return next.block === prev.block && next.index === prev.index && next.marks === prev.marks && next.parent === prev.parent && next.text === prev.text && next.annotations.equals(prev.annotations) && next.decorations.equals(prev.decorations); }); /** * Text node. * * @type {Component} */ var Text = React.forwardRef(function (props, ref) { var _ref; var annotations = props.annotations, block = props.block, decorations = props.decorations, node = props.node, parent = props.parent, editor = props.editor, style = props.style; var key = node.key; var leaves = node.getLeaves(annotations, decorations); var at = 0; return React.createElement( 'span', _extends({ ref: ref, style: style }, (_ref = {}, defineProperty(_ref, DATA_ATTRS.OBJECT, node.object), defineProperty(_ref, DATA_ATTRS.KEY, key), _ref)), leaves.map(function (leaf, index) { var text = leaf.text; var offset = at; at += text.length; return React.createElement(MemoizedLeaf, { key: node.key + '-' + index, block: block, editor: editor, index: index, annotations: leaf.annotations, decorations: leaf.decorations, marks: leaf.marks, node: node, offset: offset, parent: parent, leaves: leaves, text: text }); }) ); }); /** * Prop types. * * @type {Object} */ Text.propTypes = { annotations: ImmutableTypes.map.isRequired, block: SlateTypes.block, decorations: ImmutableTypes.list.isRequired, editor: Types.object.isRequired, node: SlateTypes.node.isRequired, parent: SlateTypes.node.isRequired, style: Types.object /** * A memoized version of `Text` that updates less frequently. * * @type {Component} */ };var MemoizedText = React.memo(Text, function (prev, next) { return ( // PERF: There are cases where it will have // changed, but it's properties will be exactly the same (eg. copy-paste) // which this won't catch. But that's rare and not a drag on performance, so // for simplicity we just let them through. next.node === prev.node && // If the node parent is a block node, and it was the last child of the // block, re-render to cleanup extra `\n`. next.parent.object === 'block' && prev.parent.nodes.last() === prev.node && next.parent.nodes.last() !== next.node && // The formatting hasn't changed. next.annotations.equals(prev.annotations) && next.decorations.equals(prev.decorations) ); }); /** * Debug. * * @type {Function} */ var debug = Debug('slate:void'); /** * Void. * * @type {Component} */ var Void = function (_React$Component) { inherits(Void, _React$Component); function Void() { var _ref; var _temp, _this, _ret; classCallCheck(this, Void); for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } return _ret = (_temp = (_this = possibleConstructorReturn(this, (_ref = Void.__proto__ || Object.getPrototypeOf(Void)).call.apply(_ref, [this].concat(args))), _this), _initialiseProps.call(_this), _temp), possibleConstructorReturn(_this, _ret); } /** * Property types. * * @type {Object} */ /** * Debug. * * @param {String} message * @param {Mixed} ...args */ createClass(Void, [{ key: 'render', /** * Render. * * @return {Element} */ value: function render() { var _attrs; var props = this.props; var children = props.children, node = props.node, readOnly = props.readOnly; var Tag = node.object === 'block' ? 'div' : 'span'; var style = { height: '0', color: 'transparent', outline: 'none', position: 'absolute' }; var spacerAttrs = defineProperty({}, DATA_ATTRS.SPACER, true); var spacer = React.createElement( Tag, _extends({ style: style }, spacerAttrs), this.renderText() ); var content = React.createElement( Tag, { contentEditable: readOnly ? null : false }, children ); this.debug('render', { props: props }); var attrs = (_attrs = {}, defineProperty(_attrs, DATA_ATTRS.VOID, true), defineProperty(_attrs, DATA_ATTRS.KEY, node.key), _attrs); return React.createElement( Tag, _extends({ contentEditable: readOnly || node.object === 'block' ? null : false }, attrs), readOnly ? null : spacer, content ); } /** * Render the void node's text node, which will catch the cursor when it the * void node is navigated to with the arrow keys. * * Having this text node there means the browser continues to manage the * selection natively, so it keeps track of the right offset when moving * across the block. * * @return {Element} */ }]); return Void; }(React.Component); /** * Export. * * @type {Component} */ Void.propTypes = { block: SlateTypes.block, children: Types.any.isRequired, editor: Types.object.isRequired, node: SlateTypes.node.isRequired, parent: SlateTypes.node.isRequired, readOnly: Types.bool.isRequired }; var _initialiseProps = function _initialiseProps() { var _this2 = this; this.debug = function (message) { for (var _len2 = arguments.length, args = Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) { args[_key2 - 1] = arguments[_key2]; } var node = _this2.props.node; var key = node.key, type = node.type; var id = key + ' (' + type + ')'; debug.apply(undefined, [message, '' + id].concat(args)); }; this.renderText = function () { var _props = _this2.props, annotations = _props.annotations, block = _props.block, decorations = _props.decorations, node = _props.node, readOnly = _props.readOnly, editor = _props.editor, textRef = _props.textRef; var child = node.getFirstText(); return React.createElement(MemoizedText, { ref: textRef, annotations: annotations, block: node.object === 'block' ? node : block, decorations: decorations, editor: editor, key: child.key, node: child, parent: node, readOnly: readOnly }); }; }; /** * Debug. * * @type {Function} */ var debug$1 = Debug('slate:node'); /** * Node. * * @type {Component} */ var Node$1 = function (_React$Component) { inherits(Node$$1, _React$Component); function Node$$1() { var _ref; var _temp, _this, _ret; classCallCheck(this, Node$$1); for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } return _ret = (_temp = (_this = possibleConstructorReturn(this, (_ref = Node$$1.__proto__ || Object.getPrototypeOf(Node$$1)).call.apply(_ref, [this].concat(args))), _this), _initialiseProps$1.call(_this), _temp), possibleConstructorReturn(_this, _ret); } /** * Property types. * * @type {Object} */ /** * Temporary values. * * @type {Object} */ /** * A ref for the contenteditable DOM node. * * @type {Object} */ /** * Debug. * * @param {String} message * @param {Mixed} ...args */ createClass(Node$$1, [{ key: 'shouldComponentUpdate', /** * Should the node update? * * @param {Object} nextProps * @param {Object} value * @return {Boolean} */ value: function shouldComponentUpdate(nextProps) { var props = this.props; var editor = props.editor; var shouldUpdate = editor.run('shouldNodeComponentUpdate', props, nextProps); var n = nextProps; var p = props; // If the `Component` has a custom logic to determine whether the component // needs to be updated or not, return true if it returns true. If it returns // false, we need to ignore it, because it shouldn't be allowed it. if (shouldUpdate != null) { warning(false, 'As of slate-react@0.22 the `shouldNodeComponentUpdate` middleware is deprecated. You can pass specific values down the tree using React\'s built-in "context" construct instead.'); if (shouldUpdate) { return true; } warning(shouldUpdate !== false, "Returning false in `shouldNodeComponentUpdate` does not disable Slate's internal `shouldComponentUpdate` logic. If you want to prevent updates, use React's `shouldComponentUpdate` instead."); } // If the `readOnly` status has changed, re-render in case there is any // user-land logic that depends on it, like nested editable contents. if (n.readOnly !== p.readOnly) { return true; } // If the node has changed, update. PERF: There are cases where it will have // changed, but it's properties will be exactly the same (eg. copy-paste) // which this won't catch. But that's rare and not a drag on performance, so // for simplicity we just let them through. if (n.node !== p.node) { return true; } // If the selection value of the node or of some of its children has changed, // re-render in case there is any user-land logic depends on it to render. // if the node is selected update it, even if it was already selected: the // selection value of some of its children could have been changed and they // need to be rendered again. if (!n.selection && p.selection || n.selection && !p.selection || n.selection && p.selection && !n.selection.equals(p.selection)) { return true; } // If the annotations have changed, update. if (!n.annotations.equals(p.annotations)) { return true; } // If the decorations have changed, update. if (!n.decorations.equals(p.decorations)) { return true; } // Otherwise, don't update. return false; } /** * Render. * * @return {Element} */ }, { key: 'render', value: function render() { var _this2 = this, _attributes; this.debug('render', this); var _props = this.props, annotations = _props.annotations, block = _props.block, decorations = _props.decorations, editor = _props.editor, node = _props.node, parent = _props.parent, readOnly = _props.readOnly, selection = _props.selection; var newDecorations = node.getDecorations(editor); var children = node.nodes.toArray().map(function (child, i) { var Component = child.object === 'text' ? MemoizedText : Node$$1; var sel = selection && getRelativeRange(node, i, selection); var decs = newDecorations.concat(decorations).map(function (d) { return getRelativeRange(node, i, d); }).filter(function (d) { return d; }); var anns = annotations.map(function (a) { return getRelativeRange(node, i, a); }).filter(function (a) { return a; }); return React.createElement(Component, { block: node.object === 'block' ? node : block, editor: editor, annotations: anns, decorations: decs, selection: sel, key: child.key, node: child, parent: node, readOnly: readOnly // COMPAT: We use this map of refs to lookup a DOM node down the // tree of components by path. , ref: function ref(_ref2) { if (_ref2) { _this2.tmp.nodeRefs[i] = _ref2; } else { delete _this2.tmp.nodeRefs[i]; } } }); }); // Attributes that the developer must mix into the element in their // custom node renderer component. var attributes = (_attributes = {}, defineProperty(_attributes, DATA_ATTRS.OBJECT, node.object), defineProperty(_attributes, DATA_ATTRS.KEY, node.key), defineProperty(_attributes, 'ref', this.ref), _attributes); // If it's a block node with inline children, add the proper `dir` attribute // for text direction. if (node.isLeafBlock()) { var direction = node.getTextDirection(); if (direction === 'rtl') attributes.dir = 'rtl'; } var render = void 0; if (node.object === 'block') { render = 'renderBlock'; } else if (node.object === 'document') { render = 'renderDocument'; } else if (node.object === 'inline') { render = 'renderInline'; } var element = editor.run(render, { attributes: attributes, children: children, editor: editor, isFocused: !!selection && selection.isFocused, isSelected: !!selection, node: node, parent: parent, readOnly: readOnly }); return editor.isVoid(node) ? React.createElement( Void, _extends({}, this.props, { textRef: function textRef(ref) { if (ref) { _this2.tmp.nodeRefs[0] = ref; } else { delete _this2.tmp.nodeRefs[0]; } } }), element ) : element; } }]); return Node$$1; }(React.Component); /** * Return a `range` relative to a child at `index`. * * @param {Range} range * @param {Number} index * @return {Range} */ Node$1.propTypes = { annotations: ImmutableTypes.map.isRequired, block: SlateTypes.block, decorations: ImmutableTypes.list.isRequired, editor: Types.object.isRequired, node: SlateTypes.node.isRequired, parent: SlateTypes.node, readOnly: Types.bool.isRequired, selection: SlateTypes.selection }; var _initialiseProps$1 = function _initialiseProps() { var _this3 = this; this.tmp = { nodeRefs: {} }; this.ref = React.createRef(); this.debug = function (message) { for (var _len2 = arguments.length, args = Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) { args[_key2 - 1] = arguments[_key2]; } var node = _this3.props.node; var key = node.key, type = node.type; debug$1.apply(undefined, [message, key + ' (' + type + ')'].concat(args)); }; }; function getRelativeRange(node, index, range) { if (range.isUnset) { return null; } var child = node.nodes.get(index); var _range = range, start = _range.start, end = _range.end; var _start = start, startPath = _start.path; var _end = end, endPath = _end.path; var startIndex = startPath.first(); var endIndex = endPath.first(); if (startIndex === index) { start = start.setPath(startPath.rest()); } else if (startIndex < index && index <= endIndex) { if (child.object === 'text') { start = start.moveTo(PathUtils.create([index]), 0).setKey(child.key); } else { var _child$texts = child.texts(), _child$texts2 = slicedToArray(_child$texts, 1), first = _child$texts2[0]; var _first = slicedToArray(first, 2), firstNode = _first[0], firstPath = _first[1]; start = start.moveTo(firstPath, 0).setKey(firstNode.key); } } else { start = null; } if (endIndex === index) { end = end.setPath(endPath.rest()); } else if (startIndex <= index && index < endIndex) { if (child.object === 'text') { var length = child.text.length; end = end.moveTo(PathUtils.create([index]), length).setKey(child.key); } else { var _child$texts3 = child.texts({ direction: 'backward' }), _child$texts4 = slicedToArray(_child$texts3, 1), last = _child$texts4[0]; var _last = slicedToArray(last, 2), lastNode = _last[0], lastPath = _last[1]; end = end.moveTo(lastPath, lastNode.text.length).setKey(lastNode.key); } } else { end = null; } if (!start || !end) { return null; } range = range.setAnchor(start); range = range.setFocus(end); return range; } /** * CSS overflow values that would cause scrolling. * * @type {Array} */ var OVERFLOWS = ['auto', 'overlay', 'scroll']; /** * Detect whether we are running IOS version 11 */ var IS_IOS_11 = IS_IOS && !!window.navigator.userAgent.match(/os 11_/i); /** * Find the nearest parent with scrolling, or window. * * @param {el} Element */ function findScrollContainer(el, window) { var parent = el.parentNode; var scroller = void 0; while (!scroller) { if (!parent.parentNode) break; var style = window.getComputedStyle(parent); var overflowY = style.overflowY; if (OVERFLOWS.includes(overflowY)) { scroller = parent; break; } parent = parent.parentNode; } // COMPAT: Because Chrome does not allow doucment.body.scrollTop, we're // assuming that window.scrollTo() should be used if the scrollable element // turns out to be document.body or document.documentElement. This will work // unless body is intentionally set to scrollable by restricting its height // (e.g. height: 100vh). if (!scroller) { return window.document.body; } return scroller; } /** * Scroll the current selection's focus point into view if needed. * * @param {Selection} selection */ function scrollToSelection(selection) { if (IS_IOS_11) return; if (!selection.anchorNode) return; var window = getWindow(selection.anchorNode); var scroller = findScrollContainer(selection.anchorNode, window); var isWindow = scroller === window.document.body || scroller === window.document.documentElement; var backward = isBackward(selection); var range = selection.getRangeAt(0).cloneRange(); range.collapse(backward); var cursorRect = range.getBoundingClientRect(); // COMPAT: range.getBoundingClientRect() returns 0s in Safari when range is // collapsed. Expanding the range by 1 is a relatively effective workaround // for vertical scroll, although horizontal may be off by 1 character. // https://bugs.webkit.org/show_bug.cgi?id=138949 // https://bugs.chromium.org/p/chromium/issues/detail?id=435438 if (IS_SAFARI) { if (range.collapsed && cursorRect.top === 0 && cursorRect.height === 0) { if (range.startOffset === 0) { range.setEnd(range.endContainer, 1); } else { range.setStart(range.startContainer, range.startOffset - 1); } cursorRect = range.getBoundingClientRect(); if (cursorRect.top === 0 && cursorRect.height === 0) { if (range.getClientRects().length) { cursorRect = range.getClientRects()[0]; } } } } var width = void 0; var height = void 0; var yOffset = void 0; var xOffset = void 0; var scrollerTop = 0; var scrollerLeft = 0; var scrollerBordersY = 0; var scrollerBordersX = 0; var scrollerPaddingTop = 0; var scrollerPaddingBottom = 0; var scrollerPaddingLeft = 0; var scrollerPaddingRight = 0; if (isWindow) { var innerWidth = window.innerWidth, innerHeight = window.innerHeight, pageYOffset = window.pageYOffset, pageXOffset = window.pageXOffset; width = innerWidth; height = innerHeight; yOffset = pageYOffset; xOffset = pageXOffset; } else { var offsetWidth = scroller.offsetWidth, offsetHeight = scroller.offsetHeight, scrollTop = scroller.scrollTop, scrollLeft = scroller.scrollLeft; var _window$getComputedSt = window.getComputedStyle(scroller), borderTopWidth = _window$getComputedSt.borderTopWidth, borderBottomWidth = _window$getComputedSt.borderBottomWidth, borderLeftWidth = _window$getComputedSt.borderLeftWidth, borderRightWidth = _window$getComputedSt.borderRightWidth, paddingTop = _window$getComputedSt.paddingTop, paddingBottom = _window$getComputedSt.paddingBottom, paddingLeft = _window$getComputedSt.paddingLeft, paddingRight = _window$getComputedSt.paddingRight; var scrollerRect = scroller.getBoundingClientRect(); width = offsetWidth; height = offsetHeight; scrollerTop = scrollerRect.top + parseInt(borderTopWidth, 10); scrollerLeft = scrollerRect.left + parseInt(borderLeftWidth, 10); scrollerBordersY = parseInt(borderTopWidth, 10) + parseInt(borderBottomWidth, 10); scrollerBordersX = parseInt(borderLeftWidth, 10) + parseInt(borderRightWidth, 10); scrollerPaddingTop = parseInt(paddingTop, 10); scrollerPaddingBottom = parseInt(paddingBottom, 10); scrollerPaddingLeft = parseInt(paddingLeft, 10); scrollerPaddingRight = parseInt(paddingRight, 10); yOffset = scrollTop; xOffset = scrollLeft; } var cursorTop = cursorRect.top + yOffset - scrollerTop; var cursorLeft = cursorRect.left + xOffset - scrollerLeft; var x = xOffset; var y = yOffset; if (cursorLeft < xOffset) { // selection to the left of viewport x = cursorLeft - scrollerPaddingLeft; } else if (cursorLeft + cursorRect.width + scrollerBordersX > xOffset + width) { // selection to the right of viewport x = cursorLeft + scrollerBordersX + scrollerPaddingRight - width; } if (cursorTop < yOffset) { // selection above viewport y = cursorTop - scrollerPaddingTop; } else if (cursorTop + cursorRect.height + scrollerBordersY > yOffset + height) { // selection below viewport y = cursorTop + scrollerBordersY + scrollerPaddingBottom + cursorRect.height - height; } if (isWindow) { window.scrollTo(x, y); } else { scroller.scrollTop = y; scroller.scrollLeft = x; } } /** * Cross-browser remove all ranges from a `domSelection`. * * @param {Selection} domSelection */ function removeAllRanges(domSelection) { // COMPAT: In IE 11, if the selection contains nested tables, then // `removeAllRanges` will throw an error. if (IS_IE) { var range = window.document.body.createTextRange(); range.collapse(); range.select(); } else { domSelection.removeAllRanges(); } } var FIREFOX_NODE_TYPE_ACCESS_ERROR = /Permission denied to access property "nodeType"/; /** * Debug. * * @type {Function} */ var debug$2 = Debug('slate:content'); /** * Separate debug to easily see when the DOM has updated either by render or * changing selection. * * @type {Function} */ debug$2.update = Debug('slate:update'); /** * Content. * * @type {Component} */ var Content = function (_React$Component) { inherits(Content, _React$Component); function Content() { var _ref; var _temp, _this, _ret; classCallCheck(this, Content); for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } return _ret = (_temp = (_this = possibleConstructorReturn(this, (_ref = Content.__proto__ || Object.getPrototypeOf(Content)).call.apply(_ref, [this].concat(args))), _this), _this.tmp = { isUpdatingSelection: false, nodeRef: React.createRef(), nodeRefs: {}, contentKey: 0, nativeSelection: {} // Native selection object stored to check if `onNativeSelectionChange` has triggered yet /** * A ref for the contenteditable DOM node. * * @type {Object} */ }, _this.ref = React.createRef(), _this.setRef = function (el) { _this.ref.current = el; _this.props.editor.el = el; }, _this.handlers = EVENT_HANDLERS.reduce(function (obj, handler) { obj[handler] = function (event) { return _this.onEvent(handler, event); }; return obj; }, {}), _this.updateSelection = function () { var editor = _this.props.editor; var value = editor.value; var selection = value.selection; var isBackward$$1 = selection.isBackward; var window = getWindow(_this.ref.current); var native = window.getSelection(); var activeElement = window.document.activeElement; if (debug$2.update.enabled) { debug$2.update('updateSelection', { selection: selection.toJSON() }); } // COMPAT: In Firefox, there's a but where `getSelection` can return `null`. // https://bugzilla.mozilla.org/show_bug.cgi?id=827585 (2018/11/07) if (!native) { return; } var rangeCount = native.rangeCount, anchorNode = native.anchorNode; var updated = false; // If the Slate selection is blurred, but the DOM's active element is still // the editor, we need to blur it. if (selection.isBlurred && activeElement === _this.ref.current) { _this.ref.current.blur(); updated = true; } // If the Slate selection is unset, but the DOM selection has a range // selected in the editor, we need to remove the range. // However we should _not_ remove the range if the selection as // reported by `getSelection` is not equal to `this.tmp.nativeSelection` // as this suggests `onNativeSelectionChange` has not triggered yet (which can occur in Firefox) // See: https://github.com/ianstormtaylor/slate/pull/2995 var propsToCompare = ['anchorNode', 'anchorOffset', 'focusNode', 'focusOffset', 'isCollapsed', 'rangeCount', 'type']; var selectionsEqual = true; var _iteratorNormalCompletion = true; var _didIteratorError = false; var _iteratorError = undefined; try { for (var _iterator = propsToCompare[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { var prop = _step.value; if (_this.tmp.nativeSelection[prop] !== native[prop]) { selectionsEqual = false; } } } catch (err) { _didIteratorError = true; _iteratorError = err; } finally { try { if (!_iteratorNormalCompletion && _iterator.return) { _iterator.return(); } } finally { if (_didIteratorError) { throw _iteratorError; } } } if (selection.isUnset && rangeCount && _this.isInEditor(anchorNode) && selectionsEqual) { removeAllRanges(native); updated = true; } // If the Slate selection is focused, but the DOM's active element is not // the editor, we need to focus it. We prevent scrolling because we handle // scrolling to the correct selection. if (selection.isFocused && activeElement !== _this.ref.current) { _this.ref.current.focus({ preventScroll: true }); updated = true; } // Otherwise, figure out which DOM nodes should be selected... if (selection.isFocused && selection.isSet) { var current = !!native.rangeCount && native.getRangeAt(0); var range = editor.findDOMRange(selection); if (!range) { warning(false, 'Unable to find a native DOM range from the current selection.'); return; } var startContainer = range.startContainer, startOffset = range.startOffset, endContainer = range.endContainer, endOffset = range.endOffset; // If the new range matches the current selection, there is nothing to fix. // COMPAT: The native `Range` object always has it's "start" first and "end" // last in the DOM. It has no concept of "backwards/forwards", so we have // to check both orientations here. (2017/10/31) if (current) { if (startContainer === current.startContainer && startOffset === current.startOffset && endContainer === current.endContainer && endOffset === current.endOffset || startContainer === current.endContainer && startOffset === current.endOffset && endContainer === current.startContainer && endOffset === current.startOffset) { return; } } // Otherwise, set the `isUpdatingSelection` flag and update the selection. updated = true; _this.tmp.isUpdatingSelection = true; removeAllRanges(native); // COMPAT: IE 11 does not support `setBaseAndExtent`. (2018/11/07) if (native.setBaseAndExtent) { // COMPAT: Since the DOM range has no concept of backwards/forwards // we need to check and do the right thing here. if (isBackward$$1) { native.setBaseAndExtent(range.endContainer, range.endOffset, range.startContainer, range.startOffset); } else { native.setBaseAndExtent(range.startContainer, range.startOffset, range.endContainer, range.endOffset); } } else { native.addRange(range); } // Scroll to the selection, in case it's out of view. scrollToSelection(native); // Then unset the `isUpdatingSelection` flag after a delay, to ensure that // it is still set when selection-related events from updating it fire. 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 (IS_FIREFOX && _this.ref.current) { _this.ref.current.focus(); } _this.tmp.isUpdatingSelection = false; debug$2.update('updateSelection:setTimeout', { anchorOffset: window.getSelection().anchorOffset }); }); } if (updated && (debug$2.enabled || debug$2.update.enabled)) { debug$2('updateSelection', { selection: selection, native: native, activeElement: activeElement }); debug$2.update('updateSelection:applied', { selection: selection.toJSON(), native: { anchorOffset: native.anchorOffset, focusOffset: native.focusOffset } }); } }, _this.isInEditor = function (target) { var el = void 0; try { // COMPAT: In Firefox, sometimes the node can be comment which doesn't // have .closest and it crashes. if (target.nodeType === 8) { return false; } // COMPAT: Text nodes don't have `isContentEditable` property. So, when // `target` is a text node use its parent node for check. el = target.nodeType === 3 ? target.parentNode : target; } catch (err) { // COMPAT: In Firefox, `target.nodeType` will throw an error if target is // originating from an internal "restricted" element (e.g. a stepper // arrow on a number input) // see github.com/ianstormtaylor/slate/issues/1819 if (IS_FIREFOX && FIREFOX_NODE_TYPE_ACCESS_ERROR.test(err.message)) { return false; } throw err; } return el.isContentEditable && (el === _this.ref.current || el.closest(SELECTORS.EDITOR) === _this.ref.current); }, _this.onNativeSelectionChange = throttle(function (event) { if (_this.props.readOnly) return; var window = getWindow(event.target); var activeElement = window.document.activeElement; var native = window.getSelection(); debug$2.update('onNativeSelectionChange', { anchorOffset: native.anchorOffset }); if (activeElement !== _this.ref.current) return; _this.tmp.nativeSelection = { anchorNode: native.anchorNode, anchorOffset: native.anchorOffset, focusNode: native.focusNode, focusOffset: native.focusOffset, isCollapsed: native.isCollapsed, rangeCount: native.rangeCount, type: native.type }; _this.props.onEvent('onSelect', event); }, 100), _temp), possibleConstructorReturn(_this, _ret); } /** * Property types. * * @type {Object} */ /** * Default properties. * * @type {Object} */ createClass(Content, [{ key: 'componentDidCatch', /** * An error boundary. If there is a render error, we increment `errorKey` * which is part of the container `key` which forces a re-render from * scratch. * * @param {Error} error * @param {String} info */ value: function componentDidCatch(error, info) { debug$2('componentDidCatch', { error: error, info: info }); // The call to `setState` is required despite not setting a value. // Without this call, React will not try to recreate the component tree. this.setState({}); } /** * Temporary values. * * @type {Object} */ /** * Set both `this.ref` and `editor.el` * * @type {DOMElement} */ /** * Create a set of bound event handlers. * * @type {Object} */ }, { key: 'componentDidMount', /** * When the editor first mounts in the DOM we need to: * * - Add native DOM event listeners. * - Update the selection, in case it starts focused. */ value: function componentDidMount() { var window = getWindow(this.ref.current); window.document.addEventListener('selectionchange', this.onNativeSelectionChange); // COMPAT: Restrict scope of `beforeinput` to clients that support the // Input Events Level 2 spec, since they are preventable events. if (HAS_INPUT_EVENTS_LEVEL_2) { this.ref.current.addEventListener('beforeinput', this.handlers.onBeforeInput); } this.updateSelection(); this.props.onEvent('onComponentDidMount'); } /** * When unmounting, remove DOM event listeners. */ }, { key: 'componentWillUnmount', value: function componentWillUnmount() { var window = getWindow(this.ref.current); if (window) { window.document.removeEventListener('selectionchange', this.onNativeSelectionChange); } if (HAS_INPUT_EVENTS_LEVEL_2) { this.ref.current.removeEventListener('beforeinput', this.handlers.onBeforeInput); } this.props.onEvent('onComponentWillUnmount'); } /** * On update, update the selection. */ }, { key: 'componentDidUpdate', value: function componentDidUpdate() { debug$2.update('componentDidUpdate'); this.updateSelection(); this.props.onEvent('onComponentDidUpdate'); } /** * Update the native DOM selection to reflect the internal model. */ /** * Check if an event `target` is fired from within the contenteditable * element. This should be false for edits happening in non-contenteditable * children, such as void nodes and other nested Slate editors. * * @param {Element} target * @return {Boolean} */ }, { key: 'onEvent', /** * On `event` with `handler`. * * @param {String} handler * @param {Event} event */ value: function onEvent(handler, event) { debug$2('onEvent', handler); var nativeEvent = event.nativeEvent || event; var isUndoRedo = event.type === 'keydown' && (Hotkeys.isUndo(nativeEvent) || Hotkeys.isRedo(nativeEvent)); // Ignore `onBlur`, `onFocus` and `onSelect` events generated // programmatically while updating selection. if ((this.tmp.isUpdatingSelection || isUndoRedo) && (handler === 'onSelect' || handler === 'onBlur' || handler === 'onFocus')) { return; } // COMPAT: There are situations where a select event will fire with a new // native selection that resolves to the same internal position. In those // cases we don't need to trigger any changes, since our internal model is // already up to date, but we do want to update the native selection again // to make sure it is in sync. (2017/10/16) // // ANDROID: The updateSelection causes issues in Android when you are // at the end of a block. The selection ends up to the left of the inserted // character instead of to the rig