UNPKG

substance

Version:

Substance is a JavaScript library for web-based content editing. It provides building blocks for realizing custom text editors and web-based publishing systems.

423 lines (372 loc) 10.1 kB
import { isArray, isString, forEach, EventEmitter } from '../util' /* A data storage implemention that supports data defined via a {@link Schema}, and incremental updates which are backed by a OT library. It forms the underlying implementation for {@link Document}. */ class Data extends EventEmitter { /** @param {Schema} schema @param {Object} [options] */ constructor(schema, nodeFactory) { super() /* istanbul ignore start */ if (!schema) { throw new Error('schema is mandatory') } if (!nodeFactory) { throw new Error('nodeFactory is mandatory') } /* istanbul ignore end */ this.schema = schema this.nodeFactory = nodeFactory this.nodes = {} this.indexes = {} // Sometimes necessary to resolve issues with updating indexes in presence // of cyclic dependencies this.__QUEUE_INDEXING__ = false this.queue = [] } /** Check if this storage contains a node with given id. @returns {bool} `true` if a node with id exists, `false` otherwise. */ contains(id) { return Boolean(this.nodes[id]) } /** Get a node or value via path. @param {String|String[]} path node id or path to property. @returns {Node|Object|Primitive|undefined} a Node instance, a value or undefined if not found. */ get(path, strict) { let result = this._get(path) if (strict && result === undefined) { if (isString(path)) { throw new Error("Could not find node with id '"+path+"'.") } else if (!this.contains(path[0])) { throw new Error("Could not find node with id '"+path[0]+"'.") } else { throw new Error("Property for path '"+path+"' us undefined.") } } return result } _get(path) { if (!path) return undefined let result if (isString(path)) { result = this.nodes[path] } else if (path.length === 1) { result = this.nodes[path[0]] } else if (path.length > 1) { let context = this.nodes[path[0]] for (let i = 1; i < path.length-1; i++) { if (!context) return undefined context = context[path[i]] } if (!context) return undefined result = context[path[path.length-1]] } return result } /** Get the internal storage for nodes. @return The internal node storage. */ getNodes() { return this.nodes } /** Create a node from the given data. @return {Node} The created node. */ create(nodeData) { var node = this.nodeFactory.create(nodeData.type, nodeData) if (!node) { throw new Error('Illegal argument: could not create node for data:', nodeData) } if (this.contains(node.id)) { throw new Error("Node already exists: " + node.id) } if (!node.id || !node.type) { throw new Error("Node id and type are mandatory.") } this.nodes[node.id] = node var change = { type: 'create', node: node } if (this.__QUEUE_INDEXING__) { this.queue.push(change) } else { this._updateIndexes(change) } return node } /** Delete the node with given id. @param {String} nodeId @returns {Node} The deleted node. */ delete(nodeId) { var node = this.nodes[nodeId] if (!node) return node.dispose() delete this.nodes[nodeId] var change = { type: 'delete', node: node, } if (this.__QUEUE_INDEXING__) { this.queue.push(change) } else { this._updateIndexes(change) } return node } /** Set a property to a new value. @param {Array} property path @param {Object} newValue @returns {Node} The deleted node. */ set(path, newValue) { let node = this.get(path[0]) let oldValue = this._set(path, newValue) var change = { type: 'set', node: node, path: path, newValue: newValue, oldValue: oldValue } if (this.__QUEUE_INDEXING__) { this.queue.push(change) } else { this._updateIndexes(change) } return oldValue } _set(path, newValue) { let oldValue = _setValue(this.nodes, path, newValue) return oldValue } /** Update a property incrementally. @param {Array} property path @param {Object} diff @returns {any} The value before applying the update. */ update(path, diff) { var realPath = this.getRealPath(path) if (!realPath) { console.error('Could not resolve path', path) return } let node = this.get(realPath[0]) let oldValue = this._get(realPath) let newValue if (diff.isOperation) { newValue = diff.apply(oldValue) } else { diff = this._normalizeDiff(oldValue, diff) if (isString(oldValue)) { switch (diff.type) { case 'delete': { newValue = oldValue.split('').splice(diff.start, diff.end-diff.start).join('') break } case 'insert': { newValue = [oldValue.substring(0, diff.start), diff.text, oldValue.substring(diff.start)].join('') break } default: throw new Error('Unknown diff type') } } else if (isArray(oldValue)) { newValue = oldValue.slice(0) switch (diff.type) { case 'delete': { newValue.splice(diff.pos, 1) break } case 'insert': { newValue.splice(diff.pos, 0, diff.value) break } default: throw new Error('Unknown diff type') } } else if (oldValue._isCoordinate) { switch (diff.type) { case 'shift': { // ATTENTION: in this case we do not want to create a new value oldValue = { path: oldValue.path, offset: oldValue.offset } newValue = oldValue newValue.offset += diff.value break } default: throw new Error('Unknown diff type') } } else { throw new Error('Diff is not supported:', JSON.stringify(diff)) } } this._set(realPath, newValue) var change = { type: 'update', node: node, path: realPath, newValue: newValue, oldValue: oldValue } if (this.__QUEUE_INDEXING__) { this.queue.push(change) } else { this._updateIndexes(change) } return oldValue } // normalize to support legacy formats _normalizeDiff(value, diff) { if (isString(value)) { // legacy if (diff['delete']) { console.warn('DEPRECATED: use doc.update(path, {type:"delete", start:s, end: e}) instead') diff = { type: 'delete', start: diff['delete'].start, end: diff['delete'].end } } else if (diff['insert']) { console.warn('DEPRECATED: use doc.update(path, {type:"insert", start:s, text: t}) instead') diff = { type: 'insert', start: diff['insert'].offset, text: diff['insert'].value } } } else if (isArray(value)) { // legacy if (diff['delete']) { console.warn('DEPRECATED: use doc.update(path, {type:"delete", pos:1}) instead') diff = { type: 'delete', pos: diff['delete'].offset } } else if (diff['insert']) { console.warn('DEPRECATED: use doc.update(path, {type:"insert", pos:1, value: "foo"}) instead') diff = { type: 'insert', pos: diff['insert'].offset, value: diff['insert'].value } } } else if (value._isCoordinate) { if (diff.hasOwnProperty('shift')) { console.warn('DEPRECATED: use doc.update(path, {type:"shift", value:2}) instead') diff = { type: 'shift', value: diff['shift'] } } } return diff } /* DEPRECATED: We moved away from having JSON as first-class exchange format. We will remove this soon. @internal @deprecated */ toJSON() { let nodes = {} forEach(this.nodes, (node)=>{ nodes[node.id] = node.toJSON() }) return { schema: [this.schema.id, this.schema.version], nodes: nodes } } reset() { this.clear() } /** Clear nodes. @internal */ clear() { this.nodes = {} forEach(this.indexes, index => index.clear()) } /** Add a node index. @param {String} name @param {NodeIndex} index */ addIndex(name, index) { if (this.indexes[name]) { console.error('Index with name %s already exists.', name) } index.reset(this) this.indexes[name] = index return index } /** Get the node index with given name. @param {String} name @returns {NodeIndex} The node index. */ getIndex(name) { return this.indexes[name] } /** Update a node index by providing of change object. @param {Object} change */ _updateIndexes(change) { if (!change || this.__QUEUE_INDEXING__) return forEach(this.indexes, function(index) { if (index.select(change.node)) { if (!index[change.type]) { console.error('Contract: every NodeIndex must implement ' + change.type) } index[change.type](change.node, change.path, change.newValue, change.oldValue) } }) } /** Stops indexing process, all changes will be collected in indexing queue. @private */ _stopIndexing() { this.__QUEUE_INDEXING__ = true } /** Update all index changes from indexing queue. @private */ _startIndexing() { this.__QUEUE_INDEXING__ = false while(this.queue.length >0) { var change = this.queue.shift() this._updateIndexes(change) } } } function _setValue(root, path, newValue) { let ctx = root let L = path.length for (let i = 0; i < L-1; i++) { ctx = ctx[path[i]] if (!ctx) throw new Error('Can not set value.') } let oldValue = ctx[path[L-1]] ctx[path[L-1]] = newValue return oldValue } export default Data