@grafana/slate-react
Version:
A set of React components for building completely customizable rich-text editors.
1,777 lines (1,445 loc) • 180 kB
JavaScript
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