Files
yjs/types/YArray.mjs

383 lines
9.7 KiB
JavaScript
Raw Normal View History

/**
2018-11-25 03:17:00 +01:00
* @module types
*/
2018-11-25 22:39:30 +01:00
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
2018-11-25 03:17:00 +01:00
/**
* Event that describes the changes on a YArray
*/
2018-02-26 02:18:39 +01:00
export class YArrayEvent extends YEvent {
2018-11-25 05:43:18 +01:00
/**
* @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
2018-01-25 17:28:33 -07:00
this._addedElements = null
2018-02-26 02:18:39 +01:00
this._removedElements = null
}
/**
* Child elements that were added in this transaction.
*
* @return {Set}
*/
get addedElements () {
2018-01-25 17:28:33 -07:00
if (this._addedElements === null) {
const target = this.target
const transaction = this._transaction
const addedElements = new Set()
2018-11-25 03:17:00 +01:00
transaction.newTypes.forEach(type => {
2018-01-25 17:28:33 -07:00
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 () {
2018-02-26 02:18:39 +01:00
if (this._removedElements === null) {
const target = this.target
const transaction = this._transaction
const removedElements = new Set()
2018-11-25 03:17:00 +01:00
transaction.deletedStructs.forEach(struct => {
2018-02-26 02:18:39 +01:00
if (struct._parent === target && !transaction.newTypes.has(struct)) {
removedElements.add(struct)
}
})
this._removedElements = removedElements
}
return this._removedElements
}
}
/**
* A shared Array implementation.
*/
2018-11-25 03:17:00 +01:00
export class YArray extends Type {
/**
* Creates YArray Event and calls observers.
2018-11-25 22:39:30 +01:00
*
* @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) {
2018-02-15 01:25:08 +01:00
if (!n._deleted && n._countable) {
if (index < n._length) {
2017-11-10 12:54:33 -08:00
if (n.constructor === ItemJSON || n.constructor === ItemString) {
return n._content[index]
2017-11-10 12:54:33 -08:00
} 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}
*/
2017-10-16 04:53:12 +02:00
toJSON () {
return this.map(c => {
if (c instanceof Type) {
return c.toJSON()
2017-10-16 04:53:12 +02:00
}
return c
2017-10-16 04:53:12 +02:00
})
}
/**
* 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
*/
2017-10-16 04:53:12 +02:00
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.
*/
2017-10-11 03:41:54 +02:00
forEach (f) {
let index = 0
2017-10-11 03:41:54 +02:00
let n = this._start
while (n !== null) {
2018-02-15 01:25:08 +01:00
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)
}
2017-10-11 03:41:54 +02:00
}
}
n = n._right
}
}
/**
* Computes the length of this YArray.
*/
2017-10-16 04:53:12 +02:00
get length () {
let length = 0
let n = this._start
while (n !== null) {
2018-02-15 01:25:08 +01:00
if (!n._deleted && n._countable) {
2017-10-16 04:53:12 +02:00
length += n._length
}
n = n._right
2017-10-16 04:53:12 +02:00
}
return length
}
2017-10-11 03:41:54 +02:00
[Symbol.iterator] () {
return {
next: function () {
while (this._item !== null && (this._item._deleted || this._item._length <= this._itemElement)) {
2017-10-11 03:41:54 +02:00
// 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
2017-10-11 03:41:54 +02:00
} else {
content = this._item._content[this._itemElement++]
}
return {
2018-04-27 18:33:28 +02:00
value: content,
done: false
2017-10-11 03:41:54 +02:00
}
},
_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) {
2018-02-15 01:25:08 +01:00
if (!item._deleted && item._countable) {
if (count <= index && index < count + item._length) {
const diffDel = index - count
2017-10-26 16:22:35 +02:00
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
2017-10-16 04:53:12 +02:00
}
})
if (length > 0) {
throw new Error('Delete exceeds the range of the YArray')
}
}
/**
* Inserts content after an element container.
*
2018-11-25 22:39:30 +01:00
* @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) {
2017-10-27 22:28:32 +02:00
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
2017-10-27 22:28:32 +02:00
} 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)
}
}
2017-12-19 17:37:04 +01:00
if (prevJsonIns !== null) {
if (y !== null) {
prevJsonIns._integrate(y)
} else if (prevJsonIns._left === null) {
this._start = prevJsonIns
}
2017-10-16 04:53:12 +02:00
}
2017-10-27 22:28:32 +02:00
})
2018-03-23 01:55:47 +01:00
return content
2017-10-16 04:53:12 +02:00
}
/**
* 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) {
2018-02-26 03:23:22 +01:00
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
2018-02-26 03:23:22 +01:00
right = right._splitAt(y, splitDiff)
left = right._left
count += splitDiff
break
}
if (!right._deleted) {
count += right._length
}
left = right
right = right._right
2017-10-11 03:41:54 +02:00
}
if (index > count) {
throw new Error('Index exceeds array range!')
}
2018-02-26 03:23:22 +01:00
this.insertAfter(left, content)
})
2017-10-11 03:41:54 +02:00
}
/**
* 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)
}
/**
2018-03-29 11:58:02 +02:00
* Transform this YXml Type to a readable format.
* Useful for logging as all Items and Delete implement this method.
*
* @private
*/
2017-10-11 03:41:54 +02:00
_logString () {
return logItemHelper('YArray', this, `start:${stringifyItemID(this._start)}"`)
2017-10-11 03:41:54 +02:00
}
}