slate
Version:
A completely customizable framework for building rich text editors.
2,141 lines (1,695 loc) • 489 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('immutable')) :
typeof define === 'function' && define.amd ? define(['exports', 'immutable'], factory) :
(factory((global.Slate = {}),global.Immutable));
}(this, (function (exports,immutable) { 'use strict';
/**
* Mix in an `Interface` to a `Class`.
*
* @param {Class} Class
* @param {Class} Interface
*/
function mixin(Interface, Classes) {
var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;
try {
for (var _iterator = Classes[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
var Class = _step.value;
var _iteratorNormalCompletion2 = true;
var _didIteratorError2 = false;
var _iteratorError2 = undefined;
try {
for (var _iterator2 = Object.getOwnPropertyNames(Interface)[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
var name = _step2.value;
if (Class.hasOwnProperty(name)) continue;
var desc = Object.getOwnPropertyDescriptor(Interface, name);
Object.defineProperty(Class, name, desc);
}
} 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 = Object.getOwnPropertyNames(Interface.prototype)[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) {
var _name = _step3.value;
if (Class.prototype.hasOwnProperty(_name)) continue;
var desc = Object.getOwnPropertyDescriptor(Interface.prototype, _name);
Object.defineProperty(Class.prototype, _name, desc);
}
} catch (err) {
_didIteratorError3 = true;
_iteratorError3 = err;
} finally {
try {
if (!_iteratorNormalCompletion3 && _iterator3.return) {
_iterator3.return();
}
} finally {
if (_didIteratorError3) {
throw _iteratorError3;
}
}
}
}
} catch (err) {
_didIteratorError = true;
_iteratorError = err;
} finally {
try {
if (!_iteratorNormalCompletion && _iterator.return) {
_iterator.return();
}
} finally {
if (_didIteratorError) {
throw _iteratorError;
}
}
}
}
/*!
* isobject <https://github.com/jonschlinkert/isobject>
*
* Copyright (c) 2014-2017, Jon Schlinkert.
* Released under the MIT License.
*/
var isobject = function isObject(val) {
return val != null && typeof val === 'object' && Array.isArray(val) === false;
};
function isObjectObject(o) {
return isobject(o) === true
&& Object.prototype.toString.call(o) === '[object Object]';
}
var isPlainObject = function isPlainObject(o) {
var ctor,prot;
if (isObjectObject(o) === false) return false;
// If has modified constructor
ctor = o.constructor;
if (typeof ctor !== 'function') return false;
// If has modified prototype
prot = ctor.prototype;
if (isObjectObject(prot) === false) return false;
// If constructor does not have an Object-specific method
if (prot.hasOwnProperty('isPrototypeOf') === false) {
return false;
}
// Most likely a plain Object
return true;
};
/**
* An auto-incrementing index for generating keys.
*
* @type {Number}
*/
var n = void 0;
/**
* The global key generating function.
*
* @type {Function}
*/
var generate = void 0;
/**
* Create a key, using a provided key if available.
*
* @param {String|Void} key
* @return {String}
*/
function create(key) {
if (key == null) {
return generate();
}
if (typeof key === 'string') {
return key;
}
throw new Error('Keys must be strings, but you passed: ' + key);
}
/**
* Set a different unique ID generating `function`.
*
* @param {Function} func
*/
function setGenerator(func) {
generate = func;
}
/**
* Reset the key generating function to its initial state.
*/
function resetGenerator() {
n = 0;
generate = function generate() {
return '' + n++;
};
}
/**
* Set the initial state.
*/
resetGenerator();
/**
* Export.
*
* @type {Object}
*/
var KeyUtils = {
create: create,
setGenerator: setGenerator,
resetGenerator: resetGenerator
};
/**
* Slate-specific model types.
*
* @type {Object}
*/
var MODEL_TYPES = {
BLOCK: '@@__SLATE_BLOCK__@@',
CHANGE: '@@__SLATE_CHANGE__@@',
DECORATION: '@@__SLATE_DECORATION__@@',
DOCUMENT: '@@__SLATE_DOCUMENT__@@',
HISTORY: '@@__SLATE_HISTORY__@@',
INLINE: '@@__SLATE_INLINE__@@',
LEAF: '@@__SLATE_LEAF__@@',
MARK: '@@__SLATE_MARK__@@',
OPERATION: '@@__SLATE_OPERATION__@@',
POINT: '@@__SLATE_POINT__@@',
RANGE: '@@__SLATE_RANGE__@@',
SCHEMA: '@@__SLATE_SCHEMA__@@',
SELECTION: '@@__SLATE_SELECTION__@@',
STACK: '@@__SLATE_STACK__@@',
TEXT: '@@__SLATE_TEXT__@@',
VALUE: '@@__SLATE_VALUE__@@'
/**
* Export type identification function
*
* @param {string} type
* @param {any} any
* @return {boolean}
*/
};function isType(type, any) {
return !!(any && any[MODEL_TYPES[type]]);
}
/**
* A `warning` helper, modeled after Facebook's and the `tiny-invariant` library.
*
* @param {Mixed} condition
* @param {String} message
*/
function warning(condition) {
var message = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '';
if (condition) return;
var isProduction = "development" === 'production';
var log = console.warn || console.log; // eslint-disable-line no-console
if (isProduction) {
log('Warning');
} else {
log('Warning: ' + message);
}
}
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) {
return typeof obj;
} : function (obj) {
return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
};
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 objectWithoutProperties = function (obj, keys) {
var target = {};
for (var i in obj) {
if (keys.indexOf(i) >= 0) continue;
if (!Object.prototype.hasOwnProperty.call(obj, i)) continue;
target[i] = obj[i];
}
return target;
};
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);
}
};
/**
* Data.
*
* This isn't an immutable record, it's just a thin wrapper around `Map` so that
* we can allow for more convenient creation.
*
* @type {Object}
*/
var Data = function () {
function Data() {
classCallCheck(this, Data);
}
createClass(Data, null, [{
key: 'create',
/**
* Create a new `Data` with `attrs`.
*
* @param {Object|Data|Map} attrs
* @return {Data} data
*/
value: function create() {
var attrs = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
if (immutable.Map.isMap(attrs)) {
return attrs;
}
if (isPlainObject(attrs)) {
return Data.fromJSON(attrs);
}
throw new Error('`Data.create` only accepts objects or maps, but you passed it: ' + attrs);
}
/**
* Create a `Data` from a JSON `object`.
*
* @param {Object} object
* @return {Data}
*/
}, {
key: 'fromJSON',
value: function fromJSON(object) {
return new immutable.Map(object);
}
/**
* Alias `fromJS`.
*/
}]);
return Data;
}();
/**
* Export.
*
* @type {Object}
*/
Data.fromJS = Data.fromJSON;
/**
* Default properties.
*
* @type {Object}
*/
var DEFAULTS = {
data: new immutable.Map(),
key: undefined,
nodes: new immutable.List()
/**
* Document.
*
* @type {Document}
*/
};
var Document = function (_Record) {
inherits(Document, _Record);
function Document() {
classCallCheck(this, Document);
return possibleConstructorReturn(this, (Document.__proto__ || Object.getPrototypeOf(Document)).apply(this, arguments));
}
createClass(Document, [{
key: 'toJSON',
/**
* Return a JSON representation of the document.
*
* @param {Object} options
* @return {Object}
*/
value: function toJSON() {
var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
var object = {
object: this.object,
data: this.data.toJSON(),
nodes: this.nodes.toArray().map(function (n) {
return n.toJSON(options);
})
};
if (options.preserveKeys) {
object.key = this.key;
}
return object;
}
}, {
key: 'object',
/**
* Object.
*
* @return {String}
*/
get: function get$$1() {
return 'document';
}
}], [{
key: 'create',
/**
* Create a new `Document` with `attrs`.
*
* @param {Object|Array|List|Text} attrs
* @return {Document}
*/
value: function create() {
var attrs = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
if (Document.isDocument(attrs)) {
return attrs;
}
if (immutable.List.isList(attrs) || Array.isArray(attrs)) {
attrs = { nodes: attrs };
}
if (isPlainObject(attrs)) {
return Document.fromJSON(attrs);
}
throw new Error('`Document.create` only accepts objects, arrays, lists or documents, but you passed it: ' + attrs);
}
/**
* Create a `Document` from a JSON `object`.
*
* @param {Object|Document} object
* @return {Document}
*/
}, {
key: 'fromJSON',
value: function fromJSON(object) {
if (Document.isDocument(object)) {
return object;
}
var _object$data = object.data,
data = _object$data === undefined ? {} : _object$data,
_object$key = object.key,
key = _object$key === undefined ? KeyUtils.create() : _object$key,
_object$nodes = object.nodes,
nodes = _object$nodes === undefined ? [] : _object$nodes;
var document = new Document({
key: key,
data: new immutable.Map(data),
nodes: Node.createList(nodes)
});
return document;
}
/**
* Check if `any` is a `Document`.
*
* @param {Any} any
* @return {Boolean}
*/
}]);
return Document;
}(immutable.Record(DEFAULTS));
/**
* Attach a pseudo-symbol for type checking.
*/
Document.isDocument = isType.bind(null, 'DOCUMENT');
Document.prototype[MODEL_TYPES.DOCUMENT] = true;
/**
* Default properties.
*
* @type {Object}
*/
var DEFAULTS$1 = {
data: new immutable.Map(),
key: undefined,
nodes: new immutable.List(),
type: undefined
/**
* Inline.
*
* @type {Inline}
*/
};
var Inline = function (_Record) {
inherits(Inline, _Record);
function Inline() {
classCallCheck(this, Inline);
return possibleConstructorReturn(this, (Inline.__proto__ || Object.getPrototypeOf(Inline)).apply(this, arguments));
}
createClass(Inline, [{
key: 'toJSON',
/**
* Return a JSON representation of the inline.
*
* @param {Object} options
* @return {Object}
*/
value: function toJSON() {
var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
var object = {
object: this.object,
type: this.type,
data: this.data.toJSON(),
nodes: this.nodes.toArray().map(function (n) {
return n.toJSON(options);
})
};
if (options.preserveKeys) {
object.key = this.key;
}
return object;
}
}, {
key: 'object',
/**
* Object.
*
* @return {String}
*/
get: function get$$1() {
return 'inline';
}
}], [{
key: 'create',
/**
* Create a new `Inline` with `attrs`.
*
* @param {Object|String|Inline} attrs
* @return {Inline}
*/
value: function create() {
var attrs = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
if (Inline.isInline(attrs)) {
return attrs;
}
if (typeof attrs == 'string') {
attrs = { type: attrs };
}
if (isPlainObject(attrs)) {
return Inline.fromJSON(attrs);
}
throw new Error('`Inline.create` only accepts objects, strings or inlines, but you passed it: ' + attrs);
}
/**
* Create a list of `Inlines` from an array.
*
* @param {Array<Inline|Object>|List<Inline|Object>} elements
* @return {List<Inline>}
*/
}, {
key: 'createList',
value: function createList() {
var elements = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
if (immutable.List.isList(elements) || Array.isArray(elements)) {
var list = new immutable.List(elements.map(Inline.create));
return list;
}
throw new Error('`Inline.createList` only accepts arrays or lists, but you passed it: ' + elements);
}
/**
* Create a `Inline` from a JSON `object`.
*
* @param {Object|Inline} object
* @return {Inline}
*/
}, {
key: 'fromJSON',
value: function fromJSON(object) {
if (Inline.isInline(object)) {
return object;
}
var _object$data = object.data,
data = _object$data === undefined ? {} : _object$data,
_object$key = object.key,
key = _object$key === undefined ? KeyUtils.create() : _object$key,
_object$nodes = object.nodes,
nodes = _object$nodes === undefined ? [] : _object$nodes,
type = object.type;
if (typeof type != 'string') {
throw new Error('`Inline.fromJS` requires a `type` string.');
}
var inline = new Inline({
key: key,
type: type,
data: new immutable.Map(data),
nodes: Node.createList(nodes)
});
return inline;
}
/**
* Check if `any` is a `Inline`.
*
* @param {Any} any
* @return {Boolean}
*/
}, {
key: 'isInlineList',
/**
* Check if `any` is a list of inlines.
*
* @param {Any} any
* @return {Boolean}
*/
value: function isInlineList(any) {
return immutable.List.isList(any) && any.every(function (item) {
return Inline.isInline(item);
});
}
}]);
return Inline;
}(immutable.Record(DEFAULTS$1));
/**
* Attach a pseudo-symbol for type checking.
*/
Inline.isInline = isType.bind(null, 'INLINE');
Inline.prototype[MODEL_TYPES.INLINE] = true;
/**
* Default properties.
*
* @type {Object}
*/
var DEFAULTS$2 = {
data: new immutable.Map(),
type: undefined
/**
* Mark.
*
* @type {Mark}
*/
};
var Mark = function (_Record) {
inherits(Mark, _Record);
function Mark() {
classCallCheck(this, Mark);
return possibleConstructorReturn(this, (Mark.__proto__ || Object.getPrototypeOf(Mark)).apply(this, arguments));
}
createClass(Mark, [{
key: 'toJSON',
/**
* Return a JSON representation of the mark.
*
* @return {Object}
*/
value: function toJSON() {
var object = {
object: this.object,
type: this.type,
data: this.data.toJSON()
};
return object;
}
}, {
key: 'object',
/**
* Object.
*/
get: function get$$1() {
return 'mark';
}
}], [{
key: 'create',
/**
* Create a new `Mark` with `attrs`.
*
* @param {Object|Mark} attrs
* @return {Mark}
*/
value: function create() {
var attrs = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
if (Mark.isMark(attrs)) {
return attrs;
}
if (typeof attrs == 'string') {
attrs = { type: attrs };
}
if (isPlainObject(attrs)) {
return Mark.fromJSON(attrs);
}
throw new Error('`Mark.create` only accepts objects, strings or marks, but you passed it: ' + attrs);
}
/**
* Create a set of marks.
*
* @param {Array<Object|Mark>} elements
* @return {Set<Mark>}
*/
}, {
key: 'createSet',
value: function createSet(elements) {
if (immutable.Set.isSet(elements) || Array.isArray(elements)) {
var marks = new immutable.Set(elements.map(Mark.create));
return marks;
}
if (elements == null) {
return immutable.Set();
}
throw new Error('`Mark.createSet` only accepts sets, arrays or null, but you passed it: ' + elements);
}
/**
* Create a dictionary of settable mark properties from `attrs`.
*
* @param {Object|String|Mark} attrs
* @return {Object}
*/
}, {
key: 'createProperties',
value: function createProperties() {
var attrs = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
if (Mark.isMark(attrs)) {
return {
data: attrs.data,
type: attrs.type
};
}
if (typeof attrs == 'string') {
return { type: attrs };
}
if (isPlainObject(attrs)) {
var props = {};
if ('type' in attrs) props.type = attrs.type;
if ('data' in attrs) props.data = Data.create(attrs.data);
return props;
}
throw new Error('`Mark.createProperties` only accepts objects, strings or marks, but you passed it: ' + attrs);
}
/**
* Create a `Mark` from a JSON `object`.
*
* @param {Object} object
* @return {Mark}
*/
}, {
key: 'fromJSON',
value: function fromJSON(object) {
var _object$data = object.data,
data = _object$data === undefined ? {} : _object$data,
type = object.type;
if (typeof type != 'string') {
throw new Error('`Mark.fromJS` requires a `type` string.');
}
var mark = new Mark({
type: type,
data: new immutable.Map(data)
});
return mark;
}
/**
* Check if `any` is a `Mark`.
*
* @param {Any} any
* @return {Boolean}
*/
}, {
key: 'isMarkSet',
/**
* Check if `any` is a set of marks.
*
* @param {Any} any
* @return {Boolean}
*/
value: function isMarkSet(any) {
return immutable.Set.isSet(any) && any.every(function (item) {
return Mark.isMark(item);
});
}
}]);
return Mark;
}(immutable.Record(DEFAULTS$2));
/**
* Attach a pseudo-symbol for type checking.
*/
Mark.isMark = isType.bind(null, 'MARK');
Mark.prototype[MODEL_TYPES.MARK] = true;
/**
* Default properties.
*
* @type {Object}
*/
var DEFAULTS$3 = {
marks: immutable.Set(),
text: ''
/**
* Leaf.
*
* @type {Leaf}
*/
};
var Leaf = function (_Record) {
inherits(Leaf, _Record);
function Leaf() {
classCallCheck(this, Leaf);
return possibleConstructorReturn(this, (Leaf.__proto__ || Object.getPrototypeOf(Leaf)).apply(this, arguments));
}
createClass(Leaf, [{
key: 'updateMark',
/**
* Update a `mark` at leaf, replace with newMark
*
* @param {Mark} mark
* @param {Mark} newMark
* @returns {Leaf}
*/
value: function updateMark(mark, newMark) {
var marks = this.marks;
if (newMark.equals(mark)) return this;
if (!marks.has(mark)) return this;
var newMarks = marks.withMutations(function (collection) {
collection.remove(mark).add(newMark);
});
return this.set('marks', newMarks);
}
/**
* Add a `mark` to the leaf.
*
* @param {Mark} mark
* @returns {Text}
*/
}, {
key: 'addMark',
value: function addMark(mark) {
var marks = this.marks;
return this.set('marks', marks.add(mark));
}
/**
* Add a `set` of marks to the leaf.
*
* @param {Set<Mark>} set
* @returns {Text}
*/
}, {
key: 'addMarks',
value: function addMarks(set$$1) {
var marks = this.marks;
return this.set('marks', marks.union(set$$1));
}
/**
* Remove a `mark` from the leaf.
*
* @param {Mark} mark
* @returns {Text}
*/
}, {
key: 'removeMark',
value: function removeMark(mark) {
var marks = this.marks;
return this.set('marks', marks.remove(mark));
}
/**
* Return a JSON representation of the leaf.
*
* @return {Object}
*/
}, {
key: 'toJSON',
value: function toJSON() {
var object = {
object: this.object,
text: this.text,
marks: this.marks.toArray().map(function (m) {
return m.toJSON();
})
};
return object;
}
}, {
key: 'object',
/**
* Object.
*
* @return {String}
*/
get: function get$$1() {
return 'leaf';
}
}], [{
key: 'create',
/**
* Create a new `Leaf` with `attrs`.
*
* @param {Object|Leaf} attrs
* @return {Leaf}
*/
value: function create() {
var attrs = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
if (Leaf.isLeaf(attrs)) {
return attrs;
}
if (typeof attrs == 'string') {
attrs = { text: attrs };
}
if (isPlainObject(attrs)) {
return Leaf.fromJSON(attrs);
}
throw new Error('`Leaf.create` only accepts objects, strings or leaves, but you passed it: ' + attrs);
}
/**
* Create a valid List of `Leaf` from `leaves`
*
* @param {List<Leaf>} leaves
* @return {List<Leaf>}
*/
}, {
key: 'createLeaves',
value: function createLeaves(leaves) {
if (leaves.size <= 1) return leaves;
var invalid = false;
// TODO: we can make this faster with [List] and then flatten
var result = immutable.List().withMutations(function (cache) {
// Search from the leaves left end to find invalid node;
leaves.findLast(function (leaf, index) {
var firstLeaf = cache.first();
// If the first leaf of cache exist, check whether the first leaf is connectable with the current leaf
if (firstLeaf) {
// If marks equals, then the two leaves can be connected
if (firstLeaf.marks.equals(leaf.marks)) {
invalid = true;
cache.set(0, firstLeaf.set('text', '' + leaf.text + firstLeaf.text));
return;
}
// If the cached leaf is empty, drop the empty leaf with the upcoming leaf
if (firstLeaf.text === '') {
invalid = true;
cache.set(0, leaf);
return;
}
// If the current leaf is empty, drop the leaf
if (leaf.text === '') {
invalid = true;
return;
}
}
cache.unshift(leaf);
});
});
if (!invalid) return leaves;
return result;
}
/**
* Split a list of leaves to two lists; if the leaves are valid leaves, the returned leaves are also valid
* Corner Cases:
* 1. if offset is smaller than 0, then return [List(), leaves]
* 2. if offset is bigger than the text length, then return [leaves, List()]
*
* @param {List<Leaf> leaves
* @return {Array<List<Leaf>>}
*/
}, {
key: 'splitLeaves',
value: function splitLeaves(leaves, offset) {
if (offset < 0) return [immutable.List(), leaves];
if (leaves.size === 0) {
return [immutable.List(), immutable.List()];
}
var endOffset = 0;
var index = -1;
var left = void 0,
right = void 0;
leaves.find(function (leaf) {
index++;
var startOffset = endOffset;
var text = leaf.text;
endOffset += text.length;
if (endOffset < offset) return false;
if (startOffset > offset) return false;
var length = offset - startOffset;
left = leaf.set('text', text.slice(0, length));
right = leaf.set('text', text.slice(length));
return true;
});
if (!left) return [leaves, immutable.List()];
if (left.text === '') {
if (index === 0) {
return [immutable.List.of(left), leaves];
}
return [leaves.take(index), leaves.skip(index)];
}
if (right.text === '') {
if (index === leaves.size - 1) {
return [leaves, immutable.List.of(right)];
}
return [leaves.take(index + 1), leaves.skip(index + 1)];
}
return [leaves.take(index).push(left), leaves.skip(index + 1).unshift(right)];
}
/**
* Create a `Leaf` list from `attrs`.
*
* @param {Array<Leaf|Object>|List<Leaf|Object>} attrs
* @return {List<Leaf>}
*/
}, {
key: 'createList',
value: function createList() {
var attrs = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
if (immutable.List.isList(attrs) || Array.isArray(attrs)) {
var list = new immutable.List(attrs.map(Leaf.create));
return list;
}
throw new Error('`Leaf.createList` only accepts arrays or lists, but you passed it: ' + attrs);
}
/**
* Create a `Leaf` from a JSON `object`.
*
* @param {Object} object
* @return {Leaf}
*/
}, {
key: 'fromJSON',
value: function fromJSON(object) {
var _object$text = object.text,
text = _object$text === undefined ? '' : _object$text,
_object$marks = object.marks,
marks = _object$marks === undefined ? [] : _object$marks;
var leaf = new Leaf({
text: text,
marks: immutable.Set(marks.map(Mark.fromJSON))
});
return leaf;
}
/**
* Check if `any` is a `Leaf`.
*
* @param {Any} any
* @return {Boolean}
*/
}, {
key: 'isLeafList',
/**
* Check if `any` is a list of leaves.
*
* @param {Any} any
* @return {Boolean}
*/
value: function isLeafList(any) {
return immutable.List.isList(any) && any.every(function (item) {
return Leaf.isLeaf(item);
});
}
}]);
return Leaf;
}(immutable.Record(DEFAULTS$3));
/**
* Attach a pseudo-symbol for type checking.
*/
Leaf.isLeaf = isType.bind(null, 'LEAF');
Leaf.prototype[MODEL_TYPES.LEAF] = true;
/**
* GLOBAL: True if memoization should is enabled.
*
* @type {Boolean}
*/
var ENABLED = true;
/**
* GLOBAL: Changing this cache key will clear all previous cached results.
*
* @type {Number}
*/
var CACHE_KEY = 0;
/**
* The leaf node of a cache tree. Used to support variable argument length. A
* unique object, so that native Maps will key it by reference.
*
* @type {Object}
*/
var LEAF = {};
/**
* A value to represent a memoized undefined value. Allows efficient value
* retrieval using Map.get only.
*
* @type {Object}
*/
var UNDEFINED = {};
/**
* Default value for unset keys in native Maps
*
* @type {Undefined}
*/
var UNSET = undefined;
/**
* Memoize all of the `properties` on a `object`.
*
* @param {Object} object
* @param {Array} properties
* @return {Record}
*/
function memoize(object, properties) {
var _loop = function _loop(property) {
var original = object[property];
if (!original) {
throw new Error("Object does not have a property named \"" + property + "\".");
}
object[property] = function () {
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
// If memoization is disabled, call into the original method.
if (!ENABLED) return original.apply(this, args);
// If the cache key is different, previous caches must be cleared.
if (CACHE_KEY !== this.__cache_key) {
this.__cache_key = CACHE_KEY;
this.__cache = new Map(); // eslint-disable-line no-undef,no-restricted-globals
this.__cache_no_args = {};
}
if (!this.__cache) {
this.__cache = new Map(); // eslint-disable-line no-undef,no-restricted-globals
}
if (!this.__cache_no_args) {
this.__cache_no_args = {};
}
var takesArguments = args.length !== 0;
var cachedValue = void 0;
var keys = void 0;
if (takesArguments) {
keys = [property].concat(args);
cachedValue = getIn(this.__cache, keys);
} else {
cachedValue = this.__cache_no_args[property];
}
// If we've got a result already, return it.
if (cachedValue !== UNSET) {
return cachedValue === UNDEFINED ? undefined : cachedValue;
}
// Otherwise calculate what it should be once and cache it.
var value = original.apply(this, args);
var v = value === undefined ? UNDEFINED : value;
if (takesArguments) {
this.__cache = setIn(this.__cache, keys, v);
} else {
this.__cache_no_args[property] = v;
}
return value;
};
};
var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;
try {
for (var _iterator = properties[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
var property = _step.value;
_loop(property);
}
} catch (err) {
_didIteratorError = true;
_iteratorError = err;
} finally {
try {
if (!_iteratorNormalCompletion && _iterator.return) {
_iterator.return();
}
} finally {
if (_didIteratorError) {
throw _iteratorError;
}
}
}
}
/**
* Get a value at a key path in a tree of Map.
*
* If not set, returns UNSET.
* If the set value is undefined, returns UNDEFINED.
*
* @param {Map} map
* @param {Array} keys
* @return {Any|UNSET|UNDEFINED}
*/
function getIn(map, keys) {
var _iteratorNormalCompletion2 = true;
var _didIteratorError2 = false;
var _iteratorError2 = undefined;
try {
for (var _iterator2 = keys[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
var key = _step2.value;
map = map.get(key);
if (map === UNSET) return UNSET;
}
} catch (err) {
_didIteratorError2 = true;
_iteratorError2 = err;
} finally {
try {
if (!_iteratorNormalCompletion2 && _iterator2.return) {
_iterator2.return();
}
} finally {
if (_didIteratorError2) {
throw _iteratorError2;
}
}
}
return map.get(LEAF);
}
/**
* Set a value at a key path in a tree of Map, creating Maps on the go.
*
* @param {Map} map
* @param {Array} keys
* @param {Any} value
* @return {Map}
*/
function setIn(map, keys, value) {
var parent = map;
var child = void 0;
var _iteratorNormalCompletion3 = true;
var _didIteratorError3 = false;
var _iteratorError3 = undefined;
try {
for (var _iterator3 = keys[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) {
var key = _step3.value;
child = parent.get(key);
// If the path was not created yet...
if (child === UNSET) {
child = new Map(); // eslint-disable-line no-undef,no-restricted-globals
parent.set(key, child);
}
parent = child;
}
// The whole path has been created, so set the value to the bottom most map.
} catch (err) {
_didIteratorError3 = true;
_iteratorError3 = err;
} finally {
try {
if (!_iteratorNormalCompletion3 && _iterator3.return) {
_iterator3.return();
}
} finally {
if (_didIteratorError3) {
throw _iteratorError3;
}
}
}
child.set(LEAF, value);
return map;
}
/**
* In DEV mode, clears the previously memoized values, globally.
*
* @return {Void}
*/
function resetMemoization() {
CACHE_KEY++;
if (CACHE_KEY >= Number.MAX_SAFE_INTEGER) {
CACHE_KEY = 0;
}
}
/**
* In DEV mode, enable or disable the use of memoize values, globally.
*
* @param {Boolean} enabled
* @return {Void}
*/
function useMemoization(enabled) {
ENABLED = enabled;
}
/**
* Default properties.
*
* @type {Object}
*/
var DEFAULTS$4 = {
leaves: immutable.List(),
key: undefined
/**
* Text.
*
* @type {Text}
*/
};
var Text = function (_Record) {
inherits(Text, _Record);
function Text() {
classCallCheck(this, Text);
return possibleConstructorReturn(this, (Text.__proto__ || Object.getPrototypeOf(Text)).apply(this, arguments));
}
createClass(Text, [{
key: 'searchLeafAtOffset',
/**
* Find the 'first' leaf at offset; By 'first' the alorighthm prefers `endOffset === offset` than `startOffset === offset`
* Corner Cases:
* 1. if offset is negative, return the first leaf;
* 2. if offset is larger than text length, the leaf is null, startOffset, endOffset and index is of the last leaf
*
* @param {number}
* @returns {Object}
* @property {number} startOffset
* @property {number} endOffset
* @property {number} index
* @property {Leaf} leaf
*/
value: function searchLeafAtOffset(offset) {
var endOffset = 0;
var startOffset = 0;
var index = -1;
var leaf = this.leaves.find(function (l) {
index++;
startOffset = endOffset;
endOffset = startOffset + l.text.length;
return endOffset >= offset;
});
return {
leaf: leaf,
endOffset: endOffset,
index: index,
startOffset: startOffset
};
}
/**
* Add a `mark` at `index` and `length`.
*
* @param {Number} index
* @param {Number} length
* @param {Mark} mark
* @return {Text}
*/
}, {
key: 'addMark',
value: function addMark(index, length, mark) {
var marks = immutable.Set.of(mark);
return this.addMarks(index, length, marks);
}
/**
* Add a `set` of marks at `index` and `length`.
* Corner Cases:
* 1. If empty text, and if length === 0 and index === 0, will make sure the text contain an empty leaf with the given mark.
*
* @param {Number} index
* @param {Number} length
* @param {Set<Mark>} set
* @return {Text}
*/
}, {
key: 'addMarks',
value: function addMarks(index, length, set$$1) {
if (this.text === '' && length === 0 && index === 0) {
var _leaves = this.leaves;
var first = _leaves.first();
if (!first) {
return this.set('leaves', immutable.List.of(Leaf.fromJSON({ text: '', marks: set$$1 })));
}
var newFirst = first.addMarks(set$$1);
if (newFirst === first) return this;
return this.set('leaves', immutable.List.of(newFirst));
}
if (this.text === '') return this;
if (length === 0) return this;
if (index >= this.text.length) return this;
var _Leaf$splitLeaves = Leaf.splitLeaves(this.leaves, index),
_Leaf$splitLeaves2 = slicedToArray(_Leaf$splitLeaves, 2),
before = _Leaf$splitLeaves2[0],
bundle = _Leaf$splitLeaves2[1];
var _Leaf$splitLeaves3 = Leaf.splitLeaves(bundle, length),
_Leaf$splitLeaves4 = slicedToArray(_Leaf$splitLeaves3, 2),
middle = _Leaf$splitLeaves4[0],
after = _Leaf$splitLeaves4[1];
var leaves = before.concat(middle.map(function (x) {
return x.addMarks(set$$1);
}), after);
return this.setLeaves(leaves);
}
/**
* Derive the leaves for a list of `decorations`.
*
* @param {Array|Void} decorations (optional)
* @return {List<Leaf>}
*/
}, {
key: 'getLeaves',
value: function getLeaves() {
var _this2 = this;
var decorations = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
var leaves = this.leaves;
if (leaves.size === 0) return immutable.List.of(Leaf.create({}));
if (!decorations || decorations.length === 0) return leaves;
if (this.text.length === 0) return leaves;
var key = this.key;
decorations.forEach(function (dec) {
var start = dec.start,
end = dec.end,
mark = dec.mark;
var hasStart = start.key == key;
var hasEnd = end.key == key;
if (hasStart && hasEnd) {
var index = hasStart ? start.offset : 0;
var length = hasEnd ? end.offset - index : _this2.text.length - index;
if (length < 1) return;
if (index >= _this2.text.length) return;
if (index !== 0 || length < _this2.text.length) {
var _Leaf$splitLeaves5 = Leaf.splitLeaves(leaves, index),
_Leaf$splitLeaves6 = slicedToArray(_Leaf$splitLeaves5, 2),
before = _Leaf$splitLeaves6[0],
bundle = _Leaf$splitLeaves6[1];
var _Leaf$splitLeaves7 = Leaf.splitLeaves(bundle, length),
_Leaf$splitLeaves8 = slicedToArray(_Leaf$splitLeaves7, 2),
middle = _Leaf$splitLeaves8[0],
after = _Leaf$splitLeaves8[1];
leaves = before.concat(middle.map(function (x) {
return x.addMark(mark);
}), after);
return;
}
}
leaves = leaves.map(function (x) {
return x.addMark(mark);
});
});
if (leaves === this.leaves) return leaves;
return Leaf.createLeaves(leaves);
}
/**
* Get all of the active marks on between two offsets
* Corner Cases:
* 1. if startOffset is equal or bigger than endOffset, then return Set();
* 2. If no text is selected between start and end, then return Set()
*
* @return {Set<Mark>}
*/
}, {
key: 'getActiveMarksBetweenOffsets',
value: function getActiveMarksBetweenOffsets(startOffset, endOffset) {
if (startOffset <= 0 && endOffset >= this.text.length) {
return this.getActiveMarks();
}
if (startOffset >= endOffset) return immutable.Set();
// For empty text in a paragraph, use getActiveMarks;
if (this.text === '') return this.getActiveMarks();
var result = null;
var leafEnd = 0;
this.leaves.forEach(function (leaf) {
var leafStart = leafEnd;
leafEnd = leafStart + leaf.text.length;
if (leafEnd <= startOffset) return;
if (leafStart >= endOffset) return false;
if (!result) {
result = leaf.marks;
return;
}
result = result.intersect(leaf.marks);
if (result && result.size === 0) return false;
return false;
});
return result || immutable.Set();
}
/**
* Get all of the active marks on the text
*
* @return {Set<Mark>}
*/
}, {
key: 'getActiveMarks',
value: function getActiveMarks() {
var _this3 = this;
if (this.leaves.size === 0) return immutable.Set();
var result = this.leaves.first().marks;
if (result.size === 0) return result;
return result.withMutations(function (x) {
_this3.leaves.forEach(function (c) {
x.intersect(c.marks);
if (x.size === 0) return false;
});
});
}
/**
* Get all of the marks on between two offsets
* Corner Cases:
* 1. if startOffset is equal or bigger than endOffset, then return Set();
* 2. If no text is selected between start and end, then return Set()
*
* @return {OrderedSet<Mark>}
*/
}, {
key: 'getMarksBetweenOffsets',
value: function getMarksBetweenOffsets(startOffset, endOffset) {
if (startOffset <= 0 && endOffset >= this.text.length) {
return this.getMarks();
}
if (startOffset >= endOffset) return immutable.Set();
// For empty text in a paragraph, use getActiveMarks;
if (this.text === '') return this.getActiveMarks();
var result = null;
var leafEnd = 0;
this.leaves.forEach(function (leaf) {
var leafStart = leafEnd;
leafEnd = leafStart + leaf.text.length;
if (leafEnd <= startOffset) return;
if (leafStart >= endOffset) return false;
if (!result) {
result = leaf.marks;
return;
}
result = result.union(leaf.marks);
});
return result || immutable.Set();
}
/**
* Get all of the marks on the text.
*
* @return {OrderedSet<Mark>}
*/
}, {
key: 'getMarks',
value: function getMarks() {
var array = this.getMarksAsArray();
return new immutable.OrderedSet(array);
}
/**
* Get all of the marks on the text as an array
*
* @return {Array}
*/
}, {
key: 'getMarksAsArray',
value: function getMarksAsArray() {
if (this.leaves.size === 0) return [];
var first = this.leaves.first().marks;
if (this.leaves.size === 1) return first.toArray();
var result = [];
this.leaves.forEach(function (leaf) {
result.push(leaf.marks.toArray());
});
return Array.prototype.concat.apply(first.toArray(), result);
}
/**
* Get the marks on the text at `index`.
* Corner Cases:
* 1. if no text is before the index, and index !== 0, then return Set()
* 2. (for insert after split node or mark at range) if index === 0, and text === '', then return the leaf.marks
* 3. if index === 0, text !== '', return Set()
*
*
* @param {Number} index
* @return {Set<Mark>}
*/
}, {
key: 'getMarksAtIndex',
value: function getMarksAtIndex(index) {
var _searchLeafAtOffset = this.searchLeafAtOffset(index),
leaf = _searchLeafAtOffset.leaf;
if (!leaf) return immutable.Set();
return leaf.marks;
}
/**
* Insert `text` at `index`.
*
* @param {Numbder} offset
* @param {String} text
* @param {Set} marks (optional)
* @return {Text}
*/
}, {
key: 'insertText',
value: function insertText(offset, text, marks) {
if (this.text === '') {
return this.set('leaves', immutable.List.of(Leaf.create({ text: text, marks: marks })));
}
if (text.length === 0) return this;
if (!marks) marks = immutable.Set();
var _searchLeafAtOffset2 = this.searchLeafAtOffset(offset),
startOffset = _searchLeafAtOffset2.startOffset,
leaf = _searchLeafAtOffset2.leaf,
index = _searchLeafAtOffset2.index;
var delta = offset - startOffset;
var beforeText = leaf.text.slice(0, delta);
var afterText = leaf.text.slice(delta);
var leaves = this.leaves;
if (leaf.marks.equals(marks)) {
return this.set('leaves', leaves.set(index, leaf.set('text', beforeText + text + afterText)));
}
var nextLeaves = leaves.splice(index, 1, leaf.set('text', beforeText), Leaf.create({ text: text, marks: marks }), leaf.set('text', afterText));
return this.setLeaves(nextLeaves);
}
/**
* Remove a `mark` at `index` and `length`.
*
* @param {Number} index
* @param {Number} length
* @param {Mark} mark
* @return {Text}
*/
}, {
key: 'removeMark',
value: function removeMark(index, length, mark) {
if (this.text === '' && index === 0 && length === 0) {
var first = this.leaves.first();
if (!first) return this;
var newFirst = first.removeMark(mark);
if (newFirst === first) return this;
return this.set('leaves', immutable.List.of(newFirst));
}
if (length <= 0) return this;
if (index >= this.text.length) return this;
var _Leaf$splitLeaves9 = Leaf.splitLeaves(this.leaves, index),
_Leaf$splitLeaves10 = slicedToArray(_Leaf$splitLeaves9, 2),
before = _Leaf$splitLeaves10[0],
bundle = _Leaf$splitLeaves10[1];
var _Leaf$splitLeaves11 = Leaf.splitLeaves(bundle, length),
_Leaf$splitLeaves12 = slicedToArray(_Leaf$splitLeaves11, 2),
middle = _Leaf$splitLeaves12[0],
after = _Leaf$splitLeaves12[1];
var leaves = before.concat(middle.map(function (x) {
return x.removeMark(mark);
}), after);
return this.setLeaves(leaves);
}
/**
* Remove text from the text node at `start` for `length`.
*
* @param {Number} start
* @param {Number} length
* @return {Text}
*/
}, {
key: 'removeText',
value: function removeText(start, length) {
if (length <= 0) return this;
if (start >= this.text.length) return this;
// PERF: For simple backspace, we can operate directly on the leaf
if (length === 1) {
var _searchLeafAtOffset3 = this.searchLeafAtOffset(start + 1),
leaf = _searchLeafAtOffset3.leaf,
index = _searchLeafAtOffset3.index,
startOffset = _searchLeafAtOffset3.startOffset;
var offset = start - startOffset;
if (leaf) {
if (leaf.text.length === 1) {
var _leaves2 = this.leaves.remove(index);
return this.setLeaves(_leaves2);
}
var beforeText = leaf.text.slice(0, offset);
var afterText = leaf.text.slice(offset + length);
var text = beforeText + afterText;
if (text.length > 0) {
return this.set('leaves', this.leaves.set(index, leaf.set('text', text)));
}
}
}
var _Leaf$splitLeaves13 = Leaf.splitLeaves(this.leaves, start),
_Leaf$splitLeaves14 = slicedToArray(_Leaf$splitLeaves13, 2),
before = _Leaf$splitLeaves14[0],
bundle = _Leaf$splitLeaves14[1];
var after = Leaf.splitLeaves(bundle, length)[1];
var leaves = Leaf.createLeaves(before.concat(after));
if (leaves.size === 1) {
var first = leaves.first();
if (first.text === '') {
return this.set('leaves', immutable.List.of(first.set('marks', this.getActiveMarks())));
}
}
return this.set('leaves', leaves);
}
/**
* Return a JSON representation of the text.
*
* @param {Object} options
* @return {Object}
*/
}, {
key: 'toJ