UNPKG

slate

Version:

A completely customizable framework for building rich text editors.

2,141 lines (1,695 loc) 489 kB
(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