/** * @module types */ import { Type } from '../structs/Type.mjs' import { ItemJSON } from '../structs/ItemJSON.mjs' import { ItemString } from '../structs/ItemString.mjs' import { stringifyItemID, logItemHelper } from '../protocols/syncProtocol.mjs' import { YEvent } from '../utils/YEvent.mjs' import { Transaction } from '../utils/Transaction.mjs' // eslint-disable-line import { Item } from '../structs/Item.mjs' // eslint-disable-line /** * Event that describes the changes on a YArray */ export class YArrayEvent extends YEvent { /** * @param {YArray} yarray The changed type * @param {Boolean} remote Whether the changed was caused by a remote peer * @param {Transaction} transaction The transaction object */ constructor (yarray, remote, transaction) { super(yarray) this.remote = remote this._transaction = transaction this._addedElements = null this._removedElements = null } /** * Child elements that were added in this transaction. * * @return {Set} */ get addedElements () { if (this._addedElements === null) { const target = this.target const transaction = this._transaction const addedElements = new Set() transaction.newTypes.forEach(type => { if (type._parent === target && !transaction.deletedStructs.has(type)) { addedElements.add(type) } }) this._addedElements = addedElements } return this._addedElements } /** * Child elements that were removed in this transaction. * * @return {Set} */ get removedElements () { if (this._removedElements === null) { const target = this.target const transaction = this._transaction const removedElements = new Set() transaction.deletedStructs.forEach(struct => { if (struct._parent === target && !transaction.newTypes.has(struct)) { removedElements.add(struct) } }) this._removedElements = removedElements } return this._removedElements } } /** * A shared Array implementation. */ export class YArray extends Type { /** * Creates YArray Event and calls observers. * * @private */ _callObserver (transaction, parentSubs, remote) { this._callEventHandler(transaction, new YArrayEvent(this, remote, transaction)) } /** * Returns the i-th element from a YArray. * * @param {number} index The index of the element to return from the YArray */ get (index) { let n = this._start while (n !== null) { if (!n._deleted && n._countable) { if (index < n._length) { if (n.constructor === ItemJSON || n.constructor === ItemString) { return n._content[index] } else { return n } } index -= n._length } n = n._right } } /** * Transforms this YArray to a JavaScript Array. * * @return {Array} */ toArray () { return this.map(c => c) } /** * Transforms this Shared Type to a JSON object. * * @return {Array} */ toJSON () { return this.map(c => { if (c instanceof Type) { return c.toJSON() } return c }) } /** * Returns an Array with the result of calling a provided function on every * element of this YArray. * * @param {Function} f Function that produces an element of the new Array * @return {Array} A new array with each element being the result of the * callback function */ map (f) { const res = [] this.forEach((c, i) => { res.push(f(c, i, this)) }) return res } /** * Executes a provided function on once on overy element of this YArray. * * @param {Function} f A function to execute on every element of this YArray. */ forEach (f) { let index = 0 let n = this._start while (n !== null) { if (!n._deleted && n._countable) { if (n instanceof Type) { f(n, index++, this) } else { const content = n._content const contentLen = content.length for (let i = 0; i < contentLen; i++) { index++ f(content[i], index, this) } } } n = n._right } } /** * Computes the length of this YArray. */ get length () { let length = 0 let n = this._start while (n !== null) { if (!n._deleted && n._countable) { length += n._length } n = n._right } return length } [Symbol.iterator] () { return { next: function () { while (this._item !== null && (this._item._deleted || this._item._length <= this._itemElement)) { // item is deleted or itemElement does not exist (is deleted) this._item = this._item._right this._itemElement = 0 } if (this._item === null) { return { done: true } } let content if (this._item instanceof Type) { content = this._item } else { content = this._item._content[this._itemElement++] } return { value: content, done: false } }, _item: this._start, _itemElement: 0, _count: 0 } } /** * Deletes elements starting from an index. * * @param {number} index Index at which to start deleting elements * @param {number} length The number of elements to remove. Defaults to 1. */ delete (index, length = 1) { this._y.transact(() => { let item = this._start let count = 0 while (item !== null && length > 0) { if (!item._deleted && item._countable) { if (count <= index && index < count + item._length) { const diffDel = index - count item = item._splitAt(this._y, diffDel) item._splitAt(this._y, length) length -= item._length item._delete(this._y) count += diffDel } else { count += item._length } } item = item._right } }) if (length > 0) { throw new Error('Delete exceeds the range of the YArray') } } /** * Inserts content after an element container. * * @private * @param {Item} left The element container to use as a reference. * @param {Array} content The Array of content to insert (see {@see insert}) */ insertAfter (left, content) { this._transact(y => { let right if (left === null) { right = this._start } else { right = left._right } let prevJsonIns = null for (let i = 0; i < content.length; i++) { let c = content[i] if (typeof c === 'function') { c = new c() // eslint-disable-line new-cap } if (c instanceof Type) { if (prevJsonIns !== null) { if (y !== null) { prevJsonIns._integrate(y) } left = prevJsonIns prevJsonIns = null } c._origin = left c._left = left c._right = right c._right_origin = right c._parent = this if (y !== null) { c._integrate(y) } else if (left === null) { this._start = c } else { left._right = c } left = c } else { if (prevJsonIns === null) { prevJsonIns = new ItemJSON() prevJsonIns._origin = left prevJsonIns._left = left prevJsonIns._right = right prevJsonIns._right_origin = right prevJsonIns._parent = this prevJsonIns._content = [] } prevJsonIns._content.push(c) } } if (prevJsonIns !== null) { if (y !== null) { prevJsonIns._integrate(y) } else if (prevJsonIns._left === null) { this._start = prevJsonIns } } }) return content } /** * Inserts new content at an index. * * Important: This function expects an array of content. Not just a content * object. The reason for this "weirdness" is that inserting several elements * is very efficient when it is done as a single operation. * * @example * // Insert character 'a' at position 0 * yarray.insert(0, ['a']) * // Insert numbers 1, 2 at position 1 * yarray.insert(2, [1, 2]) * * @param {number} index The index to insert content at. * @param {Array} content The array of content */ insert (index, content) { this._transact(() => { let left = null let right = this._start let count = 0 const y = this._y while (right !== null) { const rightLen = right._deleted ? 0 : (right._length - 1) if (count <= index && index <= count + rightLen) { const splitDiff = index - count right = right._splitAt(y, splitDiff) left = right._left count += splitDiff break } if (!right._deleted) { count += right._length } left = right right = right._right } if (index > count) { throw new Error('Index exceeds array range!') } this.insertAfter(left, content) }) } /** * Appends content to this YArray. * * @param {Array} content Array of content to append. */ push (content) { let n = this._start let lastUndeleted = null while (n !== null) { if (!n._deleted) { lastUndeleted = n } n = n._right } this.insertAfter(lastUndeleted, content) } /** * Transform this YXml Type to a readable format. * Useful for logging as all Items and Delete implement this method. * * @private */ _logString () { return logItemHelper('YArray', this, `start:${stringifyItemID(this._start)}"`) } }