mirror of
https://github.com/yjs/yjs.git
synced 2026-02-23 19:49:59 +01:00
more refactoring
This commit is contained in:
2
package-lock.json
generated
2
package-lock.json
generated
@@ -30,7 +30,7 @@
|
||||
}
|
||||
},
|
||||
"../lib0": {
|
||||
"version": "0.2.115",
|
||||
"version": "0.2.116",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"0ecdsa-generate-keypair": "src/bin/0ecdsa-generate-keypair.js",
|
||||
|
||||
10
src/index.js
10
src/index.js
@@ -3,13 +3,7 @@
|
||||
export {
|
||||
Doc,
|
||||
Transaction,
|
||||
YArray as Array,
|
||||
YMap as Map,
|
||||
YText as Text,
|
||||
YXmlText as XmlText,
|
||||
YXmlHook as XmlHook,
|
||||
YXmlElement as XmlElement,
|
||||
YXmlFragment as XmlFragment,
|
||||
YType as Type,
|
||||
YEvent,
|
||||
Item,
|
||||
AbstractStruct,
|
||||
@@ -46,7 +40,6 @@ export {
|
||||
getItem,
|
||||
getItemCleanStart,
|
||||
getItemCleanEnd,
|
||||
typeListToArraySnapshot,
|
||||
typeMapGetSnapshot,
|
||||
typeMapGetAllSnapshot,
|
||||
createDocFromSnapshot,
|
||||
@@ -72,7 +65,6 @@ export {
|
||||
equalSnapshots,
|
||||
tryGc,
|
||||
transact,
|
||||
AbstractConnector,
|
||||
logType,
|
||||
mergeUpdates,
|
||||
mergeUpdatesV2,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from './utils/AbstractConnector.js'
|
||||
export * from './utils/IdSet.js'
|
||||
export * from './utils/Doc.js'
|
||||
export * from './utils/UpdateDecoder.js'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {
|
||||
YText, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Item, StructStore, Transaction // eslint-disable-line
|
||||
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Item, Transaction // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as error from 'lib0/error'
|
||||
@@ -67,7 +67,7 @@ export class ContentFormat {
|
||||
*/
|
||||
integrate (_transaction, item) {
|
||||
// @todo searchmarker are currently unsupported for rich text documents
|
||||
const p = /** @type {YText<any>} */ (item.parent)
|
||||
const p = /** @type {import('../ytype.js').YType<any>} */ (item.parent)
|
||||
p._searchMarker = null
|
||||
p._hasFormatting = true
|
||||
}
|
||||
|
||||
@@ -1,33 +1,9 @@
|
||||
import {
|
||||
readYArray,
|
||||
readYMap,
|
||||
readYText,
|
||||
readYXmlElement,
|
||||
readYXmlFragment,
|
||||
readYXmlHook,
|
||||
readYXmlText,
|
||||
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Transaction, Item // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
/**
|
||||
* @typedef {import('../utils/types.js').YType} YType_CT
|
||||
*/
|
||||
|
||||
import * as error from 'lib0/error'
|
||||
|
||||
/**
|
||||
* @type {Array<(decoder: UpdateDecoderV1 | UpdateDecoderV2)=>(import('../utils/types.js').YType)>}
|
||||
* @private
|
||||
*/
|
||||
export const typeRefs = [
|
||||
readYArray,
|
||||
readYMap,
|
||||
readYText,
|
||||
readYXmlElement,
|
||||
readYXmlFragment,
|
||||
readYXmlHook,
|
||||
readYXmlText
|
||||
]
|
||||
import { readYType } from '../ytype.js'
|
||||
|
||||
export const YArrayRefID = 0
|
||||
export const YMapRefID = 1
|
||||
@@ -42,11 +18,11 @@ export const YXmlTextRefID = 6
|
||||
*/
|
||||
export class ContentType {
|
||||
/**
|
||||
* @param {YType_CT} type
|
||||
* @param {import('../ytype.js').YType} type
|
||||
*/
|
||||
constructor (type) {
|
||||
/**
|
||||
* @type {YType_CT}
|
||||
* @type {import('../ytype.js').YType}
|
||||
*/
|
||||
this.type = type
|
||||
}
|
||||
@@ -173,4 +149,4 @@ export class ContentType {
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||
* @return {ContentType}
|
||||
*/
|
||||
export const readContentType = decoder => new ContentType(typeRefs[decoder.readTypeRef()](decoder))
|
||||
export const readContentType = decoder => new ContentType(readYType(decoder))
|
||||
|
||||
@@ -29,10 +29,6 @@ import * as error from 'lib0/error'
|
||||
import * as binary from 'lib0/binary'
|
||||
import * as array from 'lib0/array'
|
||||
|
||||
/**
|
||||
* @typedef {import('../utils/types.js').YType} YType__
|
||||
*/
|
||||
|
||||
/**
|
||||
* @todo This should return several items
|
||||
*
|
||||
@@ -72,7 +68,7 @@ export const followRedone = (store, id) => {
|
||||
export const keepItem = (item, keep) => {
|
||||
while (item !== null && item.keep !== keep) {
|
||||
item.keep = keep
|
||||
item = /** @type {YType__} */ (item.parent)._item
|
||||
item = /** @type {YType} */ (item.parent)._item
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +115,7 @@ export const splitItem = (transaction, leftItem, diff) => {
|
||||
transaction._mergeStructs.push(rightItem)
|
||||
// update parent._map
|
||||
if (rightItem.parentSub !== null && rightItem.right === null) {
|
||||
/** @type {YType__} */ (rightItem.parent)._map.set(rightItem.parentSub, rightItem)
|
||||
/** @type {YType} */ (rightItem.parent)._map.set(rightItem.parentSub, rightItem)
|
||||
}
|
||||
} else {
|
||||
rightItem.left = null
|
||||
@@ -177,7 +173,7 @@ export const redoItem = (transaction, item, redoitems, itemsToDelete, ignoreRemo
|
||||
if (redone !== null) {
|
||||
return getItemCleanStart(transaction, redone)
|
||||
}
|
||||
let parentItem = /** @type {YType__} */ (item.parent)._item
|
||||
let parentItem = /** @type {YType} */ (item.parent)._item
|
||||
/**
|
||||
* @type {Item|null}
|
||||
*/
|
||||
@@ -197,9 +193,9 @@ export const redoItem = (transaction, item, redoitems, itemsToDelete, ignoreRemo
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @type {YType__}
|
||||
* @type {YType}
|
||||
*/
|
||||
const parentType = /** @type {YType__} */ (parentItem === null ? item.parent : /** @type {ContentType} */ (parentItem.content).type)
|
||||
const parentType = /** @type {YType} */ (parentItem === null ? item.parent : /** @type {ContentType} */ (parentItem.content).type)
|
||||
|
||||
if (item.parentSub === null) {
|
||||
// Is an array item. Insert at the old position
|
||||
@@ -212,10 +208,10 @@ export const redoItem = (transaction, item, redoitems, itemsToDelete, ignoreRemo
|
||||
*/
|
||||
let leftTrace = left
|
||||
// trace redone until parent matches
|
||||
while (leftTrace !== null && /** @type {YType__} */ (leftTrace.parent)._item !== parentItem) {
|
||||
while (leftTrace !== null && /** @type {YType} */ (leftTrace.parent)._item !== parentItem) {
|
||||
leftTrace = leftTrace.redone === null ? null : getItemCleanStart(transaction, leftTrace.redone)
|
||||
}
|
||||
if (leftTrace !== null && /** @type {YType__} */ (leftTrace.parent)._item === parentItem) {
|
||||
if (leftTrace !== null && /** @type {YType} */ (leftTrace.parent)._item === parentItem) {
|
||||
left = leftTrace
|
||||
break
|
||||
}
|
||||
@@ -227,10 +223,10 @@ export const redoItem = (transaction, item, redoitems, itemsToDelete, ignoreRemo
|
||||
*/
|
||||
let rightTrace = right
|
||||
// trace redone until parent matches
|
||||
while (rightTrace !== null && /** @type {YType__} */ (rightTrace.parent)._item !== parentItem) {
|
||||
while (rightTrace !== null && /** @type {YType} */ (rightTrace.parent)._item !== parentItem) {
|
||||
rightTrace = rightTrace.redone === null ? null : getItemCleanStart(transaction, rightTrace.redone)
|
||||
}
|
||||
if (rightTrace !== null && /** @type {YType__} */ (rightTrace.parent)._item === parentItem) {
|
||||
if (rightTrace !== null && /** @type {YType} */ (rightTrace.parent)._item === parentItem) {
|
||||
right = rightTrace
|
||||
break
|
||||
}
|
||||
@@ -282,7 +278,7 @@ export class Item extends AbstractStruct {
|
||||
* @param {ID | null} origin
|
||||
* @param {Item | null} right
|
||||
* @param {ID | null} rightOrigin
|
||||
* @param {YType<any,any>|ID|null} parent Is a type if integrated, is null if it is possible to copy parent from left or right, is ID before integration to search for it.
|
||||
* @param {YType|ID|null} parent Is a type if integrated, is null if it is possible to copy parent from left or right, is ID before integration to search for it.
|
||||
* @param {string | null} parentSub
|
||||
* @param {AbstractContent} content
|
||||
*/
|
||||
@@ -309,7 +305,7 @@ export class Item extends AbstractStruct {
|
||||
*/
|
||||
this.rightOrigin = rightOrigin
|
||||
/**
|
||||
* @type {YType<any,any>|ID|null}
|
||||
* @type {YType|ID|null}
|
||||
*/
|
||||
this.parent = parent
|
||||
/**
|
||||
@@ -466,12 +462,12 @@ export class Item extends AbstractStruct {
|
||||
if (left !== null) {
|
||||
o = left.right
|
||||
} else if (this.parentSub !== null) {
|
||||
o = /** @type {AbstractType<any>} */ (this.parent)._map.get(this.parentSub) || null
|
||||
o = /** @type {YType} */ (this.parent)._map.get(this.parentSub) || null
|
||||
while (o !== null && o.left !== null) {
|
||||
o = o.left
|
||||
}
|
||||
} else {
|
||||
o = /** @type {AbstractType<any>} */ (this.parent)._start
|
||||
o = /** @type {YType} */ (this.parent)._start
|
||||
}
|
||||
// TODO: use something like DeleteSet here (a tree implementation would be best)
|
||||
// @todo use global set definitions
|
||||
@@ -520,13 +516,13 @@ export class Item extends AbstractStruct {
|
||||
} else {
|
||||
let r
|
||||
if (this.parentSub !== null) {
|
||||
r = /** @type {AbstractType<any>} */ (this.parent)._map.get(this.parentSub) || null
|
||||
r = /** @type {YType} */ (this.parent)._map.get(this.parentSub) || null
|
||||
while (r !== null && r.left !== null) {
|
||||
r = r.left
|
||||
}
|
||||
} else {
|
||||
r = /** @type {AbstractType<any>} */ (this.parent)._start
|
||||
;/** @type {AbstractType<any>} */ (this.parent)._start = this
|
||||
r = /** @type {YType} */ (this.parent)._start
|
||||
;/** @type {YType} */ (this.parent)._start = this
|
||||
}
|
||||
this.right = r
|
||||
}
|
||||
@@ -534,7 +530,7 @@ export class Item extends AbstractStruct {
|
||||
this.right.left = this
|
||||
} else if (this.parentSub !== null) {
|
||||
// set as current parent value if right === null and this is parentSub
|
||||
/** @type {AbstractType<any>} */ (this.parent)._map.set(this.parentSub, this)
|
||||
/** @type {YType} */ (this.parent)._map.set(this.parentSub, this)
|
||||
if (this.left !== null) {
|
||||
// this is the current attribute value of parent. delete right
|
||||
this.left.delete(transaction)
|
||||
@@ -542,14 +538,14 @@ export class Item extends AbstractStruct {
|
||||
}
|
||||
// adjust length of parent
|
||||
if (this.parentSub === null && this.countable && !this.deleted) {
|
||||
/** @type {AbstractType<any>} */ (this.parent)._length += this.length
|
||||
/** @type {YType} */ (this.parent)._length += this.length
|
||||
}
|
||||
addStructToIdSet(transaction.insertSet, this)
|
||||
addStruct(transaction.doc.store, this)
|
||||
this.content.integrate(transaction, this)
|
||||
// add parent to transaction.changed
|
||||
addChangedTypeToTransaction(transaction, /** @type {import('../utils/types.js').YType} */ (this.parent), this.parentSub)
|
||||
if ((/** @type {AbstractType<any>} */ (this.parent)._item !== null && /** @type {AbstractType<any>} */ (this.parent)._item.deleted) || (this.parentSub !== null && this.right !== null)) {
|
||||
addChangedTypeToTransaction(transaction, /** @type {YType} */ (this.parent), this.parentSub)
|
||||
if ((/** @type {YType} */ (this.parent)._item !== null && /** @type {YType} */ (this.parent)._item.deleted) || (this.parentSub !== null && this.right !== null)) {
|
||||
// delete if parent is deleted or if this is not the current attribute value of parent
|
||||
this.delete(transaction)
|
||||
}
|
||||
@@ -609,7 +605,7 @@ export class Item extends AbstractStruct {
|
||||
this.content.constructor === right.content.constructor &&
|
||||
this.content.mergeWith(right.content)
|
||||
) {
|
||||
const searchMarker = /** @type {AbstractType<any>} */ (this.parent)._searchMarker
|
||||
const searchMarker = /** @type {YType} */ (this.parent)._searchMarker
|
||||
if (searchMarker) {
|
||||
searchMarker.forEach(marker => {
|
||||
if (marker.p === right) {
|
||||
@@ -642,7 +638,7 @@ export class Item extends AbstractStruct {
|
||||
*/
|
||||
delete (transaction) {
|
||||
if (!this.deleted) {
|
||||
const parent = /** @type {import('../utils/types.js').YType} */ (this.parent)
|
||||
const parent = /** @type {YType} */ (this.parent)
|
||||
// adjust the length of parent
|
||||
if (this.countable && this.parentSub === null) {
|
||||
parent._length -= this.length
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,270 +0,0 @@
|
||||
/**
|
||||
* @module YArray
|
||||
*/
|
||||
|
||||
import {
|
||||
AbstractType,
|
||||
typeListGet,
|
||||
typeListToArray,
|
||||
typeListForEach,
|
||||
typeListCreateIterator,
|
||||
typeListInsertGenerics,
|
||||
typeListPushGenerics,
|
||||
typeListDelete,
|
||||
typeListMap,
|
||||
YArrayRefID,
|
||||
transact,
|
||||
warnPrematureAccess,
|
||||
typeListSlice,
|
||||
noAttributionsManager,
|
||||
AbstractAttributionManager, ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as delta from 'lib0/delta'
|
||||
|
||||
/**
|
||||
* A shared Array implementation.
|
||||
* @template {import('../utils/types.js').YValue} T
|
||||
* @extends {AbstractType<delta.Delta<T>,YArray<T>>}
|
||||
* @implements {Iterable<T>}
|
||||
*/
|
||||
// @todo remove this
|
||||
// @ts-ignore
|
||||
export class YType extends AbstractType {
|
||||
constructor () {
|
||||
super()
|
||||
/**
|
||||
* @type {Array<any>?}
|
||||
* @private
|
||||
*/
|
||||
this._prelimContent = []
|
||||
/**
|
||||
* @type {Array<ArraySearchMarker>}
|
||||
*/
|
||||
this._searchMarker = []
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new YArray containing the specified items.
|
||||
* @template {import('../utils/types.js').YValue} T
|
||||
* @param {Array<T>} items
|
||||
* @return {YArray<T>}
|
||||
*/
|
||||
static from (items) {
|
||||
/**
|
||||
* @type {YArray<T>}
|
||||
*/
|
||||
const a = new YArray()
|
||||
a.push(items)
|
||||
return a
|
||||
}
|
||||
|
||||
/**
|
||||
* Integrate this type into the Yjs instance.
|
||||
*
|
||||
* * Save this struct in the os
|
||||
* * This type is sent to other client
|
||||
* * Observer functions are fired
|
||||
*
|
||||
* @param {Doc} y The Yjs instance
|
||||
* @param {Item?} item
|
||||
*/
|
||||
_integrate (y, item) {
|
||||
super._integrate(y, item)
|
||||
this.insert(0, /** @type {Array<any>} */ (this._prelimContent))
|
||||
this._prelimContent = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a copy of this data type that can be included somewhere else.
|
||||
*
|
||||
* Note that the content is only readable _after_ it has been included somewhere in the Ydoc.
|
||||
*
|
||||
* @return {YArray<T>}
|
||||
*/
|
||||
clone () {
|
||||
/**
|
||||
* @type {this}
|
||||
*/
|
||||
const arr = /** @type {this} */ (new YArray())
|
||||
arr.insert(0, this.toArray().map(el =>
|
||||
// @ts-ignore
|
||||
el instanceof AbstractType ? /** @type {any} */ (el.clone()) : el
|
||||
))
|
||||
return arr
|
||||
}
|
||||
|
||||
get length () {
|
||||
this.doc ?? warnPrematureAccess()
|
||||
return this._length
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(1, [1, 2])
|
||||
*
|
||||
* @param {number} index The index to insert content at.
|
||||
* @param {Array<T>} content The array of content
|
||||
*/
|
||||
insert (index, content) {
|
||||
if (this.doc !== null) {
|
||||
transact(this.doc, transaction => {
|
||||
typeListInsertGenerics(transaction, this, index, /** @type {any} */ (content))
|
||||
})
|
||||
} else {
|
||||
/** @type {Array<any>} */ (this._prelimContent).splice(index, 0, ...content)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends content to this YArray.
|
||||
*
|
||||
* @param {Array<T>} content Array of content to append.
|
||||
*
|
||||
* @todo Use the following implementation in all types.
|
||||
*/
|
||||
push (content) {
|
||||
if (this.doc !== null) {
|
||||
transact(this.doc, transaction => {
|
||||
typeListPushGenerics(transaction, this, /** @type {any} */ (content))
|
||||
})
|
||||
} else {
|
||||
/** @type {Array<any>} */ (this._prelimContent).push(...content)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepends content to this YArray.
|
||||
*
|
||||
* @param {Array<T>} content Array of content to prepend.
|
||||
*/
|
||||
unshift (content) {
|
||||
this.insert(0, content)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
if (this.doc !== null) {
|
||||
transact(this.doc, transaction => {
|
||||
typeListDelete(transaction, this, index, length)
|
||||
})
|
||||
} else {
|
||||
/** @type {Array<any>} */ (this._prelimContent).splice(index, length)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the i-th element from a YArray.
|
||||
*
|
||||
* @param {number} index The index of the element to return from the YArray
|
||||
* @return {T}
|
||||
*/
|
||||
get (index) {
|
||||
return typeListGet(this, index)
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms this YArray to a JavaScript Array.
|
||||
*
|
||||
* @return {Array<T>}
|
||||
*/
|
||||
toArray () {
|
||||
return typeListToArray(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the difference to another ydoc (which can be empty) and highlight the differences with
|
||||
* attributions.
|
||||
*
|
||||
* Note that deleted content that was not deleted in prevYdoc is rendered as an insertion with the
|
||||
* attribution `{ isDeleted: true, .. }`.
|
||||
*
|
||||
* @param {AbstractAttributionManager} am
|
||||
* @return {delta.ArrayDelta<import('./AbstractType.js').TypeToDelta<T>>} The Delta representation of this type.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
getContentDeep (am = noAttributionsManager) {
|
||||
return super.getContentDeep(am)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a portion of this YArray into a JavaScript Array selected
|
||||
* from start to end (end not included).
|
||||
*
|
||||
* @param {number} [start]
|
||||
* @param {number} [end]
|
||||
* @return {Array<T>}
|
||||
*/
|
||||
slice (start = 0, end = this.length) {
|
||||
return typeListSlice(this, start, end)
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms this Shared Type to a JSON object.
|
||||
*
|
||||
* @return {Array<any>}
|
||||
*/
|
||||
toJSON () {
|
||||
return this.map(c => c instanceof AbstractType ? c.toJSON() : c)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an Array with the result of calling a provided function on every
|
||||
* element of this YArray.
|
||||
*
|
||||
* @template M
|
||||
* @param {function(T,number,YArray<T>):M} f Function that produces an element of the new Array
|
||||
* @return {Array<M>} A new array with each element being the result of the
|
||||
* callback function
|
||||
*/
|
||||
map (f) {
|
||||
return typeListMap(this, /** @type {any} */ (f))
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a provided function once on every element of this YArray.
|
||||
*
|
||||
* @param {function(T,number,YArray<T>):void} f A function to execute on every element of this YArray.
|
||||
*/
|
||||
forEach (f) {
|
||||
typeListForEach(this, f)
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {IterableIterator<T>}
|
||||
*/
|
||||
[Symbol.iterator] () {
|
||||
return typeListCreateIterator(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||
*/
|
||||
_write (encoder) {
|
||||
encoder.writeTypeRef(YArrayRefID)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} _decoder
|
||||
* @return {import('../utils/types.js').YType}
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const readYArray = _decoder => new YArray()
|
||||
@@ -1,270 +0,0 @@
|
||||
/**
|
||||
* @module YArray
|
||||
*/
|
||||
|
||||
import {
|
||||
AbstractType,
|
||||
typeListGet,
|
||||
typeListToArray,
|
||||
typeListForEach,
|
||||
typeListCreateIterator,
|
||||
typeListInsertGenerics,
|
||||
typeListPushGenerics,
|
||||
typeListDelete,
|
||||
typeListMap,
|
||||
YArrayRefID,
|
||||
transact,
|
||||
warnPrematureAccess,
|
||||
typeListSlice,
|
||||
noAttributionsManager,
|
||||
AbstractAttributionManager, ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as delta from 'lib0/delta' // eslint-disable-line
|
||||
|
||||
/**
|
||||
* A shared Array implementation.
|
||||
* @template {import('../utils/types.js').YValue} T
|
||||
* @extends {AbstractType<delta.ArrayDelta<T>,YArray<T>>}
|
||||
* @implements {Iterable<T>}
|
||||
*/
|
||||
// @todo remove this
|
||||
// @ts-ignore
|
||||
export class YArray extends AbstractType {
|
||||
constructor () {
|
||||
super()
|
||||
/**
|
||||
* @type {Array<any>?}
|
||||
* @private
|
||||
*/
|
||||
this._prelimContent = []
|
||||
/**
|
||||
* @type {Array<ArraySearchMarker>}
|
||||
*/
|
||||
this._searchMarker = []
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new YArray containing the specified items.
|
||||
* @template {import('../utils/types.js').YValue} T
|
||||
* @param {Array<T>} items
|
||||
* @return {YArray<T>}
|
||||
*/
|
||||
static from (items) {
|
||||
/**
|
||||
* @type {YArray<T>}
|
||||
*/
|
||||
const a = new YArray()
|
||||
a.push(items)
|
||||
return a
|
||||
}
|
||||
|
||||
/**
|
||||
* Integrate this type into the Yjs instance.
|
||||
*
|
||||
* * Save this struct in the os
|
||||
* * This type is sent to other client
|
||||
* * Observer functions are fired
|
||||
*
|
||||
* @param {Doc} y The Yjs instance
|
||||
* @param {Item?} item
|
||||
*/
|
||||
_integrate (y, item) {
|
||||
super._integrate(y, item)
|
||||
this.insert(0, /** @type {Array<any>} */ (this._prelimContent))
|
||||
this._prelimContent = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a copy of this data type that can be included somewhere else.
|
||||
*
|
||||
* Note that the content is only readable _after_ it has been included somewhere in the Ydoc.
|
||||
*
|
||||
* @return {YArray<T>}
|
||||
*/
|
||||
clone () {
|
||||
/**
|
||||
* @type {this}
|
||||
*/
|
||||
const arr = /** @type {this} */ (new YArray())
|
||||
arr.insert(0, this.toArray().map(el =>
|
||||
// @ts-ignore
|
||||
el instanceof AbstractType ? /** @type {any} */ (el.clone()) : el
|
||||
))
|
||||
return arr
|
||||
}
|
||||
|
||||
get length () {
|
||||
this.doc ?? warnPrematureAccess()
|
||||
return this._length
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(1, [1, 2])
|
||||
*
|
||||
* @param {number} index The index to insert content at.
|
||||
* @param {Array<T>} content The array of content
|
||||
*/
|
||||
insert (index, content) {
|
||||
if (this.doc !== null) {
|
||||
transact(this.doc, transaction => {
|
||||
typeListInsertGenerics(transaction, this, index, /** @type {any} */ (content))
|
||||
})
|
||||
} else {
|
||||
/** @type {Array<any>} */ (this._prelimContent).splice(index, 0, ...content)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends content to this YArray.
|
||||
*
|
||||
* @param {Array<T>} content Array of content to append.
|
||||
*
|
||||
* @todo Use the following implementation in all types.
|
||||
*/
|
||||
push (content) {
|
||||
if (this.doc !== null) {
|
||||
transact(this.doc, transaction => {
|
||||
typeListPushGenerics(transaction, this, /** @type {any} */ (content))
|
||||
})
|
||||
} else {
|
||||
/** @type {Array<any>} */ (this._prelimContent).push(...content)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepends content to this YArray.
|
||||
*
|
||||
* @param {Array<T>} content Array of content to prepend.
|
||||
*/
|
||||
unshift (content) {
|
||||
this.insert(0, content)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
if (this.doc !== null) {
|
||||
transact(this.doc, transaction => {
|
||||
typeListDelete(transaction, this, index, length)
|
||||
})
|
||||
} else {
|
||||
/** @type {Array<any>} */ (this._prelimContent).splice(index, length)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the i-th element from a YArray.
|
||||
*
|
||||
* @param {number} index The index of the element to return from the YArray
|
||||
* @return {T}
|
||||
*/
|
||||
get (index) {
|
||||
return typeListGet(this, index)
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms this YArray to a JavaScript Array.
|
||||
*
|
||||
* @return {Array<T>}
|
||||
*/
|
||||
toArray () {
|
||||
return typeListToArray(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the difference to another ydoc (which can be empty) and highlight the differences with
|
||||
* attributions.
|
||||
*
|
||||
* Note that deleted content that was not deleted in prevYdoc is rendered as an insertion with the
|
||||
* attribution `{ isDeleted: true, .. }`.
|
||||
*
|
||||
* @param {AbstractAttributionManager} am
|
||||
* @return {delta.ArrayDelta<import('./AbstractType.js').TypeToDelta<T>>} The Delta representation of this type.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
getContentDeep (am = noAttributionsManager) {
|
||||
return super.getContentDeep(am)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a portion of this YArray into a JavaScript Array selected
|
||||
* from start to end (end not included).
|
||||
*
|
||||
* @param {number} [start]
|
||||
* @param {number} [end]
|
||||
* @return {Array<T>}
|
||||
*/
|
||||
slice (start = 0, end = this.length) {
|
||||
return typeListSlice(this, start, end)
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms this Shared Type to a JSON object.
|
||||
*
|
||||
* @return {Array<any>}
|
||||
*/
|
||||
toJSON () {
|
||||
return this.map(c => c instanceof AbstractType ? c.toJSON() : c)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an Array with the result of calling a provided function on every
|
||||
* element of this YArray.
|
||||
*
|
||||
* @template M
|
||||
* @param {function(T,number,YArray<T>):M} f Function that produces an element of the new Array
|
||||
* @return {Array<M>} A new array with each element being the result of the
|
||||
* callback function
|
||||
*/
|
||||
map (f) {
|
||||
return typeListMap(this, /** @type {any} */ (f))
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a provided function once on every element of this YArray.
|
||||
*
|
||||
* @param {function(T,number,YArray<T>):void} f A function to execute on every element of this YArray.
|
||||
*/
|
||||
forEach (f) {
|
||||
typeListForEach(this, f)
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {IterableIterator<T>}
|
||||
*/
|
||||
[Symbol.iterator] () {
|
||||
return typeListCreateIterator(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||
*/
|
||||
_write (encoder) {
|
||||
encoder.writeTypeRef(YArrayRefID)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} _decoder
|
||||
* @return {import('../utils/types.js').YType}
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const readYArray = _decoder => new YArray()
|
||||
@@ -1,244 +0,0 @@
|
||||
/**
|
||||
* @module YMap
|
||||
*/
|
||||
|
||||
import {
|
||||
AbstractType,
|
||||
typeMapDelete,
|
||||
typeMapSet,
|
||||
typeMapGet,
|
||||
typeMapHas,
|
||||
createMapIterator,
|
||||
YMapRefID,
|
||||
transact,
|
||||
warnPrematureAccess,
|
||||
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Item // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as iterator from 'lib0/iterator'
|
||||
import * as delta from 'lib0/delta' // eslint-disable-line
|
||||
|
||||
/**
|
||||
* @template MapType
|
||||
* A shared Map implementation.
|
||||
*
|
||||
* @extends AbstractType<delta.MapDelta<{[K in string]:MapType}>>
|
||||
* @implements {Iterable<[string, MapType]>}
|
||||
*/
|
||||
export class YMap extends AbstractType {
|
||||
/**
|
||||
*
|
||||
* @param {Iterable<readonly [string, any]>=} entries - an optional iterable to initialize the YMap
|
||||
*/
|
||||
constructor (entries) {
|
||||
super()
|
||||
/**
|
||||
* @type {Map<string,any>?}
|
||||
* @private
|
||||
*/
|
||||
this._prelimContent = null
|
||||
|
||||
if (entries === undefined) {
|
||||
this._prelimContent = new Map()
|
||||
} else {
|
||||
this._prelimContent = new Map(entries)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Integrate this type into the Yjs instance.
|
||||
*
|
||||
* * Save this struct in the os
|
||||
* * This type is sent to other client
|
||||
* * Observer functions are fired
|
||||
*
|
||||
* @param {Doc} y The Yjs instance
|
||||
* @param {Item?} item
|
||||
*/
|
||||
_integrate (y, item) {
|
||||
super._integrate(y, item)
|
||||
;/** @type {Map<string, any>} */ (this._prelimContent).forEach((value, key) => {
|
||||
this.set(key, value)
|
||||
})
|
||||
this._prelimContent = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a copy of this data type that can be included somewhere else.
|
||||
*
|
||||
* Note that the content is only readable _after_ it has been included somewhere in the Ydoc.
|
||||
*
|
||||
* @return {this}
|
||||
*/
|
||||
clone () {
|
||||
const map = this._copy()
|
||||
this.forEach((value, key) => {
|
||||
map.set(key, value instanceof AbstractType ? /** @type {typeof value} */ (value.clone()) : value)
|
||||
})
|
||||
return map
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms this Shared Type to a JSON object.
|
||||
*
|
||||
* @return {Object<string,any>}
|
||||
*/
|
||||
toJSON () {
|
||||
this.doc ?? warnPrematureAccess()
|
||||
/**
|
||||
* @type {Object<string,MapType>}
|
||||
*/
|
||||
const map = {}
|
||||
this._map.forEach((item, key) => {
|
||||
if (!item.deleted) {
|
||||
const v = item.content.getContent()[item.length - 1]
|
||||
map[key] = v instanceof AbstractType ? v.toJSON() : v
|
||||
}
|
||||
})
|
||||
return map
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size of the YMap (count of key/value pairs)
|
||||
*
|
||||
* @return {number}
|
||||
*/
|
||||
get size () {
|
||||
return [...createMapIterator(this)].length
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the keys for each element in the YMap Type.
|
||||
*
|
||||
* @return {IterableIterator<string>}
|
||||
*/
|
||||
keys () {
|
||||
return iterator.iteratorMap(createMapIterator(this), /** @param {any} v */ v => v[0])
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the values for each element in the YMap Type.
|
||||
*
|
||||
* @return {IterableIterator<MapType>}
|
||||
*/
|
||||
values () {
|
||||
return iterator.iteratorMap(createMapIterator(this), /** @param {any} v */ v => v[1].content.getContent()[v[1].length - 1])
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an Iterator of [key, value] pairs
|
||||
*
|
||||
* @return {IterableIterator<[string, MapType]>}
|
||||
*/
|
||||
entries () {
|
||||
return iterator.iteratorMap(createMapIterator(this), /** @param {any} v */ v => /** @type {any} */ ([v[0], v[1].content.getContent()[v[1].length - 1]]))
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a provided function on once on every key-value pair.
|
||||
*
|
||||
* @param {function(MapType,string,YMap<MapType>):void} f A function to execute on every element of this YArray.
|
||||
*/
|
||||
forEach (f) {
|
||||
this.doc ?? warnPrematureAccess()
|
||||
this._map.forEach((item, key) => {
|
||||
if (!item.deleted) {
|
||||
f(item.content.getContent()[item.length - 1], key, this)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an Iterator of [key, value] pairs
|
||||
*
|
||||
* @return {IterableIterator<[string, MapType]>}
|
||||
*/
|
||||
[Symbol.iterator] () {
|
||||
return this.entries()
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a specified element from this YMap.
|
||||
*
|
||||
* @param {string} key The key of the element to remove.
|
||||
*/
|
||||
delete (key) {
|
||||
if (this.doc !== null) {
|
||||
transact(this.doc, transaction => {
|
||||
typeMapDelete(transaction, this, key)
|
||||
})
|
||||
} else {
|
||||
/** @type {Map<string, any>} */ (this._prelimContent).delete(key)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds or updates an element with a specified key and value.
|
||||
* @template {MapType} VAL
|
||||
*
|
||||
* @param {string} key The key of the element to add to this YMap
|
||||
* @param {VAL} value The value of the element to add
|
||||
* @return {VAL}
|
||||
*/
|
||||
set (key, value) {
|
||||
if (this.doc !== null) {
|
||||
transact(this.doc, transaction => {
|
||||
typeMapSet(transaction, this, key, /** @type {any} */ (value))
|
||||
})
|
||||
} else {
|
||||
/** @type {Map<string, any>} */ (this._prelimContent).set(key, value)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a specified element from this YMap.
|
||||
*
|
||||
* @param {string} key
|
||||
* @return {MapType|undefined}
|
||||
*/
|
||||
get (key) {
|
||||
return /** @type {any} */ (typeMapGet(this, key))
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a boolean indicating whether the specified key exists or not.
|
||||
*
|
||||
* @param {string} key The key to test.
|
||||
* @return {boolean}
|
||||
*/
|
||||
has (key) {
|
||||
return typeMapHas(this, key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all elements from this YMap.
|
||||
*/
|
||||
clear () {
|
||||
if (this.doc !== null) {
|
||||
transact(this.doc, transaction => {
|
||||
this.forEach(function (_value, key, map) {
|
||||
typeMapDelete(transaction, map, key)
|
||||
})
|
||||
})
|
||||
} else {
|
||||
/** @type {Map<string, any>} */ (this._prelimContent).clear()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||
*/
|
||||
_write (encoder) {
|
||||
encoder.writeTypeRef(YMapRefID)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} _decoder
|
||||
* @return {import('../utils/types.js').YType}
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const readYMap = _decoder => new YMap()
|
||||
@@ -1,934 +0,0 @@
|
||||
/**
|
||||
* @module YText
|
||||
*/
|
||||
|
||||
import {
|
||||
AbstractType,
|
||||
getItemCleanStart,
|
||||
getState,
|
||||
createID,
|
||||
YTextRefID,
|
||||
transact,
|
||||
ContentEmbed,
|
||||
GC,
|
||||
ContentFormat,
|
||||
ContentString,
|
||||
iterateStructsByIdSet,
|
||||
findMarker,
|
||||
typeMapDelete,
|
||||
typeMapSet,
|
||||
typeMapGet,
|
||||
typeMapGetAll,
|
||||
updateMarkerChanges,
|
||||
ContentType,
|
||||
warnPrematureAccess,
|
||||
noAttributionsManager, AbstractAttributionManager, ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Item, Transaction, // eslint-disable-line
|
||||
createIdSet,
|
||||
equalAttrs
|
||||
} from '../internals.js'
|
||||
|
||||
import * as math from 'lib0/math'
|
||||
import * as traits from 'lib0/traits'
|
||||
import * as map from 'lib0/map'
|
||||
import * as error from 'lib0/error'
|
||||
|
||||
export class ItemTextListPosition {
|
||||
/**
|
||||
* @param {Item|null} left
|
||||
* @param {Item|null} right
|
||||
* @param {number} index
|
||||
* @param {Map<string,any>} currentAttributes
|
||||
* @param {AbstractAttributionManager} am
|
||||
*/
|
||||
constructor (left, right, index, currentAttributes, am) {
|
||||
this.left = left
|
||||
this.right = right
|
||||
this.index = index
|
||||
this.currentAttributes = currentAttributes
|
||||
this.am = am
|
||||
}
|
||||
|
||||
/**
|
||||
* Only call this if you know that this.right is defined
|
||||
*/
|
||||
forward () {
|
||||
if (this.right === null) {
|
||||
error.unexpectedCase()
|
||||
}
|
||||
switch (this.right.content.constructor) {
|
||||
case ContentFormat:
|
||||
if (!this.right.deleted) {
|
||||
updateCurrentAttributes(this.currentAttributes, /** @type {ContentFormat} */ (this.right.content))
|
||||
}
|
||||
break
|
||||
default:
|
||||
this.index += this.am.contentLength(this.right)
|
||||
break
|
||||
}
|
||||
this.left = this.right
|
||||
this.right = this.right.right
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {import('../utils/types.js').YType} parent
|
||||
* @param {number} length
|
||||
* @param {Object<string,any>} attributes
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
formatText (transaction, parent, length, attributes) {
|
||||
const doc = transaction.doc
|
||||
const ownClientId = doc.clientID
|
||||
minimizeAttributeChanges(this, attributes)
|
||||
const negatedAttributes = insertAttributes(transaction, parent, this, attributes)
|
||||
// iterate until first non-format or null is found
|
||||
// delete all formats with attributes[format.key] != null
|
||||
// also check the attributes after the first non-format as we do not want to insert redundant negated attributes there
|
||||
// eslint-disable-next-line no-labels
|
||||
iterationLoop: while (
|
||||
this.right !== null &&
|
||||
(length > 0 ||
|
||||
(
|
||||
negatedAttributes.size > 0 &&
|
||||
((this.right.deleted && this.am.contentLength(this.right) === 0) || this.right.content.constructor === ContentFormat)
|
||||
)
|
||||
)
|
||||
) {
|
||||
switch (this.right.content.constructor) {
|
||||
case ContentFormat: {
|
||||
if (!this.right.deleted) {
|
||||
const { key, value } = /** @type {ContentFormat} */ (this.right.content)
|
||||
const attr = attributes[key]
|
||||
if (attr !== undefined) {
|
||||
if (equalAttrs(attr, value)) {
|
||||
negatedAttributes.delete(key)
|
||||
} else {
|
||||
if (length === 0) {
|
||||
// no need to further extend negatedAttributes
|
||||
// eslint-disable-next-line no-labels
|
||||
break iterationLoop
|
||||
}
|
||||
negatedAttributes.set(key, value)
|
||||
}
|
||||
this.right.delete(transaction)
|
||||
} else {
|
||||
this.currentAttributes.set(key, value)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
default: {
|
||||
const item = this.right
|
||||
const rightLen = this.am.contentLength(item)
|
||||
if (length < rightLen) {
|
||||
/**
|
||||
* @type {Array<import('../internals.js').AttributedContent<any>>}
|
||||
*/
|
||||
const contents = []
|
||||
this.am.readContent(contents, item.id.client, item.id.clock, item.deleted, item.content, 0)
|
||||
let i = 0
|
||||
for (; i < contents.length && length > 0; i++) {
|
||||
const c = contents[i]
|
||||
if ((!c.deleted || c.attrs != null) && c.content.isCountable()) {
|
||||
length -= c.content.getLength()
|
||||
}
|
||||
}
|
||||
if (length < 0 || (length === 0 && i !== contents.length)) {
|
||||
const c = contents[--i]
|
||||
getItemCleanStart(transaction, createID(item.id.client, c.clock + c.content.getLength() + length))
|
||||
}
|
||||
} else {
|
||||
length -= rightLen
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
this.forward()
|
||||
}
|
||||
// Quill just assumes that the editor starts with a newline and that it always
|
||||
// ends with a newline. We only insert that newline when a new newline is
|
||||
// inserted - i.e when length is bigger than type.length
|
||||
if (length > 0) {
|
||||
let newlines = ''
|
||||
for (; length > 0; length--) {
|
||||
newlines += '\n'
|
||||
}
|
||||
this.right = new Item(createID(ownClientId, getState(doc.store, ownClientId)), this.left, this.left && this.left.lastId, this.right, this.right && this.right.id, parent, null, new ContentString(newlines))
|
||||
this.right.integrate(transaction, 0)
|
||||
this.forward()
|
||||
}
|
||||
insertNegatedAttributes(transaction, parent, this, negatedAttributes)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {ItemTextListPosition} pos
|
||||
* @param {number} count steps to move forward
|
||||
* @return {ItemTextListPosition}
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
const findNextPosition = (transaction, pos, count) => {
|
||||
while (pos.right !== null && count > 0) {
|
||||
switch (pos.right.content.constructor) {
|
||||
case ContentFormat:
|
||||
if (!pos.right.deleted) {
|
||||
updateCurrentAttributes(pos.currentAttributes, /** @type {ContentFormat} */ (pos.right.content))
|
||||
}
|
||||
break
|
||||
default:
|
||||
if (!pos.right.deleted) {
|
||||
if (count < pos.right.length) {
|
||||
// split right
|
||||
getItemCleanStart(transaction, createID(pos.right.id.client, pos.right.id.clock + count))
|
||||
}
|
||||
pos.index += pos.right.length
|
||||
count -= pos.right.length
|
||||
}
|
||||
break
|
||||
}
|
||||
pos.left = pos.right
|
||||
pos.right = pos.right.right
|
||||
// pos.forward() - we don't forward because that would halve the performance because we already do the checks above
|
||||
}
|
||||
return pos
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {import('../utils/types.js').YType} parent
|
||||
* @param {number} index
|
||||
* @param {boolean} useSearchMarker
|
||||
* @return {ItemTextListPosition}
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
const findPosition = (transaction, parent, index, useSearchMarker) => {
|
||||
const currentAttributes = new Map()
|
||||
const marker = useSearchMarker ? findMarker(parent, index) : null
|
||||
if (marker) {
|
||||
const pos = new ItemTextListPosition(marker.p.left, marker.p, marker.index, currentAttributes, noAttributionsManager)
|
||||
return findNextPosition(transaction, pos, index - marker.index)
|
||||
} else {
|
||||
const pos = new ItemTextListPosition(null, parent._start, 0, currentAttributes, noAttributionsManager)
|
||||
return findNextPosition(transaction, pos, index)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Negate applied formats
|
||||
*
|
||||
* @param {Transaction} transaction
|
||||
* @param {import('../utils/types.js').YType} parent
|
||||
* @param {ItemTextListPosition} currPos
|
||||
* @param {Map<string,any>} negatedAttributes
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
const insertNegatedAttributes = (transaction, parent, currPos, negatedAttributes) => {
|
||||
// check if we really need to remove attributes
|
||||
while (
|
||||
currPos.right !== null && (
|
||||
(currPos.right.deleted && (currPos.am === noAttributionsManager || currPos.am.contentLength(currPos.right) === 0)) || (
|
||||
currPos.right.content.constructor === ContentFormat &&
|
||||
equalAttrs(negatedAttributes.get(/** @type {ContentFormat} */ (currPos.right.content).key), /** @type {ContentFormat} */ (currPos.right.content).value)
|
||||
)
|
||||
)
|
||||
) {
|
||||
if (!currPos.right.deleted) {
|
||||
negatedAttributes.delete(/** @type {ContentFormat} */ (currPos.right.content).key)
|
||||
}
|
||||
currPos.forward()
|
||||
}
|
||||
const doc = transaction.doc
|
||||
const ownClientId = doc.clientID
|
||||
negatedAttributes.forEach((val, key) => {
|
||||
const left = currPos.left
|
||||
const right = currPos.right
|
||||
const nextFormat = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentFormat(key, val))
|
||||
nextFormat.integrate(transaction, 0)
|
||||
currPos.right = nextFormat
|
||||
currPos.forward()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Map<string,any>} currentAttributes
|
||||
* @param {ContentFormat} format
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
const updateCurrentAttributes = (currentAttributes, format) => {
|
||||
const { key, value } = format
|
||||
if (value === null) {
|
||||
currentAttributes.delete(key)
|
||||
} else {
|
||||
currentAttributes.set(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ItemTextListPosition} currPos
|
||||
* @param {Object<string,any>} attributes
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
const minimizeAttributeChanges = (currPos, attributes) => {
|
||||
// go right while attributes[right.key] === right.value (or right is deleted)
|
||||
while (true) {
|
||||
if (currPos.right === null) {
|
||||
break
|
||||
} else if (currPos.right.deleted ? (currPos.am.contentLength(currPos.right) === 0) : (!currPos.right.deleted && currPos.right.content.constructor === ContentFormat && equalAttrs(attributes[(/** @type {ContentFormat} */ (currPos.right.content)).key] ?? null, /** @type {ContentFormat} */ (currPos.right.content).value))) {
|
||||
//
|
||||
} else {
|
||||
break
|
||||
}
|
||||
currPos.forward()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {import('../utils/types.js').YType} parent
|
||||
* @param {ItemTextListPosition} currPos
|
||||
* @param {Object<string,any>} attributes
|
||||
* @return {Map<string,any>}
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
**/
|
||||
const insertAttributes = (transaction, parent, currPos, attributes) => {
|
||||
const doc = transaction.doc
|
||||
const ownClientId = doc.clientID
|
||||
const negatedAttributes = new Map()
|
||||
// insert format-start items
|
||||
for (const key in attributes) {
|
||||
const val = attributes[key]
|
||||
const currentVal = currPos.currentAttributes.get(key) ?? null
|
||||
if (!equalAttrs(currentVal, val)) {
|
||||
// save negated attribute (set null if currentVal undefined)
|
||||
negatedAttributes.set(key, currentVal)
|
||||
const { left, right } = currPos
|
||||
currPos.right = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentFormat(key, val))
|
||||
currPos.right.integrate(transaction, 0)
|
||||
currPos.forward()
|
||||
}
|
||||
}
|
||||
return negatedAttributes
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {import('../utils/types.js').YType} parent
|
||||
* @param {ItemTextListPosition} currPos
|
||||
* @param {string|object|import('../utils/types.js').YType} text
|
||||
* @param {Object<string,any>} attributes
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
**/
|
||||
export const insertText = (transaction, parent, currPos, text, attributes) => {
|
||||
currPos.currentAttributes.forEach((_val, key) => {
|
||||
if (attributes[key] === undefined) {
|
||||
attributes[key] = null
|
||||
}
|
||||
})
|
||||
const doc = transaction.doc
|
||||
const ownClientId = doc.clientID
|
||||
minimizeAttributeChanges(currPos, attributes)
|
||||
const negatedAttributes = insertAttributes(transaction, parent, currPos, attributes)
|
||||
// insert content
|
||||
const content = text.constructor === String ? new ContentString(/** @type {string} */ (text)) : (text instanceof AbstractType ? new ContentType(text) : new ContentEmbed(text))
|
||||
let { left, right, index } = currPos
|
||||
if (parent._searchMarker) {
|
||||
updateMarkerChanges(parent._searchMarker, currPos.index, content.getLength())
|
||||
}
|
||||
right = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, content)
|
||||
right.integrate(transaction, 0)
|
||||
currPos.right = right
|
||||
currPos.index = index
|
||||
currPos.forward()
|
||||
insertNegatedAttributes(transaction, parent, currPos, negatedAttributes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this function after string content has been deleted in order to
|
||||
* clean up formatting Items.
|
||||
*
|
||||
* @param {Transaction} transaction
|
||||
* @param {Item} start
|
||||
* @param {Item|null} curr exclusive end, automatically iterates to the next Content Item
|
||||
* @param {Map<string,any>} startAttributes
|
||||
* @param {Map<string,any>} currAttributes
|
||||
* @return {number} The amount of formatting Items deleted.
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
const cleanupFormattingGap = (transaction, start, curr, startAttributes, currAttributes) => {
|
||||
if (!transaction.doc.cleanupFormatting) return 0
|
||||
/**
|
||||
* @type {Item|null}
|
||||
*/
|
||||
let end = start
|
||||
/**
|
||||
* @type {Map<string,ContentFormat>}
|
||||
*/
|
||||
const endFormats = map.create()
|
||||
while (end && (!end.countable || end.deleted)) {
|
||||
if (!end.deleted && end.content.constructor === ContentFormat) {
|
||||
const cf = /** @type {ContentFormat} */ (end.content)
|
||||
endFormats.set(cf.key, cf)
|
||||
}
|
||||
end = end.right
|
||||
}
|
||||
let cleanups = 0
|
||||
let reachedCurr = false
|
||||
while (start !== end) {
|
||||
if (curr === start) {
|
||||
reachedCurr = true
|
||||
}
|
||||
if (!start.deleted) {
|
||||
const content = start.content
|
||||
switch (content.constructor) {
|
||||
case ContentFormat: {
|
||||
const { key, value } = /** @type {ContentFormat} */ (content)
|
||||
const startAttrValue = startAttributes.get(key) ?? null
|
||||
if (endFormats.get(key) !== content || startAttrValue === value) {
|
||||
// Either this format is overwritten or it is not necessary because the attribute already existed.
|
||||
start.delete(transaction)
|
||||
transaction.cleanUps.add(start.id.client, start.id.clock, start.length)
|
||||
cleanups++
|
||||
if (!reachedCurr && (currAttributes.get(key) ?? null) === value && startAttrValue !== value) {
|
||||
if (startAttrValue === null) {
|
||||
currAttributes.delete(key)
|
||||
} else {
|
||||
currAttributes.set(key, startAttrValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!reachedCurr && !start.deleted) {
|
||||
updateCurrentAttributes(currAttributes, /** @type {ContentFormat} */ (content))
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
start = /** @type {Item} */ (start.right)
|
||||
}
|
||||
return cleanups
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {Item | null} item
|
||||
*/
|
||||
const cleanupContextlessFormattingGap = (transaction, item) => {
|
||||
if (!transaction.doc.cleanupFormatting) return 0
|
||||
// iterate until item.right is null or content
|
||||
while (item && item.right && (item.right.deleted || !item.right.countable)) {
|
||||
item = item.right
|
||||
}
|
||||
const attrs = new Set()
|
||||
// iterate back until a content item is found
|
||||
while (item && (item.deleted || !item.countable)) {
|
||||
if (!item.deleted && item.content.constructor === ContentFormat) {
|
||||
const key = /** @type {ContentFormat} */ (item.content).key
|
||||
if (attrs.has(key)) {
|
||||
item.delete(transaction)
|
||||
transaction.cleanUps.add(item.id.client, item.id.clock, item.length)
|
||||
} else {
|
||||
attrs.add(key)
|
||||
}
|
||||
}
|
||||
item = item.left
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is experimental and subject to change / be removed.
|
||||
*
|
||||
* Ideally, we don't need this function at all. Formatting attributes should be cleaned up
|
||||
* automatically after each change. This function iterates twice over the complete YText type
|
||||
* and removes unnecessary formatting attributes. This is also helpful for testing.
|
||||
*
|
||||
* This function won't be exported anymore as soon as there is confidence that the YText type works as intended.
|
||||
*
|
||||
* @param {YText<any>} type
|
||||
* @return {number} How many formatting attributes have been cleaned up.
|
||||
*/
|
||||
export const cleanupYTextFormatting = type => {
|
||||
if (!type.doc?.cleanupFormatting) return 0
|
||||
let res = 0
|
||||
transact(/** @type {Doc} */ (type.doc), transaction => {
|
||||
let start = /** @type {Item} */ (type._start)
|
||||
let end = type._start
|
||||
let startAttributes = map.create()
|
||||
const currentAttributes = map.copy(startAttributes)
|
||||
while (end) {
|
||||
if (end.deleted === false) {
|
||||
switch (end.content.constructor) {
|
||||
case ContentFormat:
|
||||
updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (end.content))
|
||||
break
|
||||
default:
|
||||
res += cleanupFormattingGap(transaction, start, end, startAttributes, currentAttributes)
|
||||
startAttributes = map.copy(currentAttributes)
|
||||
start = end
|
||||
break
|
||||
}
|
||||
}
|
||||
end = end.right
|
||||
}
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
* This will be called by the transaction once the event handlers are called to potentially cleanup
|
||||
* formatting attributes.
|
||||
*
|
||||
* @param {Transaction} transaction
|
||||
*/
|
||||
export const cleanupYTextAfterTransaction = transaction => {
|
||||
/**
|
||||
* @type {Set<YText<any>>}
|
||||
*/
|
||||
const needFullCleanup = new Set()
|
||||
// check if another formatting item was inserted
|
||||
const doc = transaction.doc
|
||||
iterateStructsByIdSet(transaction, transaction.insertSet, (item) => {
|
||||
if (
|
||||
!item.deleted && /** @type {Item} */ (item).content.constructor === ContentFormat && item.constructor !== GC
|
||||
) {
|
||||
needFullCleanup.add(/** @type {any} */ (item).parent)
|
||||
}
|
||||
})
|
||||
// cleanup in a new transaction
|
||||
transact(doc, (t) => {
|
||||
iterateStructsByIdSet(transaction, transaction.deleteSet, item => {
|
||||
if (item instanceof GC || !(/** @type {YText<any>} */ (item.parent)._hasFormatting) || needFullCleanup.has(/** @type {YText<any>} */ (item.parent))) {
|
||||
return
|
||||
}
|
||||
const parent = /** @type {YText<any>} */ (item.parent)
|
||||
if (item.content.constructor === ContentFormat) {
|
||||
needFullCleanup.add(parent)
|
||||
} else {
|
||||
// If no formatting attribute was inserted or deleted, we can make due with contextless
|
||||
// formatting cleanups.
|
||||
// Contextless: it is not necessary to compute currentAttributes for the affected position.
|
||||
cleanupContextlessFormattingGap(t, item)
|
||||
}
|
||||
})
|
||||
// If a formatting item was inserted, we simply clean the whole type.
|
||||
// We need to compute currentAttributes for the current position anyway.
|
||||
for (const yText of needFullCleanup) {
|
||||
cleanupYTextFormatting(yText)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {ItemTextListPosition} currPos
|
||||
* @param {number} length
|
||||
* @return {ItemTextListPosition}
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const deleteText = (transaction, currPos, length) => {
|
||||
const startLength = length
|
||||
const startAttrs = map.copy(currPos.currentAttributes)
|
||||
const start = currPos.right
|
||||
while (length > 0 && currPos.right !== null) {
|
||||
if (!currPos.right.deleted) {
|
||||
switch (currPos.right.content.constructor) {
|
||||
case ContentType:
|
||||
case ContentEmbed:
|
||||
case ContentString:
|
||||
if (length < currPos.right.length) {
|
||||
getItemCleanStart(transaction, createID(currPos.right.id.client, currPos.right.id.clock + length))
|
||||
}
|
||||
length -= currPos.right.length
|
||||
currPos.right.delete(transaction)
|
||||
break
|
||||
}
|
||||
} else if (currPos.am !== noAttributionsManager) {
|
||||
const item = currPos.right
|
||||
/**
|
||||
* @type {Array<import('../internals.js').AttributedContent<any>>}
|
||||
*/
|
||||
const contents = []
|
||||
currPos.am.readContent(contents, item.id.client, item.id.clock, true, item.content, 0)
|
||||
for (let i = 0; i < contents.length; i++) {
|
||||
const c = contents[i]
|
||||
if (c.content.isCountable() && c.attrs != null) {
|
||||
// deleting already deleted content. store that information in a meta property, but do
|
||||
// nothing
|
||||
const contentLen = math.min(c.content.getLength(), length)
|
||||
map.setIfUndefined(transaction.meta, 'attributedDeletes', createIdSet).add(item.id.client, c.clock, contentLen)
|
||||
length -= contentLen
|
||||
}
|
||||
}
|
||||
const lastContent = contents.length > 0 ? contents[contents.length - 1] : null
|
||||
const nextItemClock = item.id.clock + item.length
|
||||
const nextContentClock = lastContent != null ? lastContent.clock + lastContent.content.getLength() : nextItemClock
|
||||
if (nextContentClock < nextItemClock) {
|
||||
getItemCleanStart(transaction, createID(item.id.client, nextContentClock))
|
||||
}
|
||||
}
|
||||
currPos.forward()
|
||||
}
|
||||
if (start) {
|
||||
cleanupFormattingGap(transaction, start, currPos.right, startAttrs, currPos.currentAttributes)
|
||||
}
|
||||
const parent = /** @type {AbstractType<any>} */ (/** @type {Item} */ (currPos.left || currPos.right).parent)
|
||||
if (parent._searchMarker) {
|
||||
updateMarkerChanges(parent._searchMarker, currPos.index, -startLength + length)
|
||||
}
|
||||
return currPos
|
||||
}
|
||||
|
||||
/**
|
||||
* The Quill Delta format represents changes on a text document with
|
||||
* formatting information. For more information visit {@link https://quilljs.com/docs/delta/|Quill Delta}
|
||||
*
|
||||
* @example
|
||||
* {
|
||||
* ops: [
|
||||
* { insert: 'Gandalf', attributes: { bold: true } },
|
||||
* { insert: ' the ' },
|
||||
* { insert: 'Grey', attributes: { color: '#cccccc' } }
|
||||
* ]
|
||||
* }
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* Attributes that can be assigned to a selection of text.
|
||||
*
|
||||
* @example
|
||||
* {
|
||||
* bold: true,
|
||||
* font-size: '40px'
|
||||
* }
|
||||
*
|
||||
* @typedef {Object} TextAttributes
|
||||
*/
|
||||
|
||||
/**
|
||||
* Type that represents text with formatting information.
|
||||
*
|
||||
* This type replaces y-richtext as this implementation is able to handle
|
||||
* block formats (format information on a paragraph), embeds (complex elements
|
||||
* like pictures and videos), and text formats (**bold**, *italic*).
|
||||
*
|
||||
* @template {{ [key:string]:any } | import('../utils/types.js').YType} [Embeds={ [key:string]:any } | import('../utils/types.js').YType]
|
||||
* @extends {AbstractType<import('lib0/delta').TextDelta<Embeds>>}
|
||||
*/
|
||||
export class YText extends AbstractType {
|
||||
/**
|
||||
* @param {String} [string] The initial value of the YText.
|
||||
*/
|
||||
constructor (string) {
|
||||
super()
|
||||
/**
|
||||
* Array of pending operations on this type
|
||||
* @type {Array<function():void>?}
|
||||
*/
|
||||
this._pending = string !== undefined ? [() => this.insert(0, string)] : []
|
||||
/**
|
||||
* @type {Array<ArraySearchMarker>|null}
|
||||
*/
|
||||
this._searchMarker = []
|
||||
/**
|
||||
* Whether this YText contains formatting attributes.
|
||||
* This flag is updated when a formatting item is integrated (see ContentFormat.integrate)
|
||||
*/
|
||||
this._hasFormatting = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of characters of this text type.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
get length () {
|
||||
this.doc ?? warnPrematureAccess()
|
||||
return this._length
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Doc} y
|
||||
* @param {Item?} item
|
||||
*/
|
||||
_integrate (y, item) {
|
||||
super._integrate(y, item)
|
||||
try {
|
||||
/** @type {Array<function>} */ (this._pending).forEach(f => f())
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
this._pending = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a copy of this data type that can be included somewhere else.
|
||||
*
|
||||
* Note that the content is only readable _after_ it has been included somewhere in the Ydoc.
|
||||
*
|
||||
* @return {YText<Embeds>}
|
||||
*/
|
||||
clone () {
|
||||
/**
|
||||
* @type {YText<Embeds>}
|
||||
*/
|
||||
const text = /** @type {any} */ (new YText())
|
||||
text.applyDelta(this.getContent())
|
||||
return text
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates YTextEvent and calls observers.
|
||||
*
|
||||
* @param {Transaction} transaction
|
||||
* @param {Set<null|string>} parentSubs Keys changed on this type. `null` if list was modified.
|
||||
*/
|
||||
_callObserver (transaction, parentSubs) {
|
||||
super._callObserver(transaction, parentSubs)
|
||||
// If a remote change happened, we try to cleanup potential formatting duplicates.
|
||||
if (!transaction.local && this._hasFormatting) {
|
||||
transaction._needFormattingCleanup = true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the unformatted string representation of this YText type.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
toString () {
|
||||
this.doc ?? warnPrematureAccess()
|
||||
let str = ''
|
||||
/**
|
||||
* @type {Item|null}
|
||||
*/
|
||||
let n = this._start
|
||||
while (n !== null) {
|
||||
if (!n.deleted && n.countable && n.content.constructor === ContentString) {
|
||||
str += /** @type {ContentString} */ (n.content).str
|
||||
}
|
||||
n = n.right
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the unformatted string representation of this YText type.
|
||||
*
|
||||
* @return {string}
|
||||
* @public
|
||||
*/
|
||||
toJSON () {
|
||||
return this.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert text at a given index.
|
||||
*
|
||||
* @param {number} index The index at which to start inserting.
|
||||
* @param {String} text The text to insert at the specified position.
|
||||
* @param {TextAttributes} [attributes] Optionally define some formatting
|
||||
* information to apply on the inserted
|
||||
* Text.
|
||||
* @public
|
||||
*/
|
||||
insert (index, text, attributes) {
|
||||
if (text.length <= 0) {
|
||||
return
|
||||
}
|
||||
const y = this.doc
|
||||
if (y !== null) {
|
||||
transact(y, transaction => {
|
||||
const pos = findPosition(transaction, this, index, !attributes)
|
||||
if (!attributes) {
|
||||
attributes = {}
|
||||
// @ts-ignore
|
||||
pos.currentAttributes.forEach((v, k) => { attributes[k] = v })
|
||||
}
|
||||
insertText(transaction, this, pos, text, attributes)
|
||||
})
|
||||
} else {
|
||||
/** @type {Array<function>} */ (this._pending).push(() => this.insert(index, text, attributes))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts an embed at a index.
|
||||
*
|
||||
* @param {number} index The index to insert the embed at.
|
||||
* @param {Object | AbstractType<any>} embed The Object that represents the embed.
|
||||
* @param {TextAttributes} [attributes] Attribute information to apply on the
|
||||
* embed
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
insertEmbed (index, embed, attributes) {
|
||||
const y = this.doc
|
||||
if (y !== null) {
|
||||
transact(y, transaction => {
|
||||
const pos = findPosition(transaction, this, index, !attributes)
|
||||
insertText(transaction, this, pos, embed, attributes || {})
|
||||
})
|
||||
} else {
|
||||
/** @type {Array<function>} */ (this._pending).push(() => this.insertEmbed(index, embed, attributes || {}))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes text starting from an index.
|
||||
*
|
||||
* @param {number} index Index at which to start deleting.
|
||||
* @param {number} length The number of characters to remove. Defaults to 1.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
delete (index, length) {
|
||||
if (length === 0) {
|
||||
return
|
||||
}
|
||||
const y = this.doc
|
||||
if (y !== null) {
|
||||
transact(y, transaction => {
|
||||
deleteText(transaction, findPosition(transaction, this, index, true), length)
|
||||
})
|
||||
} else {
|
||||
/** @type {Array<function>} */ (this._pending).push(() => this.delete(index, length))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns properties to a range of text.
|
||||
*
|
||||
* @param {number} index The position where to start formatting.
|
||||
* @param {number} length The amount of characters to assign properties to.
|
||||
* @param {TextAttributes} attributes Attribute information to apply on the
|
||||
* text.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
format (index, length, attributes) {
|
||||
if (length === 0) {
|
||||
return
|
||||
}
|
||||
const y = this.doc
|
||||
if (y !== null) {
|
||||
transact(y, transaction => {
|
||||
const pos = findPosition(transaction, this, index, false)
|
||||
if (pos.right === null) {
|
||||
return
|
||||
}
|
||||
pos.formatText(transaction, this, length, attributes)
|
||||
})
|
||||
} else {
|
||||
/** @type {Array<function>} */ (this._pending).push(() => this.format(index, length, attributes))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an attribute.
|
||||
*
|
||||
* @note Xml-Text nodes don't have attributes. You can use this feature to assign properties to complete text-blocks.
|
||||
*
|
||||
* @param {String} attributeName The attribute name that is to be removed.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
removeAttribute (attributeName) {
|
||||
if (this.doc !== null) {
|
||||
transact(this.doc, transaction => {
|
||||
typeMapDelete(transaction, this, attributeName)
|
||||
})
|
||||
} else {
|
||||
/** @type {Array<function>} */ (this._pending).push(() => this.removeAttribute(attributeName))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets or updates an attribute.
|
||||
*
|
||||
* @note Xml-Text nodes don't have attributes. You can use this feature to assign properties to complete text-blocks.
|
||||
*
|
||||
* @param {String} attributeName The attribute name that is to be set.
|
||||
* @param {any} attributeValue The attribute value that is to be set.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
setAttribute (attributeName, attributeValue) {
|
||||
if (this.doc !== null) {
|
||||
transact(this.doc, transaction => {
|
||||
typeMapSet(transaction, this, attributeName, attributeValue)
|
||||
})
|
||||
} else {
|
||||
/** @type {Array<function>} */ (this._pending).push(() => this.setAttribute(attributeName, attributeValue))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an attribute value that belongs to the attribute name.
|
||||
*
|
||||
* @note Xml-Text nodes don't have attributes. You can use this feature to assign properties to complete text-blocks.
|
||||
*
|
||||
* @param {String} attributeName The attribute name that identifies the
|
||||
* queried value.
|
||||
* @return {any} The queried attribute value.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
getAttribute (attributeName) {
|
||||
return /** @type {any} */ (typeMapGet(this, attributeName))
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all attribute name/value pairs in a JSON Object.
|
||||
*
|
||||
* @note Xml-Text nodes don't have attributes. You can use this feature to assign properties to complete text-blocks.
|
||||
*
|
||||
* @return {Object<string, any>} A JSON Object that describes the attributes.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
getAttributes () {
|
||||
return typeMapGetAll(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||
*/
|
||||
_write (encoder) {
|
||||
encoder.writeTypeRef(YTextRefID)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {this} other
|
||||
*/
|
||||
[traits.EqualityTraitSymbol] (other) {
|
||||
return this.getContent().equals(other.getContent())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} _decoder
|
||||
* @return {import('../utils/types.js').YType}
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const readYText = _decoder => new YText()
|
||||
@@ -1,227 +0,0 @@
|
||||
import * as object from 'lib0/object'
|
||||
|
||||
import {
|
||||
YXmlFragment,
|
||||
transact,
|
||||
typeMapDelete,
|
||||
typeMapHas,
|
||||
typeMapSet,
|
||||
typeMapGet,
|
||||
typeMapGetAll,
|
||||
typeMapGetAllSnapshot,
|
||||
YXmlElementRefID,
|
||||
Snapshot, YXmlText, ContentType, AbstractType, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Item, // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
/**
|
||||
* @typedef {Object|number|null|Array<any>|string|Uint8Array|AbstractType<any>} ValueTypes
|
||||
*/
|
||||
|
||||
/**
|
||||
* An YXmlElement imitates the behavior of a
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element
|
||||
*
|
||||
* * An YXmlElement has attributes (key value pairs)
|
||||
* * An YXmlElement has childElements that must inherit from YXmlElement
|
||||
*
|
||||
* @template {{ [key: string]: any }} [Attrs={ [key: string]: string }]
|
||||
* @template {any} [Children=any]
|
||||
* @extends YXmlFragment<Children,Attrs>
|
||||
*/
|
||||
export class YXmlElement extends YXmlFragment {
|
||||
constructor (nodeName = 'UNDEFINED') {
|
||||
super()
|
||||
this.nodeName = nodeName
|
||||
/**
|
||||
* @type {Map<string, any>|null}
|
||||
*/
|
||||
this._prelimAttrs = new Map()
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {YXmlElement|YXmlText|null}
|
||||
*/
|
||||
get nextSibling () {
|
||||
const n = this._item ? this._item.next : null
|
||||
return n ? /** @type {YXmlElement|YXmlText} */ (/** @type {ContentType} */ (n.content).type) : null
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {YXmlElement|YXmlText|null}
|
||||
*/
|
||||
get prevSibling () {
|
||||
const n = this._item ? this._item.prev : null
|
||||
return n ? /** @type {YXmlElement|YXmlText} */ (/** @type {ContentType} */ (n.content).type) : null
|
||||
}
|
||||
|
||||
/**
|
||||
* Integrate this type into the Yjs instance.
|
||||
*
|
||||
* * Save this struct in the os
|
||||
* * This type is sent to other client
|
||||
* * Observer functions are fired
|
||||
*
|
||||
* @param {Doc} y The Yjs instance
|
||||
* @param {Item?} item
|
||||
*/
|
||||
_integrate (y, item) {
|
||||
super._integrate(y, item)
|
||||
;(/** @type {Map<string, any>} */ (this._prelimAttrs)).forEach((value, key) => {
|
||||
this.setAttribute(key, value)
|
||||
})
|
||||
this._prelimAttrs = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an Item with the same effect as this Item (without position effect)
|
||||
*
|
||||
* @return {this}
|
||||
*/
|
||||
_copy () {
|
||||
return /** @type {any} */ (new YXmlElement(this.nodeName))
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a copy of this data type that can be included somewhere else.
|
||||
*
|
||||
* Note that the content is only readable _after_ it has been included somewhere in the Ydoc.
|
||||
*
|
||||
* @return {this}
|
||||
*/
|
||||
clone () {
|
||||
const el = this._copy()
|
||||
const attrs = this.getAttributes()
|
||||
object.forEach(attrs, (value, key) => {
|
||||
if (typeof value === 'string') {
|
||||
el.setAttribute(key, value)
|
||||
}
|
||||
})
|
||||
// @ts-ignore
|
||||
el.insert(0, this.toArray().map(item => item instanceof AbstractType ? item.clone() : item))
|
||||
return el
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the XML serialization of this YXmlElement.
|
||||
* The attributes are ordered by attribute-name, so you can easily use this
|
||||
* method to compare YXmlElements
|
||||
*
|
||||
* @return {string} The string representation of this type.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
toString () {
|
||||
const attrs = this.getAttributes()
|
||||
const stringBuilder = []
|
||||
const keys = []
|
||||
for (const key in attrs) {
|
||||
keys.push(key)
|
||||
}
|
||||
keys.sort()
|
||||
const keysLen = keys.length
|
||||
for (let i = 0; i < keysLen; i++) {
|
||||
const key = keys[i]
|
||||
stringBuilder.push(key + '="' + attrs[key] + '"')
|
||||
}
|
||||
const nodeName = this.nodeName.toLocaleLowerCase()
|
||||
const attrsString = stringBuilder.length > 0 ? ' ' + stringBuilder.join(' ') : ''
|
||||
return `<${nodeName}${attrsString}>${super.toString()}</${nodeName}>`
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an attribute from this YXmlElement.
|
||||
*
|
||||
* @param {string} attributeName The attribute name that is to be removed.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
removeAttribute (attributeName) {
|
||||
if (this.doc !== null) {
|
||||
transact(this.doc, transaction => {
|
||||
typeMapDelete(transaction, this, attributeName)
|
||||
})
|
||||
} else {
|
||||
/** @type {Map<string,any>} */ (this._prelimAttrs).delete(attributeName)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets or updates an attribute.
|
||||
*
|
||||
* @template {keyof Attrs & string} KEY
|
||||
*
|
||||
* @param {KEY} attributeName The attribute name that is to be set.
|
||||
* @param {Attrs[KEY]} attributeValue The attribute value that is to be set.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
setAttribute (attributeName, attributeValue) {
|
||||
if (this.doc !== null) {
|
||||
transact(this.doc, transaction => {
|
||||
typeMapSet(transaction, this, attributeName, /** @type {any} */ (attributeValue))
|
||||
})
|
||||
} else {
|
||||
/** @type {Map<string, any>} */ (this._prelimAttrs).set(attributeName, attributeValue)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an attribute value that belongs to the attribute name.
|
||||
*
|
||||
* @template {keyof Attrs & string} KEY
|
||||
*
|
||||
* @param {KEY} attributeName The attribute name that identifies the
|
||||
* queried value.
|
||||
* @return {Attrs[KEY]|undefined} The queried attribute value.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
getAttribute (attributeName) {
|
||||
return /** @type {any} */ (typeMapGet(this, attributeName))
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether an attribute exists
|
||||
*
|
||||
* @param {string} attributeName The attribute name to check for existence.
|
||||
* @return {boolean} whether the attribute exists.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
hasAttribute (attributeName) {
|
||||
return /** @type {any} */ (typeMapHas(this, attributeName))
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all attribute name/value pairs in a JSON Object.
|
||||
*
|
||||
* @param {Snapshot} [snapshot]
|
||||
* @return {{ [Key in Extract<keyof Attrs,string>]?: Attrs[Key]}} A JSON Object that describes the attributes.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
getAttributes (snapshot) {
|
||||
return /** @type {any} */ (snapshot ? typeMapGetAllSnapshot(this, snapshot) : typeMapGetAll(this))
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the properties of this type to binary and write it to an
|
||||
* BinaryEncoder.
|
||||
*
|
||||
* This is called when this Item is sent to a remote peer.
|
||||
*
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder The encoder to write data to.
|
||||
*/
|
||||
_write (encoder) {
|
||||
encoder.writeTypeRef(YXmlElementRefID)
|
||||
encoder.writeKey(this.nodeName)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||
* @return {import('../utils/types.js').YType}
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const readYXmlElement = decoder => new YXmlElement(decoder.readKey())
|
||||
@@ -1,266 +0,0 @@
|
||||
/**
|
||||
* @module YXml
|
||||
*/
|
||||
|
||||
import {
|
||||
AbstractType,
|
||||
typeListMap,
|
||||
typeListForEach,
|
||||
typeListInsertGenerics,
|
||||
typeListInsertGenericsAfter,
|
||||
typeListDelete,
|
||||
typeListToArray,
|
||||
YXmlFragmentRefID,
|
||||
transact,
|
||||
typeListGet,
|
||||
typeListSlice,
|
||||
warnPrematureAccess,
|
||||
YXmlElement, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item, YXmlText, YXmlHook // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as delta from 'lib0/delta' // eslint-disable-line
|
||||
import * as error from 'lib0/error'
|
||||
|
||||
/**
|
||||
* Define the elements to which a set of CSS queries apply.
|
||||
* {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors|CSS_Selectors}
|
||||
*
|
||||
* @example
|
||||
* query = '.classSelector'
|
||||
* query = 'nodeSelector'
|
||||
* query = '#idSelector'
|
||||
*
|
||||
* @typedef {string} CSS_Selector
|
||||
*/
|
||||
|
||||
/**
|
||||
* Dom filter function.
|
||||
*
|
||||
* @callback domFilter
|
||||
* @param {string} nodeName The nodeName of the element
|
||||
* @param {Map} attributes The map of attributes.
|
||||
* @return {boolean} Whether to include the Dom node in the YXmlElement.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents a list of {@link YXmlElement}.and {@link YXmlText} types.
|
||||
* A YxmlFragment is similar to a {@link YXmlElement}, but it does not have a
|
||||
* nodeName and it does not have attributes. Though it can be bound to a DOM
|
||||
* element - in this case the attributes and the nodeName are not shared.
|
||||
*
|
||||
* @public
|
||||
* @template {any} [Children=any]
|
||||
* @template {{[K in string]:any}} [Attrs={}]
|
||||
* @extends AbstractType<delta.Delta<any,Attrs,Children,any>>
|
||||
*/
|
||||
export class YXmlFragment extends AbstractType {
|
||||
constructor () {
|
||||
super()
|
||||
/**
|
||||
* @todo remove _prelimContent
|
||||
* @type {Array<any>|null}
|
||||
*/
|
||||
this._prelimContent = []
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {YXmlElement|YXmlText|null}
|
||||
*/
|
||||
get firstChild () {
|
||||
const first = this._first
|
||||
return first ? first.content.getContent()[0] : null
|
||||
}
|
||||
|
||||
/**
|
||||
* Integrate this type into the Yjs instance.
|
||||
*
|
||||
* * Save this struct in the os
|
||||
* * This type is sent to other client
|
||||
* * Observer functions are fired
|
||||
*
|
||||
* @param {Doc} y The Yjs instance
|
||||
* @param {Item?} item
|
||||
*/
|
||||
_integrate (y, item) {
|
||||
super._integrate(y, item)
|
||||
this.insert(0, /** @type {Array<any>} */ (this._prelimContent))
|
||||
this._prelimContent = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a copy of this data type that can be included somewhere else.
|
||||
*
|
||||
* Note that the content is only readable _after_ it has been included somewhere in the Ydoc.
|
||||
*
|
||||
* @return {this}
|
||||
*/
|
||||
clone () {
|
||||
const el = this._copy()
|
||||
el.insert(0, this.toArray().map(item => item instanceof AbstractType ? item.clone() : item))
|
||||
return el
|
||||
}
|
||||
|
||||
get length () {
|
||||
this.doc ?? warnPrematureAccess()
|
||||
return this._prelimContent === null ? this._length : this._prelimContent.length
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the string representation of all the children of this YXmlFragment.
|
||||
*
|
||||
* @return {string} The string representation of all children.
|
||||
*/
|
||||
toString () {
|
||||
return typeListMap(this, xml => xml.toString()).join('')
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
toJSON () {
|
||||
return this.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts new content at an index.
|
||||
*
|
||||
* @example
|
||||
* // Insert character 'a' at position 0
|
||||
* xml.insert(0, [new Y.XmlText('text')])
|
||||
*
|
||||
* @param {number} index The index to insert content at
|
||||
* @param {Array<YXmlElement|YXmlText|YXmlHook>} content The array of content
|
||||
*/
|
||||
insert (index, content) {
|
||||
if (this.doc !== null) {
|
||||
transact(this.doc, transaction => {
|
||||
typeListInsertGenerics(transaction, this, index, content)
|
||||
})
|
||||
} else {
|
||||
// @ts-ignore _prelimContent is defined because this is not yet integrated
|
||||
this._prelimContent.splice(index, 0, ...content)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts new content at an index.
|
||||
*
|
||||
* @example
|
||||
* // Insert character 'a' at position 0
|
||||
* xml.insert(0, [new Y.XmlText('text')])
|
||||
*
|
||||
* @param {null|Item|YXmlElement|YXmlText} ref The index to insert content at
|
||||
* @param {Array<YXmlElement|YXmlText>} content The array of content
|
||||
*/
|
||||
insertAfter (ref, content) {
|
||||
if (this.doc !== null) {
|
||||
transact(this.doc, transaction => {
|
||||
const refItem = (ref && ref instanceof AbstractType) ? ref._item : ref
|
||||
typeListInsertGenericsAfter(transaction, this, refItem, content)
|
||||
})
|
||||
} else {
|
||||
const pc = /** @type {Array<any>} */ (this._prelimContent)
|
||||
const index = ref === null ? 0 : pc.findIndex(el => el === ref) + 1
|
||||
if (index === 0 && ref !== null) {
|
||||
throw error.create('Reference item not found')
|
||||
}
|
||||
pc.splice(index, 0, ...content)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes elements starting from an index.
|
||||
*
|
||||
* @param {number} index Index at which to start deleting elements
|
||||
* @param {number} [length=1] The number of elements to remove. Defaults to 1.
|
||||
*/
|
||||
delete (index, length = 1) {
|
||||
if (this.doc !== null) {
|
||||
transact(this.doc, transaction => {
|
||||
typeListDelete(transaction, this, index, length)
|
||||
})
|
||||
} else {
|
||||
// @ts-ignore _prelimContent is defined because this is not yet integrated
|
||||
this._prelimContent.splice(index, length)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms this YArray to a JavaScript Array.
|
||||
*
|
||||
* @return {Array<YXmlElement|YXmlText|YXmlHook>}
|
||||
*/
|
||||
toArray () {
|
||||
return typeListToArray(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends content to this YArray.
|
||||
*
|
||||
* @param {Array<YXmlElement|YXmlText>} content Array of content to append.
|
||||
*/
|
||||
push (content) {
|
||||
this.insert(this.length, content)
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepends content to this YArray.
|
||||
*
|
||||
* @param {Array<YXmlElement|YXmlText>} content Array of content to prepend.
|
||||
*/
|
||||
unshift (content) {
|
||||
this.insert(0, content)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the i-th element from a YArray.
|
||||
*
|
||||
* @param {number} index The index of the element to return from the YArray
|
||||
* @return {YXmlElement|YXmlText}
|
||||
*/
|
||||
get (index) {
|
||||
return typeListGet(this, index)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a portion of this YXmlFragment into a JavaScript Array selected
|
||||
* from start to end (end not included).
|
||||
*
|
||||
* @param {number} [start]
|
||||
* @param {number} [end]
|
||||
* @return {Array<YXmlElement|YXmlText>}
|
||||
*/
|
||||
slice (start = 0, end = this.length) {
|
||||
return typeListSlice(this, start, end)
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a provided function on once on every child element.
|
||||
*
|
||||
* @param {function(YXmlElement|YXmlText,number, typeof self):void} f A function to execute on every element of this YArray.
|
||||
*/
|
||||
forEach (f) {
|
||||
typeListForEach(this, f)
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the properties of this type to binary and write it to an
|
||||
* BinaryEncoder.
|
||||
*
|
||||
* This is called when this Item is sent to a remote peer.
|
||||
*
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder The encoder to write data to.
|
||||
*/
|
||||
_write (encoder) {
|
||||
encoder.writeTypeRef(YXmlFragmentRefID)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} _decoder
|
||||
* @return {import('../utils/types.js').YType}
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const readYXmlFragment = _decoder => new YXmlFragment()
|
||||
@@ -1,68 +0,0 @@
|
||||
import {
|
||||
YMap,
|
||||
YXmlHookRefID,
|
||||
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2 // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
/**
|
||||
* You can manage binding to a custom type with YXmlHook.
|
||||
*
|
||||
* @extends {YMap<any>}
|
||||
*/
|
||||
export class YXmlHook extends YMap {
|
||||
/**
|
||||
* @param {string} hookName nodeName of the Dom Node.
|
||||
*/
|
||||
constructor (hookName) {
|
||||
super()
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
this.hookName = hookName
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {this}
|
||||
*/
|
||||
_copy () {
|
||||
return /** @type {this} */ (new YXmlHook(this.hookName))
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a copy of this data type that can be included somewhere else.
|
||||
*
|
||||
* Note that the content is only readable _after_ it has been included somewhere in the Ydoc.
|
||||
*
|
||||
* @return {this}
|
||||
*/
|
||||
clone () {
|
||||
const el = this._copy()
|
||||
this.forEach((value, key) => {
|
||||
el.set(key, value)
|
||||
})
|
||||
return el
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the properties of this type to binary and write it to an
|
||||
* BinaryEncoder.
|
||||
*
|
||||
* This is called when this Item is sent to a remote peer.
|
||||
*
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder The encoder to write data to.
|
||||
*/
|
||||
_write (encoder) {
|
||||
encoder.writeTypeRef(YXmlHookRefID)
|
||||
encoder.writeKey(this.hookName)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||
* @return {import('../utils/types.js').YType}
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const readYXmlHook = decoder =>
|
||||
new YXmlHook(decoder.readKey())
|
||||
@@ -1,66 +0,0 @@
|
||||
import {
|
||||
YText,
|
||||
YXmlTextRefID,
|
||||
ContentType, YXmlElement, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
/**
|
||||
* @todo can we deprecate this?
|
||||
*
|
||||
* Represents text in a Dom Element. In the future this type will also handle
|
||||
* simple formatting information like bold and italic.
|
||||
* @extends YText
|
||||
*/
|
||||
export class YXmlText extends YText {
|
||||
/**
|
||||
* @type {YXmlElement|YXmlText|null}
|
||||
*/
|
||||
get nextSibling () {
|
||||
const n = this._item ? this._item.next : null
|
||||
return n ? /** @type {YXmlElement|YXmlText} */ (/** @type {ContentType} */ (n.content).type) : null
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {YXmlElement|YXmlText|null}
|
||||
*/
|
||||
get prevSibling () {
|
||||
const n = this._item ? this._item.prev : null
|
||||
return n ? /** @type {YXmlElement|YXmlText} */ (/** @type {ContentType} */ (n.content).type) : null
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a copy of this data type that can be included somewhere else.
|
||||
*
|
||||
* Note that the content is only readable _after_ it has been included somewhere in the Ydoc.
|
||||
*
|
||||
* @return {this}
|
||||
*/
|
||||
clone () {
|
||||
const text = /** @type {this} */ (this._copy())
|
||||
text.applyDelta(this.getContent())
|
||||
return text
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
toJSON () {
|
||||
return this.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
|
||||
*/
|
||||
_write (encoder) {
|
||||
encoder.writeTypeRef(YXmlTextRefID)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} _decoder
|
||||
* @return {import('../utils/types.js').YType}
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const readYXmlText = _decoder => new YXmlText()
|
||||
@@ -4,13 +4,13 @@
|
||||
|
||||
import {
|
||||
StructStore,
|
||||
YType,
|
||||
transact,
|
||||
applyUpdate,
|
||||
ContentDoc, Item, Transaction, // eslint-disable-line
|
||||
encodeStateAsUpdate
|
||||
} from '../internals.js'
|
||||
|
||||
import { YType } from '../ytype.js'
|
||||
import { ObservableV2 } from 'lib0/observable'
|
||||
import * as random from 'lib0/random'
|
||||
import * as map from 'lib0/map'
|
||||
@@ -190,22 +190,18 @@ export class Doc extends ObservableV2 {
|
||||
/**
|
||||
* Define a shared data type.
|
||||
*
|
||||
* Multiple calls of `ydoc.get(name, TypeConstructor)` yield the same result
|
||||
* Multiple calls of `ydoc.get(name)` yield the same result
|
||||
* and do not overwrite each other. I.e.
|
||||
* `ydoc.get(name, Y.Array) === ydoc.get(name, Y.Array)`
|
||||
* `ydoc.get(name) === ydoc.get(name)`
|
||||
*
|
||||
* After this method is called, the type is also available on `ydoc.share.get(name)`.
|
||||
*
|
||||
* *Best Practices:*
|
||||
* Define all types right after the Y.Doc instance is created and store them in a separate object.
|
||||
* Also use the typed methods `getText(name)`, `getArray(name)`, ..
|
||||
*
|
||||
* @param {string} key
|
||||
* @param {string?} name Type-name
|
||||
*
|
||||
* @return {YType}
|
||||
*/
|
||||
get (key, name = null) {
|
||||
get (key = '', name = null) {
|
||||
return map.setIfUndefined(this.share, key, () => {
|
||||
const t = new YType(name)
|
||||
t._integrate(this, null)
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
IdSet, UpdateEncoderV1, UpdateEncoderV2, GC, StructStore, AbstractStruct, YEvent, Doc // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import {YType} from '../ytype.js'
|
||||
import { YType } from '../ytype.js' // eslint-disable-line
|
||||
import * as error from 'lib0/error'
|
||||
import * as map from 'lib0/map'
|
||||
import * as math from 'lib0/math'
|
||||
@@ -343,7 +343,6 @@ const updateCurrentAttributes = (currentAttributes, { key, value }) => {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Call this function after string content has been deleted in order to
|
||||
* clean up formatting Items.
|
||||
@@ -411,7 +410,6 @@ export const cleanupFormattingGap = (transaction, start, curr, startAttributes,
|
||||
return cleanups
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This function is experimental and subject to change / be removed.
|
||||
*
|
||||
@@ -451,7 +449,6 @@ export const cleanupYTextFormatting = type => {
|
||||
return res
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This will be called by the transaction once the event handlers are called to potentially cleanup
|
||||
* formatting attributes.
|
||||
|
||||
@@ -39,7 +39,7 @@ export class YEvent {
|
||||
*/
|
||||
this.transaction = transaction
|
||||
/**
|
||||
* @type {import('../ytype.js').DeltaConfTypesToDelta<DConf>|null}
|
||||
* @type {delta.Delta<import('../ytype.js').DeltaConfDeltaToYType<DConf>>|null}
|
||||
*/
|
||||
this._delta = null
|
||||
/**
|
||||
@@ -116,7 +116,7 @@ export class YEvent {
|
||||
* @param {AbstractAttributionManager} am
|
||||
* @param {object} [opts]
|
||||
* @param {Deep} [opts.deep]
|
||||
* @return {Deep extends true ? delta.Delta<import('../internals.js').DeltaConfTypesToDelta<DConf>> : delta.Delta<DConf>} The Delta representation of this type.
|
||||
* @return {Deep extends true ? delta.Delta<DConf> : delta.Delta<import('../internals.js').DeltaConfDeltaToYType<DConf>>} The Delta representation of this type.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
@@ -155,7 +155,7 @@ export class YEvent {
|
||||
* Compute the changes in the delta format.
|
||||
* A {@link https://quilljs.com/docs/delta/|Quill Delta}) that represents the changes on the document.
|
||||
*
|
||||
* @type {delta.Delta<DConf>} The Delta representation of this type.
|
||||
* @type {delta.Delta<import('../internals.js').DeltaConfDeltaToYType<DConf>>} The Delta representation of this type.
|
||||
* @public
|
||||
*/
|
||||
get delta () {
|
||||
@@ -166,7 +166,7 @@ export class YEvent {
|
||||
* Compute the changes in the delta format.
|
||||
* A {@link https://quilljs.com/docs/delta/|Quill Delta}) that represents the changes on the document.
|
||||
*
|
||||
* @type {import('../internals.js').DeltaConfTypesToDeltaDelta<DConf>} The Delta representation of this type.
|
||||
* @type {delta.Delta<DConf>} The Delta representation of this type.
|
||||
* @public
|
||||
*/
|
||||
get deltaDeep () {
|
||||
@@ -200,7 +200,7 @@ export const getPathTo = (parent, child, am = noAttributionsManager) => {
|
||||
// parent is map-ish
|
||||
path.unshift(child._item.parentSub)
|
||||
} else {
|
||||
const parent = /** @type {import('../utils/types.js').YType} */ (child._item.parent)
|
||||
const parent = /** @type {import('../ytype.js').YType} */ (child._item.parent)
|
||||
// parent is array-ish
|
||||
const apos = /** @type {AbsolutePosition} */ (createAbsolutePositionFromRelativePosition(createRelativePosition(parent, child._item.id), doc, false, am))
|
||||
path.unshift(apos.index)
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
/**
|
||||
* @typedef {Object<string,any>|Array<any>|number|null|string|Uint8Array|BigInt|import('./').YType} YValue
|
||||
* @typedef {Object<string,any>|Array<any>|number|null|string|Uint8Array|BigInt|import('../ytype.js').YType} YValue
|
||||
*/
|
||||
@@ -584,13 +584,13 @@ export const convertUpdateFormat = (update, blockTransformer, YDecoder, YEncoder
|
||||
* @typedef {Object} ObfuscatorOptions
|
||||
* @property {boolean} [ObfuscatorOptions.formatting=true]
|
||||
* @property {boolean} [ObfuscatorOptions.subdocs=true]
|
||||
* @property {boolean} [ObfuscatorOptions.yxml=true] Whether to obfuscate nodeName / hookName
|
||||
* @property {boolean} [ObfuscatorOptions.name=true] Whether to obfuscate nodeName / hookName
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {ObfuscatorOptions} obfuscator
|
||||
*/
|
||||
const createObfuscator = ({ formatting = true, subdocs = true, yxml = true } = {}) => {
|
||||
const createObfuscator = ({ formatting = true, subdocs = true, name = true } = {}) => {
|
||||
let i = 0
|
||||
const mapKeyCache = map.create()
|
||||
const nodeNameCache = map.create()
|
||||
@@ -613,10 +613,10 @@ const createObfuscator = ({ formatting = true, subdocs = true, yxml = true } = {
|
||||
case ContentDeleted:
|
||||
break
|
||||
case ContentType: {
|
||||
if (yxml) {
|
||||
if (name) {
|
||||
const type = /** @type {ContentType} */ (content).type
|
||||
if (type.name != null) {
|
||||
type.name = map.setIfUndefined(nodeNameCache, type.nodeName, () => 'typename-' + i)
|
||||
type.name = map.setIfUndefined(nodeNameCache, type.name, () => 'typename-' + i)
|
||||
}
|
||||
}
|
||||
break
|
||||
|
||||
333
src/ytype.js
333
src/ytype.js
@@ -184,63 +184,6 @@ export class ItemTextListPosition {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {ItemTextListPosition} pos
|
||||
* @param {number} count steps to move forward
|
||||
* @return {ItemTextListPosition}
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
const findNextPosition = (transaction, pos, count) => {
|
||||
while (pos.right !== null && count > 0) {
|
||||
switch (pos.right.content.constructor) {
|
||||
case ContentFormat:
|
||||
if (!pos.right.deleted) {
|
||||
updateCurrentAttributes(pos.currentAttributes, /** @type {ContentFormat} */ (pos.right.content))
|
||||
}
|
||||
break
|
||||
default:
|
||||
if (!pos.right.deleted) {
|
||||
if (count < pos.right.length) {
|
||||
// split right
|
||||
getItemCleanStart(transaction, createID(pos.right.id.client, pos.right.id.clock + count))
|
||||
}
|
||||
pos.index += pos.right.length
|
||||
count -= pos.right.length
|
||||
}
|
||||
break
|
||||
}
|
||||
pos.left = pos.right
|
||||
pos.right = pos.right.right
|
||||
// pos.forward() - we don't forward because that would halve the performance because we already do the checks above
|
||||
}
|
||||
return pos
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
* @param {YType} parent
|
||||
* @param {number} index
|
||||
* @param {boolean} useSearchMarker
|
||||
* @return {ItemTextListPosition}
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
const findPosition = (transaction, parent, index, useSearchMarker) => {
|
||||
const currentAttributes = new Map()
|
||||
const marker = useSearchMarker ? findMarker(parent, index) : null
|
||||
if (marker) {
|
||||
const pos = new ItemTextListPosition(marker.p.left, marker.p, marker.index, currentAttributes, noAttributionsManager)
|
||||
return findNextPosition(transaction, pos, index - marker.index)
|
||||
} else {
|
||||
const pos = new ItemTextListPosition(null, parent._start, 0, currentAttributes, noAttributionsManager)
|
||||
return findNextPosition(transaction, pos, index)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Negate applied formats
|
||||
*
|
||||
@@ -696,12 +639,22 @@ export class YType {
|
||||
this._hasFormatting = false
|
||||
}
|
||||
|
||||
/**
|
||||
* @template {delta.DeltaConf} DC
|
||||
* @param {delta.Delta<DC>} d
|
||||
* @return {YType<DC>}
|
||||
*/
|
||||
static from (d) {
|
||||
const yt = new YType(d.name)
|
||||
yt.applyDelta(d)
|
||||
return yt
|
||||
}
|
||||
|
||||
get length () {
|
||||
this.doc ?? warnPrematureAccess()
|
||||
return this._length
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns a fresh delta that can be used to change this YType.
|
||||
* @type {delta.DeltaBuilder<DeltaToYType<DConf>>}
|
||||
@@ -1075,13 +1028,7 @@ export class YType {
|
||||
for (let i = 0; i < op.insert.length; i++) {
|
||||
let ins = op.insert[i]
|
||||
if (delta.$deltaAny.check(ins)) {
|
||||
if (ins.name != null) {
|
||||
const t = new YType(ins.name)
|
||||
t.applyDelta(ins)
|
||||
ins = t
|
||||
} else {
|
||||
error.unexpectedCase()
|
||||
}
|
||||
ins = YType.from(ins)
|
||||
}
|
||||
insertText(transaction, /** @type {any} */ (this), currPos, ins, op.format || {})
|
||||
}
|
||||
@@ -1103,7 +1050,7 @@ export class YType {
|
||||
for (const op of d.attrs) {
|
||||
if (delta.$setAttrOp.check(op)) {
|
||||
typeMapSet(transaction, /** @type {any} */ (this), /** @type {any} */ (op.key), op.value)
|
||||
} else if (delta.$deleteOp.check(op)) {
|
||||
} else if (delta.$deleteAttrOp.check(op)) {
|
||||
typeMapDelete(transaction, /** @type {any} */ (this), /** @type {any} */ (op.key))
|
||||
} else {
|
||||
const sub = typeMapGet(/** @type {any} */ (this), /** @type {any} */ (op.key))
|
||||
@@ -1115,6 +1062,7 @@ export class YType {
|
||||
}
|
||||
})
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1134,14 +1082,13 @@ export class YType {
|
||||
* Removes all elements from this YMap.
|
||||
*/
|
||||
clearAttrs () {
|
||||
let d = delta.create()
|
||||
this.forEachAttr((_,key) => {
|
||||
const d = delta.create()
|
||||
this.forEachAttr((_, key) => {
|
||||
d.deleteAttr(/** @type {any} */ (key))
|
||||
})
|
||||
this.applyDelta(d)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Removes an attribute from this YXmlElement.
|
||||
*
|
||||
@@ -1163,8 +1110,9 @@ export class YType {
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
setAttribute (attributeName, attributeValue) {
|
||||
setAttr (attributeName, attributeValue) {
|
||||
this.applyDelta(delta.create().setAttr(attributeName, attributeValue).done())
|
||||
return attributeValue
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1175,7 +1123,7 @@ export class YType {
|
||||
* @return {delta.DeltaConfGetAttrs<DConf>[KEY]|undefined} The queried attribute value.
|
||||
* @public
|
||||
*/
|
||||
getAttribute (attributeName) {
|
||||
getAttr (attributeName) {
|
||||
return /** @type {any} */ (typeMapGet(this, attributeName))
|
||||
}
|
||||
|
||||
@@ -1187,7 +1135,7 @@ export class YType {
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
hasAttribute (attributeName) {
|
||||
hasAttr (attributeName) {
|
||||
return /** @type {any} */ (typeMapHas(this, attributeName))
|
||||
}
|
||||
|
||||
@@ -1199,7 +1147,7 @@ export class YType {
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
getAttributes (snapshot) {
|
||||
getAttrs (snapshot) {
|
||||
return /** @type {any} */ (snapshot ? typeMapGetAllSnapshot(this, snapshot) : typeMapGetAll(this))
|
||||
}
|
||||
|
||||
@@ -1218,9 +1166,54 @@ export class YType {
|
||||
*
|
||||
* @param {number} index The index to insert content at.
|
||||
* @param {Array<delta.DeltaConfGetChildren<DConf>>|delta.DeltaConfGetText<DConf>} content Array of content to append.
|
||||
* @param {delta.FormattingAttributes} [format]
|
||||
*/
|
||||
insert (index, content) {
|
||||
this.applyDelta(delta.create().retain(index).insert(/** @type {any} */ (content)))
|
||||
insert (index, content, format) {
|
||||
this.applyDelta(delta.create().retain(index).insert(/** @type {any} */ (content), format))
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(1, [1, 2])
|
||||
*
|
||||
* @param {number} index The index to insert content at.
|
||||
* @param {number} length The index to insert content at.
|
||||
* @param {delta.FormattingAttributes} formats
|
||||
*
|
||||
*/
|
||||
format (index, length, formats) {
|
||||
this.applyDelta(delta.create().retain(index).retain(length, formats))
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts new content after another element.
|
||||
*
|
||||
* @example
|
||||
* // Insert character 'a' at position 0
|
||||
* xml.insert(0, [new Y.XmlText('text')])
|
||||
*
|
||||
* @param {null|Item|YType} ref The index to insert content at
|
||||
* @param {Array<delta.DeltaConfGetChildren<DConf>>} content The array of content
|
||||
*/
|
||||
insertAfter (ref, content) {
|
||||
if (this.doc !== null) {
|
||||
transact(this.doc, transaction => {
|
||||
const refItem = ref && ref instanceof YType ? ref._item : ref
|
||||
typeListInsertGenericsAfter(transaction, this, refItem, content)
|
||||
})
|
||||
} else {
|
||||
// only possible once this item has been integrated
|
||||
error.unexpectedCase()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1275,21 +1268,38 @@ export class YType {
|
||||
return typeListSlice(this, start, end)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @todo refactor this, this should use getContent only!
|
||||
*
|
||||
* Transforms this YArray to a JavaScript Array.
|
||||
*
|
||||
* @return {Array<delta.DeltaConfGetChildren<DConf>>}
|
||||
* @return {Array<delta.DeltaConfGetChildren<DConf> | delta.DeltaConfGetText<DConf>>}
|
||||
*/
|
||||
toArray () {
|
||||
return typeListToArray(this)
|
||||
const dcontent = this.getContent()
|
||||
/**
|
||||
* @type {Array<any>}
|
||||
*/
|
||||
const children = []
|
||||
for (const child of dcontent.children) {
|
||||
if (delta.$insertOp.check(child) || delta.$textOp.check(child)) {
|
||||
children.push(child.insert)
|
||||
}
|
||||
}
|
||||
return children
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms this Shared Type to a JSON object.
|
||||
*/
|
||||
toJSON () {
|
||||
return this.getContent().toJSON()
|
||||
const attrs = this.getAttrs()
|
||||
const children = this.toArray()
|
||||
return {
|
||||
name: this.name,
|
||||
children,
|
||||
attrs
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1297,22 +1307,21 @@ export class YType {
|
||||
* child-element.
|
||||
*
|
||||
* @template M
|
||||
* @param {(child:delta.DeltaConfGetChildren<DConf>,index:number,ytype:this)=>M} f Function that produces an element of the new Array
|
||||
* @param {(child:delta.DeltaConfGetChildren<DConf>|delta.DeltaConfGetText<DConf>,index:number)=>M} f Function that produces an element of the new Array
|
||||
* @return {Array<M>} A new array with each element being the result of the
|
||||
* callback function
|
||||
*/
|
||||
map (f) {
|
||||
return typeListMap(this, /** @type {any} */ (f))
|
||||
return this.toArray().map(f)
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a provided function once on every element of this YArray.
|
||||
*
|
||||
* @template M
|
||||
* @param {(child:delta.DeltaConfGetChildren<DConf>,index:number,ytype:this)=>M} f Function that produces an element of the new Array
|
||||
* @param {(child:delta.DeltaConfGetChildren<DConf>|delta.DeltaConfGetText<DConf>,index:number)=>any} f Function that produces an element of the new Array
|
||||
*/
|
||||
forEach (f) {
|
||||
typeListForEach(this, f)
|
||||
return this.toArray().forEach(f)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1328,19 +1337,10 @@ export class YType {
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @return {IterableIterator<delta.DeltaConfGetChildren<DConf>>}
|
||||
*/
|
||||
[Symbol.iterator] () {
|
||||
return typeListCreateIterator(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the keys for each element in the YMap Type.
|
||||
*
|
||||
* @return {IterableIterator<keyof delta.DeltaConfGetAttrs<DConf>>}
|
||||
* @return {IterableIterator<import('lib0/ts').KeyOf<delta.DeltaConfGetAttrs<DConf>>>}
|
||||
*/
|
||||
attrKeys () {
|
||||
return iterator.iteratorMap(createMapIterator(this), /** @param {any} v */ v => v[0])
|
||||
@@ -1358,7 +1358,7 @@ export class YType {
|
||||
/**
|
||||
* Returns an Iterator of [key, value] pairs
|
||||
*
|
||||
* @return {IterableIterator<{ [K in keyof delta.DeltaConfGetAttrs<DConf>]: [K,delta.DeltaConfGetAttrs<DConf>] }[any]>}
|
||||
* @return {IterableIterator<{ [K in keyof delta.DeltaConfGetAttrs<DConf>]: [K,delta.DeltaConfGetAttrs<DConf>[K]] }[any]>}
|
||||
*/
|
||||
attrEntries () {
|
||||
return iterator.iteratorMap(createMapIterator(this), /** @param {any} v */ v => /** @type {any} */ ([v[0], v[1].content.getContent()[v[1].length - 1]]))
|
||||
@@ -1369,7 +1369,7 @@ export class YType {
|
||||
*
|
||||
* @return {number}
|
||||
*/
|
||||
attrSize () {
|
||||
get attrSize () {
|
||||
return [...createMapIterator(this)].length
|
||||
}
|
||||
|
||||
@@ -1379,7 +1379,7 @@ export class YType {
|
||||
[traits.EqualityTraitSymbol] (other) {
|
||||
return this.getContent().equals(other.getContent())
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @todo this doesn't need to live in a method.
|
||||
*
|
||||
@@ -1475,145 +1475,6 @@ export const typeListSlice = (type, start, end) => {
|
||||
return cs
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {YType} type
|
||||
* @return {Array<any>}
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const typeListToArray = type => {
|
||||
type.doc ?? warnPrematureAccess()
|
||||
const cs = []
|
||||
let n = type._start
|
||||
while (n !== null) {
|
||||
if (n.countable && !n.deleted) {
|
||||
const c = n.content.getContent()
|
||||
for (let i = 0; i < c.length; i++) {
|
||||
cs.push(c[i])
|
||||
}
|
||||
}
|
||||
n = n.right
|
||||
}
|
||||
return cs
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {YType<any>} type
|
||||
* @param {Snapshot} snapshot
|
||||
* @return {Array<any>}
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const typeListToArraySnapshot = (type, snapshot) => {
|
||||
const cs = []
|
||||
let n = type._start
|
||||
while (n !== null) {
|
||||
if (n.countable && isVisible(n, snapshot)) {
|
||||
const c = n.content.getContent()
|
||||
for (let i = 0; i < c.length; i++) {
|
||||
cs.push(c[i])
|
||||
}
|
||||
}
|
||||
n = n.right
|
||||
}
|
||||
return cs
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a provided function on once on every element of this YArray.
|
||||
*
|
||||
* @param {YType<any>} type
|
||||
* @param {function(any,number,any):void} f A function to execute on every element of this YArray.
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const typeListForEach = (type, f) => {
|
||||
let index = 0
|
||||
let n = type._start
|
||||
type.doc ?? warnPrematureAccess()
|
||||
while (n !== null) {
|
||||
if (n.countable && !n.deleted) {
|
||||
const c = n.content.getContent()
|
||||
for (let i = 0; i < c.length; i++) {
|
||||
f(c[i], index++, type)
|
||||
}
|
||||
}
|
||||
n = n.right
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @template C,R
|
||||
* @param {YType<any>} type
|
||||
* @param {function(C,number,YType<any>):R} f
|
||||
* @return {Array<R>}
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const typeListMap = (type, f) => {
|
||||
/**
|
||||
* @type {Array<any>}
|
||||
*/
|
||||
const result = []
|
||||
typeListForEach(type, (c, i) => {
|
||||
result.push(f(c, i, type))
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {YType} type
|
||||
* @return {IterableIterator<any>}
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
export const typeListCreateIterator = type => {
|
||||
let n = type._start
|
||||
/**
|
||||
* @type {Array<any>|null}
|
||||
*/
|
||||
let currentContent = null
|
||||
let currentContentIndex = 0
|
||||
return {
|
||||
[Symbol.iterator] () {
|
||||
return this
|
||||
},
|
||||
next: () => {
|
||||
// find some content
|
||||
if (currentContent === null) {
|
||||
while (n !== null && n.deleted) {
|
||||
n = n.right
|
||||
}
|
||||
// check if we reached the end, no need to check currentContent, because it does not exist
|
||||
if (n === null) {
|
||||
return {
|
||||
done: true,
|
||||
value: undefined
|
||||
}
|
||||
}
|
||||
// we found n, so we can set currentContent
|
||||
currentContent = n.content.getContent()
|
||||
currentContentIndex = 0
|
||||
n = n.right // we used the content of n, now iterate to next
|
||||
}
|
||||
const value = currentContent[currentContentIndex++]
|
||||
// check if we need to empty currentContent
|
||||
if (currentContent.length <= currentContentIndex) {
|
||||
currentContent = null
|
||||
}
|
||||
return {
|
||||
done: false,
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo remove / inline this
|
||||
*
|
||||
|
||||
@@ -14,7 +14,7 @@ import * as delta from 'lib0/delta'
|
||||
*/
|
||||
export const testRelativePositions = _tc => {
|
||||
const ydoc = new Y.Doc()
|
||||
const ytext = ydoc.getText()
|
||||
const ytext = ydoc.get()
|
||||
ytext.insert(0, 'hello world')
|
||||
const v1 = Y.cloneDoc(ydoc)
|
||||
ytext.delete(1, 6)
|
||||
@@ -32,7 +32,7 @@ export const testRelativePositions = _tc => {
|
||||
*/
|
||||
export const testAttributedEvents = _tc => {
|
||||
const ydoc = new Y.Doc()
|
||||
const ytext = ydoc.getText()
|
||||
const ytext = ydoc.get()
|
||||
ytext.insert(0, 'hello world')
|
||||
const v1 = Y.cloneDoc(ydoc)
|
||||
ydoc.transact(() => {
|
||||
@@ -56,7 +56,7 @@ export const testAttributedEvents = _tc => {
|
||||
*/
|
||||
export const testInsertionsMindingAttributedContent = _tc => {
|
||||
const ydoc = new Y.Doc()
|
||||
const ytext = ydoc.getText()
|
||||
const ytext = ydoc.get()
|
||||
ytext.insert(0, 'hello world')
|
||||
const v1 = Y.cloneDoc(ydoc)
|
||||
ydoc.transact(() => {
|
||||
@@ -74,7 +74,7 @@ export const testInsertionsMindingAttributedContent = _tc => {
|
||||
*/
|
||||
export const testInsertionsIntoAttributedContent = _tc => {
|
||||
const ydoc = new Y.Doc()
|
||||
const ytext = ydoc.getText()
|
||||
const ytext = ydoc.get()
|
||||
ytext.insert(0, 'hello ')
|
||||
const v1 = Y.cloneDoc(ydoc)
|
||||
ydoc.transact(() => {
|
||||
@@ -89,15 +89,15 @@ export const testInsertionsIntoAttributedContent = _tc => {
|
||||
|
||||
export const testYdocDiff = () => {
|
||||
const ydocStart = new Y.Doc()
|
||||
ydocStart.getText('text').insert(0, 'hello')
|
||||
ydocStart.getArray('array').insert(0, [1, 2, 3])
|
||||
ydocStart.getMap('map').set('k', 42)
|
||||
ydocStart.getMap('map').set('nested', new Y.Array())
|
||||
ydocStart.get('text').insert(0, 'hello')
|
||||
ydocStart.get('array').insert(0, [1, 2, 3])
|
||||
ydocStart.get('map').setAttr('k', 42)
|
||||
ydocStart.get('map').setAttr('nested', new Y.Type())
|
||||
const ydocUpdated = Y.cloneDoc(ydocStart)
|
||||
ydocUpdated.getText('text').insert(5, ' world')
|
||||
ydocUpdated.getArray('array').insert(1, ['x'])
|
||||
ydocUpdated.getMap('map').set('newk', 42)
|
||||
ydocUpdated.getMap('map').get('nested').insert(0, [1])
|
||||
ydocUpdated.get('text').insert(5, ' world')
|
||||
ydocUpdated.get('array').insert(1, ['x'])
|
||||
ydocUpdated.get('map').setAttr('newk', 42)
|
||||
ydocUpdated.get('map').getAttr('nested').insert(0, [1])
|
||||
// @todo add custom attribution
|
||||
const d = Y.diffDocsToDelta(ydocStart, ydocUpdated)
|
||||
console.log('calculated diff', d.toJSON())
|
||||
@@ -111,19 +111,18 @@ export const testYdocDiff = () => {
|
||||
export const testChildListContent = () => {
|
||||
const ydocStart = new Y.Doc()
|
||||
const ydocUpdated = Y.cloneDoc(ydocStart)
|
||||
const yf = new Y.XmlElement('test')
|
||||
const yf = new Y.Type('test')
|
||||
let calledEvent = 0
|
||||
yf.applyDelta(delta.create().insert('test content').setAttr('k', 'v'))
|
||||
|
||||
const yarray = ydocUpdated.getArray('array')
|
||||
yarray.observeDeep((events, tr) => {
|
||||
const yarray = ydocUpdated.get('array')
|
||||
yarray.observeDeep(event => {
|
||||
calledEvent++
|
||||
const event = events.find(event => event.target === yarray) || new Y.YEvent(yarray, tr, new Set(null))
|
||||
const d = event.deltaDeep
|
||||
const expectedD = delta.create().insert([delta.create('test').insert('test content').setAttr('k', 'v')])
|
||||
t.compare(d, expectedD)
|
||||
})
|
||||
ydocUpdated.getArray('array').insert(0, [yf])
|
||||
ydocUpdated.get('array').insert(0, [yf])
|
||||
t.assert(calledEvent === 1)
|
||||
const d = Y.diffDocsToDelta(ydocStart, ydocUpdated)
|
||||
console.log('calculated diff', d.toJSON())
|
||||
|
||||
@@ -6,7 +6,7 @@ import * as t from 'lib0/testing'
|
||||
*/
|
||||
export const testAfterTransactionRecursion = _tc => {
|
||||
const ydoc = new Y.Doc()
|
||||
const yxml = ydoc.getXmlFragment('')
|
||||
const yxml = ydoc.get('')
|
||||
ydoc.on('afterTransaction', tr => {
|
||||
if (tr.origin === 'test') {
|
||||
yxml.toJSON()
|
||||
@@ -14,7 +14,7 @@ export const testAfterTransactionRecursion = _tc => {
|
||||
})
|
||||
ydoc.transact(_tr => {
|
||||
for (let i = 0; i < 15000; i++) {
|
||||
yxml.push([new Y.XmlText('a')])
|
||||
yxml.push([new Y.Type('a')])
|
||||
}
|
||||
}, 'test')
|
||||
}
|
||||
@@ -24,12 +24,12 @@ export const testAfterTransactionRecursion = _tc => {
|
||||
*/
|
||||
export const testFindTypeInOtherDoc = _tc => {
|
||||
const ydoc = new Y.Doc()
|
||||
const ymap = ydoc.getMap()
|
||||
const ytext = ymap.set('ytext', new Y.Text())
|
||||
const ymap = ydoc.get()
|
||||
const ytext = ymap.setAttr('ytext', new Y.Type())
|
||||
const ydocClone = new Y.Doc()
|
||||
Y.applyUpdate(ydocClone, Y.encodeStateAsUpdate(ydoc))
|
||||
/**
|
||||
* @template {import('../src/utils/types.js').YType} Type
|
||||
* @template {Y.Type} Type
|
||||
* @param {Type} ytype
|
||||
* @param {Y.Doc} otherYdoc
|
||||
* @return {Type}
|
||||
@@ -47,7 +47,7 @@ export const testFindTypeInOtherDoc = _tc => {
|
||||
if (rootKey == null) {
|
||||
throw new Error('type does not exist in other ydoc')
|
||||
}
|
||||
return /** @type {Type} */ (otherYdoc.get(rootKey, /** @type {import('../src/utils/types.js').YTypeConstructors} */ (ytype.constructor)))
|
||||
return /** @type {Type} */ (otherYdoc.get(rootKey, /** @type {import('../src/utils/ts.js').YTypeConstructors} */ (ytype.constructor)))
|
||||
} else {
|
||||
/**
|
||||
* If it is a sub type, we use the item id to find the history type.
|
||||
|
||||
@@ -24,9 +24,9 @@ export const testBasicXmlAttributes = _tc => {
|
||||
yxml.setAttribute('a', '1')
|
||||
const snapshot2 = Y.snapshot(ydoc)
|
||||
yxml.setAttribute('a', '2')
|
||||
t.compare(yxml.getAttributes(), { a: '2' })
|
||||
t.compare(yxml.getAttributes(snapshot2), { a: '1' })
|
||||
t.compare(yxml.getAttributes(snapshot1), {})
|
||||
t.compare(yxml.getAttrs(), { a: '2' })
|
||||
t.compare(yxml.getAttrs(snapshot2), { a: '1' })
|
||||
t.compare(yxml.getAttrs(snapshot1), {})
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -270,7 +270,7 @@ export class TestConnector {
|
||||
* @param {t.TestCase} tc
|
||||
* @param {{users?:number}} conf
|
||||
* @param {InitTestObjectCallback<T>} [initTestObject]
|
||||
* @return {{testObjects:Array<any>,testConnector:TestConnector,users:Array<TestYInstance>,array0:Y.Array<any>,array1:Y.Array<any>,array2:Y.Array<any>,map0:Y.Map<any>,map1:Y.Map<any>,map2:Y.Map<any>,map3:Y.Map<any>,text0:Y.Text,text1:Y.Text,text2:Y.Text,xml0:Y.XmlElement,xml1:Y.XmlElement,xml2:Y.XmlElement}}
|
||||
* @return {{testObjects:Array<any>,testConnector:TestConnector,users:Array<TestYInstance>,array0:Y.Type<any>,array1:Y.Type<any>,array2:Y.Type<any>,map0:Y.Type<any>,map1:Y.Type<any>,map2:Y.Type<any>,map3:Y.Type<any>,text0:Y.Type,text1:Y.Type,text2:Y.Type,xml0:Y.Type,xml1:Y.Type,xml2:Y.Type}}
|
||||
*/
|
||||
export const init = (tc, { users = 5 } = {}, initTestObject) => {
|
||||
/**
|
||||
@@ -293,10 +293,10 @@ export const init = (tc, { users = 5 } = {}, initTestObject) => {
|
||||
const y = testConnector.createY(i)
|
||||
y.clientID = i
|
||||
result.users.push(y)
|
||||
result['array' + i] = y.getArray('array')
|
||||
result['map' + i] = y.getMap('map')
|
||||
result['xml' + i] = y.get('xml', Y.XmlElement)
|
||||
result['text' + i] = y.getText('text')
|
||||
result['array' + i] = y.get('array')
|
||||
result['map' + i] = y.get('map')
|
||||
result['xml' + i] = y.get('xml')
|
||||
result['text' + i] = y.get('text')
|
||||
}
|
||||
testConnector.syncAll()
|
||||
result.testObjects = result.users.map(initTestObject || (() => null))
|
||||
@@ -458,37 +458,37 @@ export const compare = users => {
|
||||
return ydoc
|
||||
})
|
||||
users.push(.../** @type {any} */(mergedDocs))
|
||||
const userArrayValues = users.map(u => u.getArray('array').toJSON())
|
||||
const userMapValues = users.map(u => u.getMap('map').toJSON())
|
||||
const userArrayValues = users.map(u => u.get('array').toJSON().children || [])
|
||||
const userMapValues = users.map(u => u.get('map').toJSON())
|
||||
// @todo fix type error here
|
||||
// @ts-ignore
|
||||
const userXmlValues = users.map(u => /** @type {Y.XmlElement} */ (u.get('xml', Y.XmlElement)).toString())
|
||||
const userTextValues = users.map(u => u.getText('text').getContentDeep())
|
||||
const userTextValues = users.map(u => u.get('text').getContentDeep())
|
||||
for (const u of users) {
|
||||
t.assert(u.store.pendingDs === null)
|
||||
t.assert(u.store.pendingStructs === null)
|
||||
}
|
||||
// Test Array iterator
|
||||
t.compare(users[0].getArray('array').toArray(), Array.from(users[0].getArray('array')))
|
||||
t.compare(users[0].get('array').toArray(), Array.from(users[0].get('array')))
|
||||
// Test Map iterator
|
||||
const ymapkeys = Array.from(users[0].getMap('map').keys())
|
||||
const ymapkeys = Array.from(users[0].get('map').attrKeys())
|
||||
t.assert(ymapkeys.length === Object.keys(userMapValues[0]).length)
|
||||
ymapkeys.forEach(key => t.assert(object.hasProperty(userMapValues[0], key)))
|
||||
/**
|
||||
* @type {Object<string,any>}
|
||||
*/
|
||||
const mapRes = {}
|
||||
for (const [k, v] of users[0].getMap('map')) {
|
||||
for (const [k, v] of users[0].get('map')) {
|
||||
mapRes[k] = v instanceof Y.AbstractType ? v.toJSON() : v
|
||||
}
|
||||
t.compare(userMapValues[0], mapRes)
|
||||
// Compare all users
|
||||
for (let i = 0; i < users.length - 1; i++) {
|
||||
t.compare(userArrayValues[i].length, users[i].getArray('array').length)
|
||||
t.compare(userArrayValues[i].length, users[i].get('array').length)
|
||||
t.compare(userArrayValues[i], userArrayValues[i + 1])
|
||||
t.compare(userMapValues[i], userMapValues[i + 1])
|
||||
t.compare(userXmlValues[i], userXmlValues[i + 1])
|
||||
t.compare(list.toArray(userTextValues[i].children).map(a => (delta.$textOp.check(a) || delta.$insertOp.check(a)) ? a.insert.length : 0).reduce((a, b) => a + b, 0), users[i].getText('text').length)
|
||||
t.compare(list.toArray(userTextValues[i].children).map(a => (delta.$textOp.check(a) || delta.$insertOp.check(a)) ? a.insert.length : 0).reduce((a, b) => a + b, 0), users[i].get('text').length)
|
||||
t.compare(userTextValues[i], userTextValues[i + 1], '', (_constructor, a, b) => {
|
||||
if (a instanceof Y.AbstractType) {
|
||||
t.compare(a.toJSON(), b.toJSON())
|
||||
|
||||
@@ -111,7 +111,7 @@ export const testMergeUpdates = tc => {
|
||||
compare(users)
|
||||
encoders.forEach(enc => {
|
||||
const merged = fromUpdates(users, enc)
|
||||
t.compareArrays(array0.toArray(), merged.getArray('array').toArray())
|
||||
t.compareArrays(array0.toArray(), merged.get('array').toArray())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -184,7 +184,7 @@ const checkUpdateCases = (ydoc, updates, enc, hasDeletes) => {
|
||||
// enc.logUpdate(updates)
|
||||
const merged = new Y.Doc({ gc: false })
|
||||
enc.applyUpdate(merged, mergedUpdates)
|
||||
t.compareArrays(merged.getArray().toArray(), ydoc.getArray().toArray())
|
||||
t.compareArrays(merged.get().toArray(), ydoc.get().toArray())
|
||||
t.compare(enc.encodeStateVector(merged), enc.encodeStateVectorFromUpdate(mergedUpdates))
|
||||
if (enc.updateEventName !== 'update') { // @todo should this also work on legacy updates?
|
||||
for (let j = 1; j < updates.length; j++) {
|
||||
@@ -235,7 +235,7 @@ export const testMergeUpdates1 = _tc => {
|
||||
const ydoc = new Y.Doc({ gc: false })
|
||||
const updates = /** @type {Array<Uint8Array<ArrayBuffer>>} */ ([])
|
||||
ydoc.on(enc.updateEventName, update => { updates.push(update) })
|
||||
const array = ydoc.getArray()
|
||||
const array = ydoc.get()
|
||||
array.insert(0, [1])
|
||||
array.insert(0, [2])
|
||||
array.insert(0, [3])
|
||||
@@ -253,7 +253,7 @@ export const testMergeUpdates2 = _tc => {
|
||||
const ydoc = new Y.Doc({ gc: false })
|
||||
const updates = /** @type {Array<Uint8Array<ArrayBuffer>>} */ ([])
|
||||
ydoc.on(enc.updateEventName, update => { updates.push(update) })
|
||||
const array = ydoc.getArray()
|
||||
const array = ydoc.get()
|
||||
array.insert(0, [1, 2])
|
||||
array.delete(1, 1)
|
||||
array.insert(0, [3, 4])
|
||||
@@ -274,7 +274,7 @@ export const testMergePendingUpdates = _tc => {
|
||||
yDoc.on('update', (update, _origin, _c) => {
|
||||
serverUpdates.splice(serverUpdates.length, 0, update)
|
||||
})
|
||||
const yText = yDoc.getText('textBlock')
|
||||
const yText = yDoc.get('textBlock')
|
||||
yText.applyDelta(delta.create().insert('r'))
|
||||
yText.applyDelta(delta.create().insert('o'))
|
||||
yText.applyDelta(delta.create().insert('n'))
|
||||
@@ -305,7 +305,7 @@ export const testMergePendingUpdates = _tc => {
|
||||
Y.applyUpdate(yDoc5, serverUpdates[4])
|
||||
Y.encodeStateAsUpdate(yDoc5)
|
||||
|
||||
const yText5 = yDoc5.getText('textBlock')
|
||||
const yText5 = yDoc5.get('textBlock')
|
||||
t.compareStrings(yText5.toString(), 'nenor')
|
||||
}
|
||||
|
||||
@@ -314,26 +314,26 @@ export const testMergePendingUpdates = _tc => {
|
||||
*/
|
||||
export const testObfuscateUpdates = _tc => {
|
||||
const ydoc = new Y.Doc()
|
||||
const ytext = ydoc.getText('text')
|
||||
const ymap = ydoc.getMap('map')
|
||||
const yarray = ydoc.getArray('array')
|
||||
const ytext = ydoc.get('text')
|
||||
const ymap = ydoc.get('map')
|
||||
const yarray = ydoc.get('array')
|
||||
// test ytext
|
||||
ytext.applyDelta(delta.create().insert('text', { bold: true }).insert([{ href: 'supersecreturl' }]))
|
||||
// test ymap
|
||||
ymap.set('key', 'secret1')
|
||||
ymap.set('key', 'secret2')
|
||||
ymap.setAttr('key', 'secret1')
|
||||
ymap.setAttr('key', 'secret2')
|
||||
// test yarray with subtype & subdoc
|
||||
const subtype = new Y.XmlElement('secretnodename')
|
||||
const subtype = new Y.Type('secretnodename')
|
||||
const subdoc = new Y.Doc({ guid: 'secret' })
|
||||
subtype.setAttribute('attr', 'val')
|
||||
subtype.setAttr('attr', 'val')
|
||||
yarray.insert(0, ['teststring', 42, subtype, subdoc])
|
||||
// obfuscate the content and put it into a new document
|
||||
const obfuscatedUpdate = Y.obfuscateUpdate(Y.encodeStateAsUpdate(ydoc))
|
||||
const odoc = new Y.Doc()
|
||||
Y.applyUpdate(odoc, obfuscatedUpdate)
|
||||
const otext = odoc.getText('text')
|
||||
const omap = odoc.getMap('map')
|
||||
const oarray = odoc.getArray('array')
|
||||
const otext = odoc.get('text')
|
||||
const omap = odoc.get('map')
|
||||
const oarray = odoc.get('array')
|
||||
// test ytext
|
||||
const d = /** @type {any} */ (otext.getContent().toJSON().children)
|
||||
t.assert(d.length === 2)
|
||||
@@ -343,19 +343,19 @@ export const testObfuscateUpdates = _tc => {
|
||||
t.assert(object.length(d[1].insert) === 1)
|
||||
t.assert(object.hasProperty(d[1], 'insert'))
|
||||
// test ymap
|
||||
t.assert(omap.size === 1)
|
||||
t.assert(!omap.has('key'))
|
||||
t.assert(omap.attrSize === 1)
|
||||
t.assert(!omap.hasAttr('key'))
|
||||
// test yarray with subtype & subdoc
|
||||
const result = oarray.toArray()
|
||||
t.assert(result.length === 4)
|
||||
t.assert(result[0] !== 'teststring')
|
||||
t.assert(result[1] !== 42)
|
||||
const osubtype = /** @type {Y.XmlElement} */ (result[2])
|
||||
const osubtype = /** @type {Y.Type} */ (result[2])
|
||||
const osubdoc = result[3]
|
||||
// test subtype
|
||||
t.assert(osubtype.nodeName !== subtype.nodeName)
|
||||
t.assert(object.length(osubtype.getAttributes()) === 1)
|
||||
t.assert(osubtype.getAttribute('attr') === undefined)
|
||||
t.assert(osubtype.name !== subtype.name)
|
||||
t.assert(object.length(osubtype.getAttrs()) === 1)
|
||||
t.assert(osubtype.getAttr('attr') === undefined)
|
||||
// test subdoc
|
||||
t.assert(osubdoc.guid !== subdoc.guid)
|
||||
}
|
||||
|
||||
@@ -14,10 +14,10 @@ const isDevMode = env.getVariable('node_env') === 'development'
|
||||
export const testBasicUpdate = _tc => {
|
||||
const doc1 = new Y.Doc()
|
||||
const doc2 = new Y.Doc()
|
||||
doc1.getArray('array').insert(0, ['hi'])
|
||||
doc1.get('array').insert(0, ['hi'])
|
||||
const update = Y.encodeStateAsUpdate(doc1)
|
||||
Y.applyUpdate(doc2, update)
|
||||
t.compare(doc2.getArray('array').toArray(), ['hi'])
|
||||
t.compare(doc2.get('array').toArray(), ['hi'])
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -29,8 +29,8 @@ export const testFailsObjectManipulationInDevMode = _tc => {
|
||||
const doc = new Y.Doc()
|
||||
const a = [1, 2, 3]
|
||||
const b = { o: 1 }
|
||||
doc.getArray('test').insert(0, [a])
|
||||
doc.getMap('map').set('k', b)
|
||||
doc.get('test').insert(0, [a])
|
||||
doc.get('map').setAttr('k', b)
|
||||
t.fails(() => {
|
||||
a[0] = 42
|
||||
})
|
||||
@@ -47,7 +47,7 @@ export const testFailsObjectManipulationInDevMode = _tc => {
|
||||
*/
|
||||
export const testSlice = _tc => {
|
||||
const doc1 = new Y.Doc()
|
||||
const arr = doc1.getArray('array')
|
||||
const arr = doc1.get('array')
|
||||
arr.insert(0, [1, 2, 3])
|
||||
t.compareArrays(arr.slice(0), [1, 2, 3])
|
||||
t.compareArrays(arr.slice(1), [2, 3])
|
||||
@@ -62,9 +62,9 @@ export const testSlice = _tc => {
|
||||
*/
|
||||
export const testArrayFrom = _tc => {
|
||||
const doc1 = new Y.Doc()
|
||||
const db1 = doc1.getMap('root')
|
||||
const nestedArray1 = Y.Array.from([0, 1, 2])
|
||||
db1.set('array', nestedArray1)
|
||||
const db1 = doc1.get('root')
|
||||
const nestedArray1 = Y.Type.from(delta.create().insert([0, 1, 2]))
|
||||
db1.setAttr('array', nestedArray1)
|
||||
t.compare(nestedArray1.toArray(), [0, 1, 2])
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ export const testArrayFrom = _tc => {
|
||||
*/
|
||||
export const testLengthIssue = _tc => {
|
||||
const doc1 = new Y.Doc()
|
||||
const arr = doc1.getArray('array')
|
||||
const arr = doc1.get('array')
|
||||
arr.push([0, 1, 2, 3])
|
||||
arr.delete(0)
|
||||
arr.insert(0, [0])
|
||||
@@ -104,7 +104,7 @@ export const testLengthIssue = _tc => {
|
||||
*/
|
||||
export const testLengthIssue2 = _tc => {
|
||||
const doc = new Y.Doc()
|
||||
const next = doc.getArray()
|
||||
const next = doc.get()
|
||||
doc.transact(() => {
|
||||
next.insert(0, ['group2'])
|
||||
})
|
||||
@@ -162,9 +162,9 @@ export const testDeleteInsert = tc => {
|
||||
export const testInsertThreeElementsTryRegetProperty = tc => {
|
||||
const { testConnector, users, array0, array1 } = init(tc, { users: 2 })
|
||||
array0.insert(0, [1, true, false])
|
||||
t.compare(array0.toJSON(), [1, true, false], '.toJSON() works')
|
||||
t.compare(array0.getContent(), delta.create().insert([1, true, false]), 'content works')
|
||||
testConnector.flushAllMessages()
|
||||
t.compare(array1.toJSON(), [1, true, false], '.toJSON() works after sync')
|
||||
t.compare(array1.getContent(), delta.create().insert([1, true, false]), 'comparison works after sync')
|
||||
compare(users)
|
||||
}
|
||||
|
||||
@@ -222,8 +222,8 @@ export const testDisconnectReallyPreventsSendingMessages = tc => {
|
||||
users[2].disconnect()
|
||||
array0.insert(1, ['user0'])
|
||||
array1.insert(1, ['user1'])
|
||||
t.compare(array0.toJSON(), ['x', 'user0', 'y'])
|
||||
t.compare(array1.toJSON(), ['x', 'user1', 'y'])
|
||||
t.compare(array0.toJSON().children, ['x', 'user0', 'y'])
|
||||
t.compare(array1.toJSON().children, ['x', 'user1', 'y'])
|
||||
users[1].connect()
|
||||
users[2].connect()
|
||||
compare(users)
|
||||
@@ -318,7 +318,7 @@ export const testInsertAndDeleteEventsForTypes = tc => {
|
||||
array0.observe(e => {
|
||||
event = e
|
||||
})
|
||||
array0.insert(0, [new Y.Array()])
|
||||
array0.insert(0, [new Y.Type()])
|
||||
t.assert(event !== null)
|
||||
event = null
|
||||
array0.delete(0)
|
||||
@@ -327,34 +327,6 @@ export const testInsertAndDeleteEventsForTypes = tc => {
|
||||
compare(users)
|
||||
}
|
||||
|
||||
/**
|
||||
* This issue has been reported in https://discuss.yjs.dev/t/order-in-which-events-yielded-by-observedeep-should-be-applied/261/2
|
||||
*
|
||||
* Deep observers generate multiple events. When an array added at item at, say, position 0,
|
||||
* and item 1 changed then the array-add event should fire first so that the change event
|
||||
* path is correct. A array binding might lead to an inconsistent state otherwise.
|
||||
*
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testObserveDeepEventOrder = tc => {
|
||||
const { array0, users } = init(tc, { users: 2 })
|
||||
/**
|
||||
* @type {Array<any>}
|
||||
*/
|
||||
let events = []
|
||||
array0.observeDeep(e => {
|
||||
events = e
|
||||
})
|
||||
array0.insert(0, [new Y.Map()])
|
||||
users[0].transact(() => {
|
||||
array0.get(0).set('a', 'a')
|
||||
array0.insert(0, [0])
|
||||
})
|
||||
for (let i = 1; i < events.length; i++) {
|
||||
t.assert(events[i - 1].path.length <= events[i].path.length, 'path size increases, fire top-level events first')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct index when computing event.path in observeDeep - https://github.com/yjs/yjs/issues/457
|
||||
*
|
||||
@@ -362,18 +334,18 @@ export const testObserveDeepEventOrder = tc => {
|
||||
*/
|
||||
export const testObservedeepIndexes = _tc => {
|
||||
const doc = new Y.Doc()
|
||||
const map = doc.getMap()
|
||||
const map = doc.get()
|
||||
// Create a field with the array as value
|
||||
map.set('my-array', new Y.Array())
|
||||
map.setAttr('my-array', new Y.Type())
|
||||
// Fill the array with some strings and our Map
|
||||
map.get('my-array').push(['a', 'b', 'c', new Y.Map()])
|
||||
map.getAttr('my-array').push(['a', 'b', 'c', new Y.Type()])
|
||||
/**
|
||||
* @type {Array<any>}
|
||||
*/
|
||||
let eventPath = []
|
||||
map.observeDeep((events) => { eventPath = events[0].path })
|
||||
map.observeDeep((event) => { eventPath = event.path })
|
||||
// set a value on the map inside of our array
|
||||
map.get('my-array').get(3).set('hello', 'world')
|
||||
map.getAttr('my-array').get(3).set('hello', 'world')
|
||||
console.log(eventPath)
|
||||
t.compare(eventPath, ['my-array', 3])
|
||||
}
|
||||
@@ -384,13 +356,13 @@ export const testObservedeepIndexes = _tc => {
|
||||
export const testChangeEvent = tc => {
|
||||
const { array0, users } = init(tc, { users: 2 })
|
||||
/**
|
||||
* @type {delta.Delta<any,any,any,any,any>}
|
||||
* @type {delta.Delta<any>}
|
||||
*/
|
||||
let d = delta.create()
|
||||
array0.observe(e => {
|
||||
d = e.delta
|
||||
})
|
||||
const newArr = new Y.Array()
|
||||
const newArr = new Y.Type()
|
||||
array0.insert(0, [newArr, 4, 'dtrn'])
|
||||
t.assert(d !== null && d.children.len === 1)
|
||||
t.compare(d, delta.create().insert([newArr, 4, 'dtrn']))
|
||||
@@ -415,7 +387,7 @@ export const testInsertAndDeleteEventsForTypes2 = tc => {
|
||||
array0.observe(e => {
|
||||
events.push(e)
|
||||
})
|
||||
array0.insert(0, ['hi', new Y.Map()])
|
||||
array0.insert(0, ['hi', new Y.Type()])
|
||||
t.assert(events.length === 1, 'Event is triggered exactly once for insertion of two elements')
|
||||
array0.delete(1)
|
||||
t.assert(events.length === 2, 'Event is triggered exactly once for deletion')
|
||||
@@ -430,12 +402,12 @@ export const testNewChildDoesNotEmitEventInTransaction = tc => {
|
||||
const { array0, users } = init(tc, { users: 2 })
|
||||
let fired = false
|
||||
users[0].transact(() => {
|
||||
const newMap = new Y.Map()
|
||||
const newMap = new Y.Type()
|
||||
newMap.observe(() => {
|
||||
fired = true
|
||||
})
|
||||
array0.insert(0, [newMap])
|
||||
newMap.set('tst', 42)
|
||||
newMap.setAttr('tst', 42)
|
||||
})
|
||||
t.assert(!fired, 'Event does not trigger')
|
||||
}
|
||||
@@ -494,15 +466,15 @@ export const testEventTargetIsSetCorrectlyOnRemote = tc => {
|
||||
*/
|
||||
export const testIteratingArrayContainingTypes = _tc => {
|
||||
const y = new Y.Doc()
|
||||
const arr = y.getArray('arr')
|
||||
const arr = y.get('arr')
|
||||
const numItems = 10
|
||||
for (let i = 0; i < numItems; i++) {
|
||||
const map = new Y.Map()
|
||||
map.set('value', i)
|
||||
const map = new Y.Type()
|
||||
map.setAttr('value', i)
|
||||
arr.push([map])
|
||||
}
|
||||
let cnt = 0
|
||||
for (const item of arr) {
|
||||
for (const item of arr.toArray()) {
|
||||
t.assert(item.get('value') === cnt++, 'value is correct')
|
||||
}
|
||||
y.destroy()
|
||||
@@ -514,9 +486,9 @@ export const testIteratingArrayContainingTypes = _tc => {
|
||||
export const testAttributedContent = _tc => {
|
||||
const ydoc = new Y.Doc({ gc: false })
|
||||
/**
|
||||
* @type {Y.Array<number>}
|
||||
* @type {Y.Type<{ children: number }>}
|
||||
*/
|
||||
const yarray = ydoc.getArray()
|
||||
const yarray = ydoc.get()
|
||||
yarray.insert(0, [1, 2])
|
||||
let attributionManager = Y.noAttributionsManager
|
||||
|
||||
@@ -544,7 +516,7 @@ const getUniqueNumber = () => _uniqueNumber++
|
||||
*/
|
||||
const arrayTransactions = [
|
||||
function insert (user, gen) {
|
||||
const yarray = user.getArray('array')
|
||||
const yarray = user.get('array')
|
||||
const uniqueNumber = getUniqueNumber()
|
||||
const content = []
|
||||
const len = prng.int32(gen, 1, 4)
|
||||
@@ -558,35 +530,35 @@ const arrayTransactions = [
|
||||
t.compareArrays(yarray.toArray(), oldContent) // we want to make sure that fastSearch markers insert at the correct position
|
||||
},
|
||||
function insertTypeArray (user, gen) {
|
||||
const yarray = user.getArray('array')
|
||||
const yarray = user.get('array')
|
||||
const pos = prng.int32(gen, 0, yarray.length)
|
||||
yarray.insert(pos, [new Y.Array()])
|
||||
yarray.insert(pos, [new Y.Type()])
|
||||
const array2 = yarray.get(pos)
|
||||
array2.insert(0, [1, 2, 3, 4])
|
||||
},
|
||||
function insertTypeMap (user, gen) {
|
||||
const yarray = user.getArray('array')
|
||||
const yarray = user.get('array')
|
||||
const pos = prng.int32(gen, 0, yarray.length)
|
||||
yarray.insert(pos, [new Y.Map()])
|
||||
yarray.insert(pos, [new Y.Type()])
|
||||
const map = yarray.get(pos)
|
||||
map.set('someprop', 42)
|
||||
map.set('someprop', 43)
|
||||
map.set('someprop', 44)
|
||||
},
|
||||
function insertTypeNull (user, gen) {
|
||||
const yarray = user.getArray('array')
|
||||
const yarray = user.get('array')
|
||||
const pos = prng.int32(gen, 0, yarray.length)
|
||||
yarray.insert(pos, [null])
|
||||
},
|
||||
function _delete (user, gen) {
|
||||
const yarray = user.getArray('array')
|
||||
const yarray = user.get('array')
|
||||
const length = yarray.length
|
||||
if (length > 0) {
|
||||
let somePos = prng.int32(gen, 0, length - 1)
|
||||
let delLength = prng.int32(gen, 1, math.min(2, length - somePos))
|
||||
if (prng.bool(gen)) {
|
||||
const type = yarray.get(somePos)
|
||||
if (type instanceof Y.Array && type.length > 0) {
|
||||
if (type instanceof Y.Type && type.length > 0) {
|
||||
somePos = prng.int32(gen, 0, type.length - 1)
|
||||
delLength = prng.int32(gen, 0, math.min(2, type.length - somePos))
|
||||
type.delete(somePos, delLength)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import * as Y from '../src/index.js'
|
||||
import { init, compare, applyRandomTests, Doc } from './testHelper.js' // eslint-disable-line
|
||||
import {
|
||||
compareIDs,
|
||||
noAttributionsManager,
|
||||
TwosetAttributionManager,
|
||||
createIdMapFromIdSet
|
||||
@@ -18,35 +17,34 @@ import * as object from 'lib0/object'
|
||||
export const testIterators = _tc => {
|
||||
const ydoc = new Y.Doc()
|
||||
/**
|
||||
* @type {Y.Map<number>}
|
||||
* @type {Y.Type<{attrs: { [k:string]: number} }>}
|
||||
*/
|
||||
const ymap = ydoc.getMap()
|
||||
const ymap = ydoc.get()
|
||||
// we are only checking if the type assumptions are correct
|
||||
/**
|
||||
* @type {Array<number>}
|
||||
*/
|
||||
const vals = Array.from(ymap.values())
|
||||
const vals = Array.from(ymap.attrValues())
|
||||
/**
|
||||
* @type {Array<[string,number]>}
|
||||
*/
|
||||
const entries = Array.from(ymap.entries())
|
||||
const entries = Array.from(ymap.attrEntries())
|
||||
/**
|
||||
* @type {Array<string>}
|
||||
*/
|
||||
const keys = Array.from(ymap.keys())
|
||||
const keys = Array.from(ymap.attrKeys())
|
||||
console.log(vals, entries, keys)
|
||||
}
|
||||
|
||||
export const testNestedMapEvent = () => {
|
||||
const ydoc = new Y.Doc()
|
||||
const ymap = ydoc.getMap()
|
||||
const ymapNested = ymap.set('nested', new Y.Map())
|
||||
const ymap = ydoc.get()
|
||||
const ymapNested = ymap.setAttr('nested', new Y.Type())
|
||||
let called = 0
|
||||
ymap.observeDeep((events, tr) => {
|
||||
const event = events.find(event => event.target === ymap) || new Y.YEvent(ymap, tr, new Set())
|
||||
ymap.observeDeep(event => {
|
||||
const d = event.deltaDeep
|
||||
called++
|
||||
t.compare(d, delta.create().update('nested', delta.create().set('k', 'v')))
|
||||
t.compare(d, delta.create().modifyAttr('nested', delta.create().setAttr('k', 'v')))
|
||||
})
|
||||
ymapNested.set('k', 'v')
|
||||
t.assert(called === 1)
|
||||
@@ -54,17 +52,16 @@ export const testNestedMapEvent = () => {
|
||||
|
||||
export const testNestedMapEvent2 = () => {
|
||||
const ydoc = new Y.Doc()
|
||||
const yarr = ydoc.getArray()
|
||||
const ymapNested = new Y.Map()
|
||||
const yarr = ydoc.get()
|
||||
const ymapNested = new Y.Type()
|
||||
yarr.insert(0, [ymapNested])
|
||||
let called = 0
|
||||
yarr.observeDeep((events, tr) => {
|
||||
const event = events.find(event => event.target === yarr) || new Y.YEvent(yarr, tr, new Set())
|
||||
yarr.observeDeep(event => {
|
||||
const d = event.deltaDeep
|
||||
called++
|
||||
t.compare(d, delta.create().modify(delta.create().set('k', 'v')))
|
||||
t.compare(d, delta.create().modify(delta.create().setAttr('k', 'v')))
|
||||
})
|
||||
ymapNested.set('k', 'v')
|
||||
ymapNested.setAttr('k', 'v')
|
||||
t.assert(called === 1)
|
||||
}
|
||||
|
||||
@@ -75,7 +72,7 @@ export const testNestedMapEvent2 = () => {
|
||||
*/
|
||||
export const testMapEventError = _tc => {
|
||||
const doc = new Y.Doc()
|
||||
const ymap = doc.getMap()
|
||||
const ymap = doc.get()
|
||||
/**
|
||||
* @type {any}
|
||||
*/
|
||||
@@ -96,26 +93,20 @@ export const testMapEventError = _tc => {
|
||||
*/
|
||||
export const testMapHavingIterableAsConstructorParamTests = tc => {
|
||||
const { map0 } = init(tc, { users: 1 })
|
||||
|
||||
const m1 = new Y.Map(Object.entries({ number: 1, string: 'hello' }))
|
||||
map0.set('m1', m1)
|
||||
t.assert(m1.get('number') === 1)
|
||||
t.assert(m1.get('string') === 'hello')
|
||||
|
||||
const m2 = new Y.Map([
|
||||
['object', { x: 1 }],
|
||||
['boolean', true]
|
||||
])
|
||||
map0.set('m2', m2)
|
||||
t.assert(m2.get('object').x === 1)
|
||||
t.assert(m2.get('boolean') === true)
|
||||
|
||||
const m3 = new Y.Map([...m1, ...m2])
|
||||
map0.set('m3', m3)
|
||||
t.assert(m3.get('number') === 1)
|
||||
t.assert(m3.get('string') === 'hello')
|
||||
t.assert(m3.get('object').x === 1)
|
||||
t.assert(m3.get('boolean') === true)
|
||||
const m1 = Y.Type.from(delta.create().setAttr('number', 1).setAttr('string', 'hello'))
|
||||
map0.setAttr('m1', m1)
|
||||
t.assert(m1.getAttr('number') === 1)
|
||||
t.assert(m1.getAttr('string') === 'hello')
|
||||
const m2 = Y.Type.from(delta.create(delta.$deltaAny).setAttrs({ object: { x: 1 }, boolean: true }).done())
|
||||
map0.setAttr('m2', m2)
|
||||
t.assert(m2.getAttr('object')?.x === 1)
|
||||
t.assert(m2.getAttr('boolean') === true)
|
||||
const m3 = new Y.Type().applyDelta(m1.getContent()).applyDelta(m2.getContent())
|
||||
map0.setAttr('m3', m3)
|
||||
t.assert(m3.getAttr('number') === 1)
|
||||
t.assert(m3.getAttr('string') === 'hello')
|
||||
t.assert(m3.getAttr('object')?.x === 1)
|
||||
t.assert(m3.getAttr('boolean') === true)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -125,48 +116,48 @@ export const testBasicMapTests = tc => {
|
||||
const { testConnector, users, map0, map1, map2 } = init(tc, { users: 3 })
|
||||
users[2].disconnect()
|
||||
|
||||
map0.set('null', null)
|
||||
map0.set('number', 1)
|
||||
map0.set('string', 'hello Y')
|
||||
map0.set('object', { key: { key2: 'value' } })
|
||||
map0.set('y-map', new Y.Map())
|
||||
map0.set('boolean1', true)
|
||||
map0.set('boolean0', false)
|
||||
const map = map0.get('y-map')
|
||||
map.set('y-array', new Y.Array())
|
||||
map0.setAttr('null', null)
|
||||
map0.setAttr('number', 1)
|
||||
map0.setAttr('string', 'hello Y')
|
||||
map0.setAttr('object', { key: { key2: 'value' } })
|
||||
map0.setAttr('y-map', new Y.Type())
|
||||
map0.setAttr('boolean1', true)
|
||||
map0.setAttr('boolean0', false)
|
||||
const map = map0.getAttr('y-map')
|
||||
map.set('y-array', new Y.Type())
|
||||
const array = map.get('y-array')
|
||||
array.insert(0, [0])
|
||||
array.insert(0, [-1])
|
||||
|
||||
t.assert(map0.get('null') === null, 'client 0 computed the change (null)')
|
||||
t.assert(map0.get('number') === 1, 'client 0 computed the change (number)')
|
||||
t.assert(map0.get('string') === 'hello Y', 'client 0 computed the change (string)')
|
||||
t.assert(map0.get('boolean0') === false, 'client 0 computed the change (boolean)')
|
||||
t.assert(map0.get('boolean1') === true, 'client 0 computed the change (boolean)')
|
||||
t.compare(map0.get('object'), { key: { key2: 'value' } }, 'client 0 computed the change (object)')
|
||||
t.assert(map0.get('y-map').get('y-array').get(0) === -1, 'client 0 computed the change (type)')
|
||||
t.assert(map0.size === 7, 'client 0 map has correct size')
|
||||
t.assert(map0.getAttr('null') === null, 'client 0 computed the change (null)')
|
||||
t.assert(map0.getAttr('number') === 1, 'client 0 computed the change (number)')
|
||||
t.assert(map0.getAttr('string') === 'hello Y', 'client 0 computed the change (string)')
|
||||
t.assert(map0.getAttr('boolean0') === false, 'client 0 computed the change (boolean)')
|
||||
t.assert(map0.getAttr('boolean1') === true, 'client 0 computed the change (boolean)')
|
||||
t.compare(map0.getAttr('object'), { key: { key2: 'value' } }, 'client 0 computed the change (object)')
|
||||
t.assert(map0.getAttr('y-map').get('y-array').get(0) === -1, 'client 0 computed the change (type)')
|
||||
t.assert(map0.attrSize === 7, 'client 0 map has correct size')
|
||||
|
||||
users[2].connect()
|
||||
testConnector.flushAllMessages()
|
||||
|
||||
t.assert(map1.get('null') === null, 'client 1 received the update (null)')
|
||||
t.assert(map1.get('number') === 1, 'client 1 received the update (number)')
|
||||
t.assert(map1.get('string') === 'hello Y', 'client 1 received the update (string)')
|
||||
t.assert(map1.get('boolean0') === false, 'client 1 computed the change (boolean)')
|
||||
t.assert(map1.get('boolean1') === true, 'client 1 computed the change (boolean)')
|
||||
t.compare(map1.get('object'), { key: { key2: 'value' } }, 'client 1 received the update (object)')
|
||||
t.assert(map1.get('y-map').get('y-array').get(0) === -1, 'client 1 received the update (type)')
|
||||
t.assert(map1.size === 7, 'client 1 map has correct size')
|
||||
t.assert(map1.getAttr('null') === null, 'client 1 received the update (null)')
|
||||
t.assert(map1.getAttr('number') === 1, 'client 1 received the update (number)')
|
||||
t.assert(map1.getAttr('string') === 'hello Y', 'client 1 received the update (string)')
|
||||
t.assert(map1.getAttr('boolean0') === false, 'client 1 computed the change (boolean)')
|
||||
t.assert(map1.getAttr('boolean1') === true, 'client 1 computed the change (boolean)')
|
||||
t.compare(map1.getAttr('object'), { key: { key2: 'value' } }, 'client 1 received the update (object)')
|
||||
t.assert(map1.getAttr('y-map').get('y-array').get(0) === -1, 'client 1 received the update (type)')
|
||||
t.assert(map1.attrSize === 7, 'client 1 map has correct size')
|
||||
|
||||
// compare disconnected user
|
||||
t.assert(map2.get('null') === null, 'client 2 received the update (null) - was disconnected')
|
||||
t.assert(map2.get('number') === 1, 'client 2 received the update (number) - was disconnected')
|
||||
t.assert(map2.get('string') === 'hello Y', 'client 2 received the update (string) - was disconnected')
|
||||
t.assert(map2.get('boolean0') === false, 'client 2 computed the change (boolean)')
|
||||
t.assert(map2.get('boolean1') === true, 'client 2 computed the change (boolean)')
|
||||
t.compare(map2.get('object'), { key: { key2: 'value' } }, 'client 2 received the update (object) - was disconnected')
|
||||
t.assert(map2.get('y-map').get('y-array').get(0) === -1, 'client 2 received the update (type) - was disconnected')
|
||||
t.assert(map2.getAttr('null') === null, 'client 2 received the update (null) - was disconnected')
|
||||
t.assert(map2.getAttr('number') === 1, 'client 2 received the update (number) - was disconnected')
|
||||
t.assert(map2.getAttr('string') === 'hello Y', 'client 2 received the update (string) - was disconnected')
|
||||
t.assert(map2.getAttr('boolean0') === false, 'client 2 computed the change (boolean)')
|
||||
t.assert(map2.getAttr('boolean1') === true, 'client 2 computed the change (boolean)')
|
||||
t.compare(map2.getAttr('object'), { key: { key2: 'value' } }, 'client 2 received the update (object) - was disconnected')
|
||||
t.assert(map2.getAttr('y-map').get('y-array').get(0) === -1, 'client 2 received the update (type) - was disconnected')
|
||||
compare(users)
|
||||
}
|
||||
|
||||
@@ -175,18 +166,18 @@ export const testBasicMapTests = tc => {
|
||||
*/
|
||||
export const testGetAndSetOfMapProperty = tc => {
|
||||
const { testConnector, users, map0 } = init(tc, { users: 2 })
|
||||
map0.set('stuff', 'stuffy')
|
||||
map0.set('undefined', undefined)
|
||||
map0.set('null', null)
|
||||
t.compare(map0.get('stuff'), 'stuffy')
|
||||
map0.setAttr('stuff', 'stuffy')
|
||||
map0.setAttr('undefined', undefined)
|
||||
map0.setAttr('null', null)
|
||||
t.compare(map0.getAttr('stuff'), 'stuffy')
|
||||
|
||||
testConnector.flushAllMessages()
|
||||
|
||||
for (const user of users) {
|
||||
const u = user.getMap('map')
|
||||
t.compare(u.get('stuff'), 'stuffy')
|
||||
t.assert(u.get('undefined') === undefined, 'undefined')
|
||||
t.compare(u.get('null'), null, 'null')
|
||||
const u = user.get('map')
|
||||
t.compare(u.getAttr('stuff'), 'stuffy')
|
||||
t.assert(u.getAttr('undefined') === undefined, 'undefined')
|
||||
t.compare(u.getAttr('null'), null, 'null')
|
||||
}
|
||||
compare(users)
|
||||
}
|
||||
@@ -196,8 +187,8 @@ export const testGetAndSetOfMapProperty = tc => {
|
||||
*/
|
||||
export const testYmapSetsYmap = tc => {
|
||||
const { users, map0 } = init(tc, { users: 2 })
|
||||
const map = map0.set('Map', new Y.Map())
|
||||
t.assert(map0.get('Map') === map)
|
||||
const map = map0.setAttr('Map', new Y.Type())
|
||||
t.assert(map0.getAttr('Map') === map)
|
||||
map.set('one', 1)
|
||||
t.compare(map.get('one'), 1)
|
||||
compare(users)
|
||||
@@ -208,8 +199,8 @@ export const testYmapSetsYmap = tc => {
|
||||
*/
|
||||
export const testYmapSetsYarray = tc => {
|
||||
const { users, map0 } = init(tc, { users: 2 })
|
||||
const array = map0.set('Array', new Y.Array())
|
||||
t.assert(array === map0.get('Array'))
|
||||
const array = map0.setAttr('Array', new Y.Type())
|
||||
t.assert(array === map0.getAttr('Array'))
|
||||
array.insert(0, [1, 2, 3])
|
||||
// @ts-ignore
|
||||
t.compare(map0.toJSON(), { Array: [1, 2, 3] })
|
||||
@@ -221,12 +212,12 @@ export const testYmapSetsYarray = tc => {
|
||||
*/
|
||||
export const testGetAndSetOfMapPropertySyncs = tc => {
|
||||
const { testConnector, users, map0 } = init(tc, { users: 2 })
|
||||
map0.set('stuff', 'stuffy')
|
||||
t.compare(map0.get('stuff'), 'stuffy')
|
||||
map0.setAttr('stuff', 'stuffy')
|
||||
t.compare(map0.getAttr('stuff'), 'stuffy')
|
||||
testConnector.flushAllMessages()
|
||||
for (const user of users) {
|
||||
const u = user.getMap('map')
|
||||
t.compare(u.get('stuff'), 'stuffy')
|
||||
const u = user.get('map')
|
||||
t.compare(u.getAttr('stuff'), 'stuffy')
|
||||
}
|
||||
compare(users)
|
||||
}
|
||||
@@ -236,12 +227,12 @@ export const testGetAndSetOfMapPropertySyncs = tc => {
|
||||
*/
|
||||
export const testGetAndSetOfMapPropertyWithConflict = tc => {
|
||||
const { testConnector, users, map0, map1 } = init(tc, { users: 3 })
|
||||
map0.set('stuff', 'c0')
|
||||
map1.set('stuff', 'c1')
|
||||
map0.setAttr('stuff', 'c0')
|
||||
map1.setAttr('stuff', 'c1')
|
||||
testConnector.flushAllMessages()
|
||||
for (const user of users) {
|
||||
const u = user.getMap('map')
|
||||
t.compare(u.get('stuff'), 'c1')
|
||||
const u = user.get('map')
|
||||
t.compare(u.getAttr('stuff'), 'c1')
|
||||
}
|
||||
compare(users)
|
||||
}
|
||||
@@ -251,13 +242,13 @@ export const testGetAndSetOfMapPropertyWithConflict = tc => {
|
||||
*/
|
||||
export const testSizeAndDeleteOfMapProperty = tc => {
|
||||
const { map0 } = init(tc, { users: 1 })
|
||||
map0.set('stuff', 'c0')
|
||||
map0.set('otherstuff', 'c1')
|
||||
t.assert(map0.size === 2, `map size is ${map0.size} expected 2`)
|
||||
map0.delete('stuff')
|
||||
t.assert(map0.size === 1, `map size after delete is ${map0.size}, expected 1`)
|
||||
map0.delete('otherstuff')
|
||||
t.assert(map0.size === 0, `map size after delete is ${map0.size}, expected 0`)
|
||||
map0.setAttr('stuff', 'c0')
|
||||
map0.setAttr('otherstuff', 'c1')
|
||||
t.assert(map0.attrSize === 2, `map size is ${map0.attrSize} expected 2`)
|
||||
map0.deleteAttr('stuff')
|
||||
t.assert(map0.attrSize === 1, `map size after delete is ${map0.attrSize}, expected 1`)
|
||||
map0.deleteAttr('otherstuff')
|
||||
t.assert(map0.attrSize === 0, `map size after delete is ${map0.attrSize}, expected 0`)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -265,13 +256,13 @@ export const testSizeAndDeleteOfMapProperty = tc => {
|
||||
*/
|
||||
export const testGetAndSetAndDeleteOfMapProperty = tc => {
|
||||
const { testConnector, users, map0, map1 } = init(tc, { users: 3 })
|
||||
map0.set('stuff', 'c0')
|
||||
map1.set('stuff', 'c1')
|
||||
map1.delete('stuff')
|
||||
map0.setAttr('stuff', 'c0')
|
||||
map1.setAttr('stuff', 'c1')
|
||||
map1.deleteAttr('stuff')
|
||||
testConnector.flushAllMessages()
|
||||
for (const user of users) {
|
||||
const u = user.getMap('map')
|
||||
t.assert(u.get('stuff') === undefined)
|
||||
const u = user.get('map')
|
||||
t.assert(u.getAttr('stuff') === undefined)
|
||||
}
|
||||
compare(users)
|
||||
}
|
||||
@@ -281,15 +272,15 @@ export const testGetAndSetAndDeleteOfMapProperty = tc => {
|
||||
*/
|
||||
export const testSetAndClearOfMapProperties = tc => {
|
||||
const { testConnector, users, map0 } = init(tc, { users: 1 })
|
||||
map0.set('stuff', 'c0')
|
||||
map0.set('otherstuff', 'c1')
|
||||
map0.clear()
|
||||
map0.setAttr('stuff', 'c0')
|
||||
map0.setAttr('otherstuff', 'c1')
|
||||
map0.clearAttrs()
|
||||
testConnector.flushAllMessages()
|
||||
for (const user of users) {
|
||||
const u = user.getMap('map')
|
||||
t.assert(u.get('stuff') === undefined)
|
||||
t.assert(u.get('otherstuff') === undefined)
|
||||
t.assert(u.size === 0, `map size after clear is ${u.size}, expected 0`)
|
||||
const u = user.get('map')
|
||||
t.assert(u.getAttr('stuff') === undefined)
|
||||
t.assert(u.getAttr('otherstuff') === undefined)
|
||||
t.assert(u.attrSize === 0, `map size after clear is ${u.attrSize}, expected 0`)
|
||||
}
|
||||
compare(users)
|
||||
}
|
||||
@@ -299,22 +290,22 @@ export const testSetAndClearOfMapProperties = tc => {
|
||||
*/
|
||||
export const testSetAndClearOfMapPropertiesWithConflicts = tc => {
|
||||
const { testConnector, users, map0, map1, map2, map3 } = init(tc, { users: 4 })
|
||||
map0.set('stuff', 'c0')
|
||||
map1.set('stuff', 'c1')
|
||||
map1.set('stuff', 'c2')
|
||||
map2.set('stuff', 'c3')
|
||||
map0.setAttr('stuff', 'c0')
|
||||
map1.setAttr('stuff', 'c1')
|
||||
map1.setAttr('stuff', 'c2')
|
||||
map2.setAttr('stuff', 'c3')
|
||||
testConnector.flushAllMessages()
|
||||
map0.set('otherstuff', 'c0')
|
||||
map1.set('otherstuff', 'c1')
|
||||
map2.set('otherstuff', 'c2')
|
||||
map3.set('otherstuff', 'c3')
|
||||
map3.clear()
|
||||
map0.setAttr('otherstuff', 'c0')
|
||||
map1.setAttr('otherstuff', 'c1')
|
||||
map2.setAttr('otherstuff', 'c2')
|
||||
map3.setAttr('otherstuff', 'c3')
|
||||
map3.clearAttrs()
|
||||
testConnector.flushAllMessages()
|
||||
for (const user of users) {
|
||||
const u = user.getMap('map')
|
||||
t.assert(u.get('stuff') === undefined)
|
||||
t.assert(u.get('otherstuff') === undefined)
|
||||
t.assert(u.size === 0, `map size after clear is ${u.size}, expected 0`)
|
||||
const u = user.get('map')
|
||||
t.assert(u.getAttr('stuff') === undefined)
|
||||
t.assert(u.getAttr('otherstuff') === undefined)
|
||||
t.assert(u.attrSize === 0, `map size after clear is ${u.attrSize}, expected 0`)
|
||||
}
|
||||
compare(users)
|
||||
}
|
||||
@@ -324,14 +315,14 @@ export const testSetAndClearOfMapPropertiesWithConflicts = tc => {
|
||||
*/
|
||||
export const testGetAndSetOfMapPropertyWithThreeConflicts = tc => {
|
||||
const { testConnector, users, map0, map1, map2 } = init(tc, { users: 3 })
|
||||
map0.set('stuff', 'c0')
|
||||
map1.set('stuff', 'c1')
|
||||
map1.set('stuff', 'c2')
|
||||
map2.set('stuff', 'c3')
|
||||
map0.setAttr('stuff', 'c0')
|
||||
map1.setAttr('stuff', 'c1')
|
||||
map1.setAttr('stuff', 'c2')
|
||||
map2.setAttr('stuff', 'c3')
|
||||
testConnector.flushAllMessages()
|
||||
for (const user of users) {
|
||||
const u = user.getMap('map')
|
||||
t.compare(u.get('stuff'), 'c3')
|
||||
const u = user.get('map')
|
||||
t.compare(u.getAttr('stuff'), 'c3')
|
||||
}
|
||||
compare(users)
|
||||
}
|
||||
@@ -341,114 +332,24 @@ export const testGetAndSetOfMapPropertyWithThreeConflicts = tc => {
|
||||
*/
|
||||
export const testGetAndSetAndDeleteOfMapPropertyWithThreeConflicts = tc => {
|
||||
const { testConnector, users, map0, map1, map2, map3 } = init(tc, { users: 4 })
|
||||
map0.set('stuff', 'c0')
|
||||
map1.set('stuff', 'c1')
|
||||
map1.set('stuff', 'c2')
|
||||
map2.set('stuff', 'c3')
|
||||
map0.setAttr('stuff', 'c0')
|
||||
map1.setAttr('stuff', 'c1')
|
||||
map1.setAttr('stuff', 'c2')
|
||||
map2.setAttr('stuff', 'c3')
|
||||
testConnector.flushAllMessages()
|
||||
map0.set('stuff', 'deleteme')
|
||||
map1.set('stuff', 'c1')
|
||||
map2.set('stuff', 'c2')
|
||||
map3.set('stuff', 'c3')
|
||||
map3.delete('stuff')
|
||||
map0.setAttr('stuff', 'deleteme')
|
||||
map1.setAttr('stuff', 'c1')
|
||||
map2.setAttr('stuff', 'c2')
|
||||
map3.setAttr('stuff', 'c3')
|
||||
map3.deleteAttr('stuff')
|
||||
testConnector.flushAllMessages()
|
||||
for (const user of users) {
|
||||
const u = user.getMap('map')
|
||||
t.assert(u.get('stuff') === undefined)
|
||||
const u = user.get('map')
|
||||
t.assert(u.getAttr('stuff') === undefined)
|
||||
}
|
||||
compare(users)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testObserveDeepProperties = tc => {
|
||||
const { testConnector, users, map1, map2, map3 } = init(tc, { users: 4 })
|
||||
const _map1 = map1.set('map', new Y.Map())
|
||||
let calls = 0
|
||||
let dmapid
|
||||
map1.observeDeep(events => {
|
||||
events.forEach(event => {
|
||||
calls++
|
||||
// @ts-ignore
|
||||
t.assert(event.keysChanged.has('deepmap'))
|
||||
t.assert(event.path.length === 1)
|
||||
t.assert(event.path[0] === 'map')
|
||||
// @ts-ignore
|
||||
dmapid = event.target.get('deepmap')._item.id
|
||||
})
|
||||
})
|
||||
testConnector.flushAllMessages()
|
||||
const _map3 = map3.get('map')
|
||||
_map3.set('deepmap', new Y.Map())
|
||||
testConnector.flushAllMessages()
|
||||
const _map2 = map2.get('map')
|
||||
_map2.set('deepmap', new Y.Map())
|
||||
testConnector.flushAllMessages()
|
||||
const dmap1 = _map1.get('deepmap')
|
||||
const dmap2 = _map2.get('deepmap')
|
||||
const dmap3 = _map3.get('deepmap')
|
||||
t.assert(calls > 0)
|
||||
t.assert(compareIDs(dmap1._item.id, dmap2._item.id))
|
||||
t.assert(compareIDs(dmap1._item.id, dmap3._item.id))
|
||||
// @ts-ignore we want the possibility of dmapid being undefined
|
||||
t.assert(compareIDs(dmap1._item.id, dmapid))
|
||||
compare(users)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testObserversUsingObservedeep = tc => {
|
||||
const { users, map0 } = init(tc, { users: 2 })
|
||||
/**
|
||||
* @type {Array<Array<string|number>>}
|
||||
*/
|
||||
const paths = []
|
||||
let calls = 0
|
||||
map0.observeDeep(events => {
|
||||
events.forEach(event => {
|
||||
paths.push(event.path)
|
||||
})
|
||||
calls++
|
||||
})
|
||||
map0.set('map', new Y.Map())
|
||||
map0.get('map').set('array', new Y.Array())
|
||||
map0.get('map').get('array').insert(0, ['content'])
|
||||
t.assert(calls === 3)
|
||||
t.compare(paths, [[], ['map'], ['map', 'array']])
|
||||
compare(users)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testPathsOfSiblingEvents = tc => {
|
||||
const { users, map0 } = init(tc, { users: 2 })
|
||||
/**
|
||||
* @type {Array<Array<string|number>>}
|
||||
*/
|
||||
const paths = []
|
||||
let calls = 0
|
||||
const doc = users[0]
|
||||
map0.set('map', new Y.Map())
|
||||
map0.get('map').set('text1', new Y.Text('initial'))
|
||||
map0.observeDeep(events => {
|
||||
events.forEach(event => {
|
||||
paths.push(event.path)
|
||||
})
|
||||
calls++
|
||||
})
|
||||
doc.transact(() => {
|
||||
map0.get('map').get('text1').insert(0, 'post-')
|
||||
map0.get('map').set('text2', new Y.Text('new'))
|
||||
})
|
||||
t.assert(calls === 1)
|
||||
t.compare(paths, [['map'], ['map', 'text1']])
|
||||
compare(users)
|
||||
}
|
||||
|
||||
// TODO: Test events in Y.Map
|
||||
/**
|
||||
* @param {Object<string,any>} is
|
||||
* @param {Object<string,any>} should
|
||||
@@ -471,21 +372,21 @@ export const testThrowsAddAndUpdateAndDeleteEvents = tc => {
|
||||
map0.observe(e => {
|
||||
event = e // just put it on event, should be thrown synchronously anyway
|
||||
})
|
||||
map0.set('stuff', 4)
|
||||
map0.setAttr('stuff', 4)
|
||||
compareEvent(event, {
|
||||
target: map0,
|
||||
keysChanged: new Set(['stuff'])
|
||||
})
|
||||
// update, oldValue is in contents
|
||||
map0.set('stuff', new Y.Array())
|
||||
map0.setAttr('stuff', new Y.Type())
|
||||
compareEvent(event, {
|
||||
target: map0,
|
||||
keysChanged: new Set(['stuff'])
|
||||
})
|
||||
// update, oldValue is in opContents
|
||||
map0.set('stuff', 5)
|
||||
map0.setAttr('stuff', 5)
|
||||
// delete
|
||||
map0.delete('stuff')
|
||||
map0.deleteAttr('stuff')
|
||||
compareEvent(event, {
|
||||
keysChanged: new Set(['stuff']),
|
||||
target: map0
|
||||
@@ -506,10 +407,10 @@ export const testThrowsDeleteEventsOnClear = tc => {
|
||||
event = e // just put it on event, should be thrown synchronously anyway
|
||||
})
|
||||
// set values
|
||||
map0.set('stuff', 4)
|
||||
map0.set('otherstuff', new Y.Array())
|
||||
map0.setAttr('stuff', 4)
|
||||
map0.setAttr('otherstuff', new Y.Type())
|
||||
// clear
|
||||
map0.clear()
|
||||
map0.clearAttrs()
|
||||
compareEvent(event, {
|
||||
keysChanged: new Set(['stuff', 'otherstuff']),
|
||||
target: map0
|
||||
@@ -523,38 +424,38 @@ export const testThrowsDeleteEventsOnClear = tc => {
|
||||
export const testChangeEvent = tc => {
|
||||
const { map0, users } = init(tc, { users: 2 })
|
||||
/**
|
||||
* @type {delta.Delta<any,any,any,any>?}
|
||||
* @type {delta.Delta<any>?}
|
||||
*/
|
||||
let changes = delta.create()
|
||||
map0.observe(e => {
|
||||
changes = e.delta
|
||||
})
|
||||
map0.set('a', 1)
|
||||
map0.setAttr('a', 1)
|
||||
let keyChange = changes.attrs.a
|
||||
t.assert(delta.$insertOpWith(s.$number).check(keyChange) && keyChange.prevValue === undefined)
|
||||
map0.set('a', 2)
|
||||
map0.setAttr('a', 2)
|
||||
keyChange = changes.attrs.a
|
||||
t.assert(delta.$insertOpWith(s.$number).check(keyChange) && keyChange.prevValue === 1)
|
||||
users[0].transact(() => {
|
||||
map0.set('a', 3)
|
||||
map0.set('a', 4)
|
||||
map0.setAttr('a', 3)
|
||||
map0.setAttr('a', 4)
|
||||
})
|
||||
keyChange = changes.attrs.a
|
||||
t.assert(delta.$insertOpWith(s.$number).check(keyChange) && keyChange.prevValue === 2)
|
||||
users[0].transact(() => {
|
||||
map0.set('b', 1)
|
||||
map0.set('b', 2)
|
||||
map0.setAttr('b', 1)
|
||||
map0.setAttr('b', 2)
|
||||
})
|
||||
keyChange = changes.attrs.b
|
||||
t.assert(delta.$insertOpWith(s.$number).check(keyChange) && keyChange.prevValue === undefined)
|
||||
users[0].transact(() => {
|
||||
map0.set('c', 1)
|
||||
map0.delete('c')
|
||||
map0.setAttr('c', 1)
|
||||
map0.deleteAttr('c')
|
||||
})
|
||||
t.assert(changes !== null && object.isEmpty(changes.attrs))
|
||||
users[0].transact(() => {
|
||||
map0.set('d', 1)
|
||||
map0.set('d', 2)
|
||||
map0.setAttr('d', 1)
|
||||
map0.setAttr('d', 2)
|
||||
})
|
||||
keyChange = changes.attrs.d
|
||||
t.assert(delta.$insertOpWith(s.$number).check(keyChange) && keyChange.prevValue === undefined)
|
||||
@@ -566,7 +467,7 @@ export const testChangeEvent = tc => {
|
||||
*/
|
||||
export const testYmapEventExceptionsShouldCompleteTransaction = _tc => {
|
||||
const doc = new Y.Doc()
|
||||
const map = doc.getMap('map')
|
||||
const map = doc.get('map')
|
||||
|
||||
let updateCalled = false
|
||||
let throwingObserverCalled = false
|
||||
@@ -589,7 +490,7 @@ export const testYmapEventExceptionsShouldCompleteTransaction = _tc => {
|
||||
map.observeDeep(throwingDeepObserver)
|
||||
|
||||
t.fails(() => {
|
||||
map.set('y', '2')
|
||||
map.setAttr('y', '2')
|
||||
})
|
||||
|
||||
t.assert(updateCalled)
|
||||
@@ -601,14 +502,14 @@ export const testYmapEventExceptionsShouldCompleteTransaction = _tc => {
|
||||
throwingObserverCalled = false
|
||||
throwingDeepObserverCalled = false
|
||||
t.fails(() => {
|
||||
map.set('z', '3')
|
||||
map.setAttr('z', '3')
|
||||
})
|
||||
|
||||
t.assert(updateCalled)
|
||||
t.assert(throwingObserverCalled)
|
||||
t.assert(throwingDeepObserverCalled)
|
||||
|
||||
t.assert(map.get('z') === '3')
|
||||
t.assert(map.getAttr('z') === '3')
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -623,7 +524,7 @@ export const testYmapEventHasCorrectValueWhenSettingAPrimitive = tc => {
|
||||
map0.observe(e => {
|
||||
event = e
|
||||
})
|
||||
map0.set('stuff', 2)
|
||||
map0.setAttr('stuff', 2)
|
||||
t.compare(event.value, event.target.get(event.name))
|
||||
compare(users)
|
||||
}
|
||||
@@ -640,7 +541,7 @@ export const testYmapEventHasCorrectValueWhenSettingAPrimitiveFromOtherUser = tc
|
||||
map0.observe(e => {
|
||||
event = e
|
||||
})
|
||||
map1.set('stuff', 2)
|
||||
map1.setAttr('stuff', 2)
|
||||
testConnector.flushAllMessages()
|
||||
t.compare(event.value, event.target.get(event.name))
|
||||
compare(users)
|
||||
@@ -651,7 +552,7 @@ export const testYmapEventHasCorrectValueWhenSettingAPrimitiveFromOtherUser = tc
|
||||
*/
|
||||
export const testAttributedContent = _tc => {
|
||||
const ydoc = new Y.Doc({ gc: false })
|
||||
const ymap = ydoc.getMap()
|
||||
const ymap = ydoc.get()
|
||||
let attributionManager = noAttributionsManager
|
||||
|
||||
ydoc.on('afterTransaction', tr => {
|
||||
@@ -659,21 +560,21 @@ export const testAttributedContent = _tc => {
|
||||
attributionManager = new TwosetAttributionManager(createIdMapFromIdSet(tr.insertSet, []), createIdMapFromIdSet(tr.deleteSet, []))
|
||||
})
|
||||
t.group('initial value', () => {
|
||||
ymap.set('test', 42)
|
||||
ymap.setAttr('test', 42)
|
||||
const expectedContent = { test: delta.$deltaMapChangeJson.expect({ type: 'insert', value: 42, attribution: { insert: [] } }) }
|
||||
const attributedContent = ymap.getContent(attributionManager)
|
||||
console.log(attributedContent.toJSON())
|
||||
t.compare(expectedContent, attributedContent.toJSON().attrs)
|
||||
})
|
||||
t.group('overwrite value', () => {
|
||||
ymap.set('test', 'fourtytwo')
|
||||
ymap.setAttr('test', 'fourtytwo')
|
||||
const expectedContent = { test: delta.$deltaMapChangeJson.expect({ type: 'insert', value: 'fourtytwo', attribution: { insert: [] } }) }
|
||||
const attributedContent = ymap.getContent(attributionManager)
|
||||
console.log(attributedContent)
|
||||
t.compare(expectedContent, attributedContent.toJSON().attrs)
|
||||
})
|
||||
t.group('delete value', () => {
|
||||
ymap.delete('test')
|
||||
ymap.deleteAttr('test')
|
||||
const expectedContent = { test: delta.$deltaMapChangeJson.expect({ type: 'delete', prevValue: 'fourtytwo', attribution: { delete: [] } }) }
|
||||
const attributedContent = ymap.getContent(attributionManager)
|
||||
console.log(attributedContent.toJSON())
|
||||
@@ -688,21 +589,21 @@ const mapTransactions = [
|
||||
function set (user, gen) {
|
||||
const key = prng.oneOf(gen, ['one', 'two'])
|
||||
const value = prng.utf16String(gen)
|
||||
user.getMap('map').set(key, value)
|
||||
user.get('map').setAttr(key, value)
|
||||
},
|
||||
function setType (user, gen) {
|
||||
const key = prng.oneOf(gen, ['one', 'two'])
|
||||
const type = prng.oneOf(gen, [new Y.Array(), new Y.Map()])
|
||||
user.getMap('map').set(key, type)
|
||||
if (type instanceof Y.Array) {
|
||||
const type = new Y.Type()
|
||||
user.get('map').setAttr(key, type)
|
||||
if (prng.bool(gen)) {
|
||||
type.insert(0, [1, 2, 3, 4])
|
||||
} else {
|
||||
type.set('deepkey', 'deepvalue')
|
||||
type.setAttr('deepkey', 'deepvalue')
|
||||
}
|
||||
},
|
||||
function _delete (user, gen) {
|
||||
const key = prng.oneOf(gen, ['one', 'two'])
|
||||
user.getMap('map').delete(key)
|
||||
user.get('map').deleteAttr(key)
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -148,7 +148,7 @@ export const testDeltaBug = _tc => {
|
||||
'block-id': 'block-15630542-ef45-412d-9415-88f0052238ce'
|
||||
})
|
||||
const ydoc1 = new Y.Doc()
|
||||
const ytext = ydoc1.getText()
|
||||
const ytext = ydoc1.get()
|
||||
ytext.applyDelta(initialDelta)
|
||||
const addingDash = delta.create().retain(12).insert('-')
|
||||
ytext.applyDelta(addingDash)
|
||||
@@ -168,7 +168,7 @@ export const testDeltaBug = _tc => {
|
||||
})
|
||||
ytext.applyDelta(addingList)
|
||||
const result = ytext.getContent()
|
||||
const expectedResult = delta.text()
|
||||
const expectedResult = delta.create()
|
||||
.insert('\n', { 'block-id': 'block-28eea923-9cbb-4b6f-a950-cf7fd82bc087' })
|
||||
.insert('\n\n\n', { 'table-col': { width: '150' } })
|
||||
.insert('\n', {
|
||||
@@ -306,7 +306,7 @@ export const testDeltaBug = _tc => {
|
||||
* @param {t.TestCase} _tc
|
||||
*/
|
||||
export const testDeltaBug2 = _tc => {
|
||||
const initialContent = delta.create()
|
||||
const initialContent = delta.create(delta.$deltaAny)
|
||||
.insert("Thomas' section")
|
||||
.insert('\n', { 'block-id': 'block-61ae80ac-a469-4eae-bac9-3b6a2c380118' })
|
||||
.insert('\n', { 'block-id': 'block-d265d93f-1cc7-40ee-bb58-8270fca2619f' })
|
||||
@@ -1206,7 +1206,7 @@ export const testDeltaBug2 = _tc => {
|
||||
})
|
||||
.insert('\n', { 'block-id': 'block-21099df0-afb2-4cd3-834d-bb37800eb06a' })
|
||||
const ydoc = new Y.Doc()
|
||||
const ytext = ydoc.getText('id')
|
||||
const ytext = ydoc.get('id')
|
||||
ytext.applyDelta(initialContent)
|
||||
const changeEvent = delta.create().retain(90).delete(4).retain(1, {
|
||||
layout: null,
|
||||
@@ -1350,7 +1350,7 @@ export const testFalsyFormats = tc => {
|
||||
*/
|
||||
export const testMultilineFormat = _tc => {
|
||||
const ydoc = new Y.Doc()
|
||||
const testText = ydoc.getText('test')
|
||||
const testText = ydoc.get('test')
|
||||
testText.insert(0, 'Test\nMulti-line\nFormatting')
|
||||
const tt = delta.create()
|
||||
.retain(4, { bold: true })
|
||||
@@ -1376,7 +1376,7 @@ export const testMultilineFormat = _tc => {
|
||||
*/
|
||||
export const testNotMergeEmptyLinesFormat = _tc => {
|
||||
const ydoc = new Y.Doc()
|
||||
const testText = ydoc.getText('test')
|
||||
const testText = ydoc.get('test')
|
||||
testText.applyDelta(delta.create()
|
||||
.insert('Text')
|
||||
.insert('\n', { title: true })
|
||||
@@ -1399,7 +1399,7 @@ export const testNotMergeEmptyLinesFormat = _tc => {
|
||||
*/
|
||||
export const testPreserveAttributesThroughDelete = _tc => {
|
||||
const ydoc = new Y.Doc()
|
||||
const testText = ydoc.getText('test')
|
||||
const testText = ydoc.get('test')
|
||||
testText.applyDelta(delta.create()
|
||||
.insert('Text')
|
||||
.insert('\n', { title: true })
|
||||
@@ -1437,7 +1437,7 @@ export const testGetDeltaWithEmbeds = tc => {
|
||||
export const testTypesAsEmbed = tc => {
|
||||
const { text0, text1, testConnector } = init(tc, { users: 2 })
|
||||
text0.applyDelta(delta.create()
|
||||
.insert([new Y.Map([['key', 'val']])])
|
||||
.insert([Y.Type.from(delta.create().setAttr('key', 'val'))])
|
||||
)
|
||||
t.compare(/** @type {any} */ (text0).getContentDeep().toJSON().children, [{ type: 'insert', insert: [{ type: 'delta', attrs: { key: { type: 'insert', value: 'val' } } }] }])
|
||||
let firedEvent = false
|
||||
@@ -1512,10 +1512,10 @@ export const testSnapshotDeleteAfter = tc => {
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testToJson = tc => {
|
||||
export const testDeltaCompare = tc => {
|
||||
const { text0 } = init(tc, { users: 1 })
|
||||
text0.insert(0, 'abc', { bold: true })
|
||||
t.assert(text0.toJSON() === 'abc', 'toJSON returns the unformatted text')
|
||||
t.compare(text0.getContent(), delta.create().insert('abc', { bold: true }).done())
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1524,7 +1524,7 @@ export const testToJson = tc => {
|
||||
export const testToDeltaEmbedAttributes = tc => {
|
||||
const { text0 } = init(tc, { users: 1 })
|
||||
text0.insert(0, 'ab', { bold: true })
|
||||
text0.insertEmbed(1, { image: 'imageSrc.png' }, { width: 100 })
|
||||
text0.insert(1, [{ image: 'imageSrc.png' }], { width: 100 })
|
||||
const delta0 = text0.getContent()
|
||||
t.compare(
|
||||
delta0,
|
||||
@@ -1542,7 +1542,7 @@ export const testToDeltaEmbedAttributes = tc => {
|
||||
export const testToDeltaEmbedNoAttributes = tc => {
|
||||
const { text0 } = init(tc, { users: 1 })
|
||||
text0.insert(0, 'ab', { bold: true })
|
||||
text0.insertEmbed(1, { image: 'imageSrc.png' })
|
||||
text0.insert(1, [{ image: 'imageSrc.png' }])
|
||||
const delta0 = text0.getContent()
|
||||
t.compare(
|
||||
delta0,
|
||||
@@ -1593,7 +1593,7 @@ export const testFormattingDeltaUnnecessaryAttributeChange = tc => {
|
||||
})
|
||||
testConnector.flushAllMessages()
|
||||
/**
|
||||
* @type {Array<delta.TextDelta<any>>}
|
||||
* @type {Array<delta.Delta<any>>}
|
||||
*/
|
||||
const deltas = []
|
||||
text0.observe(event => {
|
||||
@@ -1706,7 +1706,7 @@ export const testLargeFragmentedDocument = _tc => {
|
||||
let update = /** @type {any} */ (null)
|
||||
;(() => {
|
||||
const doc1 = new Y.Doc()
|
||||
const text0 = doc1.getText('txt')
|
||||
const text0 = doc1.get('txt')
|
||||
tryGc()
|
||||
t.measureTime(`time to insert ${itemsToInsert} items`, () => {
|
||||
doc1.transact(() => {
|
||||
@@ -1741,7 +1741,7 @@ export const testIncrementalUpdatesPerformanceOnLargeFragmentedDocument = _tc =>
|
||||
doc1.on('update', update => {
|
||||
updates.push(update)
|
||||
})
|
||||
const text0 = doc1.getText('txt')
|
||||
const text0 = doc1.get('txt')
|
||||
tryGc()
|
||||
t.measureTime(`time to insert ${itemsToInsert} items`, () => {
|
||||
doc1.transact(() => {
|
||||
@@ -1846,13 +1846,13 @@ export const testSearchMarkerBug1 = tc => {
|
||||
export const testFormattingBug = async _tc => {
|
||||
const ydoc1 = new Y.Doc()
|
||||
const ydoc2 = new Y.Doc()
|
||||
const text1 = ydoc1.getText()
|
||||
const text1 = ydoc1.get()
|
||||
text1.insert(0, '\n\n\n')
|
||||
text1.format(0, 3, { url: 'http://example.com' })
|
||||
ydoc1.getText().format(1, 1, { url: 'http://docs.yjs.dev' })
|
||||
ydoc2.getText().format(1, 1, { url: 'http://docs.yjs.dev' })
|
||||
ydoc1.get().format(1, 1, { url: 'http://docs.yjs.dev' })
|
||||
ydoc2.get().format(1, 1, { url: 'http://docs.yjs.dev' })
|
||||
Y.applyUpdate(ydoc2, Y.encodeStateAsUpdate(ydoc1))
|
||||
const text2 = ydoc2.getText()
|
||||
const text2 = ydoc2.get()
|
||||
const expectedResult = delta.create()
|
||||
.insert('\n', { url: 'http://example.com' })
|
||||
.insert('\n', { url: 'http://docs.yjs.dev' })
|
||||
@@ -1870,11 +1870,11 @@ export const testFormattingBug = async _tc => {
|
||||
*/
|
||||
export const testDeleteFormatting = _tc => {
|
||||
const doc = new Y.Doc()
|
||||
const text = doc.getText()
|
||||
const text = doc.get()
|
||||
text.insert(0, 'Attack ships on fire off the shoulder of Orion.')
|
||||
|
||||
const doc2 = new Y.Doc()
|
||||
const text2 = doc2.getText()
|
||||
const text2 = doc2.get()
|
||||
Y.applyUpdate(doc2, Y.encodeStateAsUpdate(doc))
|
||||
|
||||
text.format(13, 7, { bold: true })
|
||||
@@ -1897,7 +1897,7 @@ export const testDeleteFormatting = _tc => {
|
||||
*/
|
||||
export const testAttributedContent = _tc => {
|
||||
const ydoc = new Y.Doc({ gc: false })
|
||||
const ytext = ydoc.getText()
|
||||
const ytext = ydoc.get()
|
||||
ytext.insert(0, 'Hello World!')
|
||||
let attributionManager = noAttributionsManager
|
||||
|
||||
@@ -1927,11 +1927,11 @@ export const testAttributedContent = _tc => {
|
||||
export const testAttributedDiffing = _tc => {
|
||||
const ydocVersion0 = new Y.Doc({ gc: false })
|
||||
ydocVersion0.clientID = 0
|
||||
ydocVersion0.getText().insert(0, 'Hello World!')
|
||||
ydocVersion0.get().insert(0, 'Hello World!')
|
||||
const ydoc = new Y.Doc({ gc: false })
|
||||
ydoc.clientID = 1
|
||||
Y.applyUpdate(ydoc, Y.encodeStateAsUpdate(ydocVersion0))
|
||||
const ytext = ydoc.getText()
|
||||
const ytext = ydoc.get()
|
||||
ytext.applyDelta(delta.create().retain(4, { italic: true }).retain(2).delete(5).insert('attributions'))
|
||||
// this represents to all insertions of ydoc
|
||||
const insertionSet = Y.createInsertionSetFromStructStore(ydoc.store, false)
|
||||
@@ -1968,7 +1968,7 @@ const textChanges = [
|
||||
* @param {prng.PRNG} gen
|
||||
*/
|
||||
(y, gen) => { // insert text
|
||||
const ytext = y.getText('text')
|
||||
const ytext = y.get('text')
|
||||
const insertPos = prng.int32(gen, 0, ytext.length)
|
||||
const text = charCounter++ + prng.word(gen)
|
||||
const prevText = ytext.toString()
|
||||
@@ -1980,7 +1980,7 @@ const textChanges = [
|
||||
* @param {prng.PRNG} gen
|
||||
*/
|
||||
(y, gen) => { // delete text
|
||||
const ytext = y.getText('text')
|
||||
const ytext = y.get('text')
|
||||
const contentLen = ytext.toString().length
|
||||
const insertPos = prng.int32(gen, 0, contentLen)
|
||||
const overwrite = math.min(prng.int32(gen, 0, contentLen - insertPos), 2)
|
||||
@@ -2075,7 +2075,7 @@ const qChanges = [
|
||||
* @param {prng.PRNG} gen
|
||||
*/
|
||||
(y, gen) => { // insert text
|
||||
const ytext = y.getText('text')
|
||||
const ytext = y.get('text')
|
||||
const insertPos = prng.int32(gen, 0, ytext.length)
|
||||
const attrs = prng.oneOf(gen, marksChoices)
|
||||
const text = charCounter++ + prng.word(gen)
|
||||
@@ -2086,12 +2086,12 @@ const qChanges = [
|
||||
* @param {prng.PRNG} gen
|
||||
*/
|
||||
(y, gen) => { // insert embed
|
||||
const ytext = y.getText('text')
|
||||
const ytext = y.get('text')
|
||||
const insertPos = prng.int32(gen, 0, ytext.length)
|
||||
if (prng.bool(gen)) {
|
||||
ytext.insertEmbed(insertPos, { image: 'https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png' })
|
||||
ytext.insert(insertPos, [{ image: 'https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png' }])
|
||||
} else {
|
||||
ytext.insertEmbed(insertPos, new Y.Map([[prng.word(gen), prng.word(gen)]]))
|
||||
ytext.insert(insertPos, [new Y.Type([[prng.word(gen), prng.word(gen)]])])
|
||||
}
|
||||
},
|
||||
/**
|
||||
@@ -2099,7 +2099,7 @@ const qChanges = [
|
||||
* @param {prng.PRNG} gen
|
||||
*/
|
||||
(y, gen) => { // delete text
|
||||
const ytext = y.getText('text')
|
||||
const ytext = y.get('text')
|
||||
const contentLen = ytext.toString().length
|
||||
const insertPos = prng.int32(gen, 0, contentLen)
|
||||
const overwrite = math.min(prng.int32(gen, 0, contentLen - insertPos), 2)
|
||||
@@ -2110,7 +2110,7 @@ const qChanges = [
|
||||
* @param {prng.PRNG} gen
|
||||
*/
|
||||
(y, gen) => { // format text
|
||||
const ytext = y.getText('text')
|
||||
const ytext = y.get('text')
|
||||
const contentLen = ytext.toString().length
|
||||
const insertPos = prng.int32(gen, 0, contentLen)
|
||||
const overwrite = math.min(prng.int32(gen, 0, contentLen - insertPos), 2)
|
||||
@@ -2122,7 +2122,7 @@ const qChanges = [
|
||||
* @param {prng.PRNG} gen
|
||||
*/
|
||||
(y, gen) => { // insert codeblock
|
||||
const ytext = y.getText('text')
|
||||
const ytext = y.get('text')
|
||||
const insertPos = prng.int32(gen, 0, ytext.toString().length)
|
||||
const text = charCounter++ + prng.word(gen)
|
||||
const d = delta.create()
|
||||
@@ -2134,7 +2134,7 @@ const qChanges = [
|
||||
* @param {prng.PRNG} gen
|
||||
*/
|
||||
(y, gen) => { // complex delta op
|
||||
const ytext = y.getText('text')
|
||||
const ytext = y.get('text')
|
||||
const contentLen = ytext.toString().length
|
||||
let currentPos = math.max(0, prng.int32(gen, 0, contentLen - 1))
|
||||
const d = delta.create().retain(currentPos)
|
||||
@@ -2191,7 +2191,7 @@ export const testAttributionManagerDefaultPerformance = tc => {
|
||||
const MaxDeletionLength = 5 // 25% chance of deletion
|
||||
const MaxInsertionLength = 5
|
||||
const ydoc = new Y.Doc()
|
||||
const ytext = ydoc.getText()
|
||||
const ytext = ydoc.get()
|
||||
for (let i = 0; i < N; i++) {
|
||||
if (prng.bool(tc.prng) && prng.bool(tc.prng) && ytext.length > 0) {
|
||||
const index = prng.int31(tc.prng, 0, ytext.length - 1)
|
||||
|
||||
@@ -5,24 +5,24 @@ import * as delta from 'lib0/delta'
|
||||
|
||||
export const testCustomTypings = () => {
|
||||
const ydoc = new Y.Doc()
|
||||
const ymap = ydoc.getMap()
|
||||
const ymap = ydoc.get()
|
||||
/**
|
||||
* @type {Y.XmlElement<{ num: number, str: string, [k:string]: object|number|string }>}
|
||||
* @type {Y.Type<{ attrs: { num: number, str: string, [k:string]: number|string } }>}
|
||||
*/
|
||||
const yxml = ymap.set('yxml', new Y.XmlElement('test'))
|
||||
const yxml = ymap.setAttr('yxml', new Y.Type('test'))
|
||||
/**
|
||||
* @type {number|undefined}
|
||||
*/
|
||||
const num = yxml.getAttribute('num')
|
||||
const num = yxml.getAttr('num')
|
||||
/**
|
||||
* @type {string|undefined}
|
||||
*/
|
||||
const str = yxml.getAttribute('str')
|
||||
const str = yxml.getAttr('str')
|
||||
/**
|
||||
* @type {object|number|string|undefined}
|
||||
*/
|
||||
const dtrn = yxml.getAttribute('dtrn')
|
||||
const attrs = yxml.getAttributes()
|
||||
const dtrn = yxml.getAttr('dtrn')
|
||||
const attrs = yxml.getAttrs()
|
||||
/**
|
||||
* @type {object|number|string|undefined}
|
||||
*/
|
||||
@@ -35,10 +35,10 @@ export const testCustomTypings = () => {
|
||||
*/
|
||||
export const testSetProperty = tc => {
|
||||
const { testConnector, users, xml0, xml1 } = init(tc, { users: 2 })
|
||||
xml0.setAttribute('height', '10')
|
||||
t.assert(xml0.getAttribute('height') === '10', 'Simple set+get works')
|
||||
xml0.setAttr('height', '10')
|
||||
t.assert(xml0.getAttr('height') === '10', 'Simple set+get works')
|
||||
testConnector.flushAllMessages()
|
||||
t.assert(xml1.getAttribute('height') === '10', 'Simple set+get works (remote)')
|
||||
t.assert(xml1.getAttr('height') === '10', 'Simple set+get works (remote)')
|
||||
compare(users)
|
||||
}
|
||||
|
||||
@@ -47,15 +47,14 @@ export const testSetProperty = tc => {
|
||||
*/
|
||||
export const testHasProperty = tc => {
|
||||
const { testConnector, users, xml0, xml1 } = init(tc, { users: 2 })
|
||||
xml0.setAttribute('height', '10')
|
||||
t.assert(xml0.hasAttribute('height'), 'Simple set+has works')
|
||||
xml0.setAttr('height', '10')
|
||||
t.assert(xml0.hasAttr('height'), 'Simple set+has works')
|
||||
testConnector.flushAllMessages()
|
||||
t.assert(xml1.hasAttribute('height'), 'Simple set+has works (remote)')
|
||||
|
||||
xml0.removeAttribute('height')
|
||||
t.assert(!xml0.hasAttribute('height'), 'Simple set+remove+has works')
|
||||
t.assert(xml1.hasAttr('height'), 'Simple set+has works (remote)')
|
||||
xml0.deleteAttr('height')
|
||||
t.assert(!xml0.hasAttr('height'), 'Simple set+remove+has works')
|
||||
testConnector.flushAllMessages()
|
||||
t.assert(!xml1.hasAttribute('height'), 'Simple set+remove+has works (remote)')
|
||||
t.assert(!xml1.hasAttr('height'), 'Simple set+remove+has works (remote)')
|
||||
compare(users)
|
||||
}
|
||||
|
||||
@@ -64,13 +63,13 @@ export const testHasProperty = tc => {
|
||||
*/
|
||||
export const testYtextAttributes = _tc => {
|
||||
const ydoc = new Y.Doc()
|
||||
const ytext = /** @type {Y.XmlText} */ (ydoc.get('', Y.XmlText))
|
||||
const ytext = ydoc.get('')
|
||||
ytext.observe(event => {
|
||||
t.assert(event.delta.attrs.test?.type === 'insert')
|
||||
})
|
||||
ytext.setAttribute('test', 42)
|
||||
t.compare(ytext.getAttribute('test'), 42)
|
||||
t.compare(ytext.getAttributes(), { test: 42 })
|
||||
ytext.setAttr('test', 42)
|
||||
t.compare(ytext.getAttr('test'), 42)
|
||||
t.compare(ytext.getAttrs(), { test: 42 })
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -78,15 +77,12 @@ export const testYtextAttributes = _tc => {
|
||||
*/
|
||||
export const testSiblings = _tc => {
|
||||
const ydoc = new Y.Doc()
|
||||
const yxml = ydoc.getXmlFragment()
|
||||
const first = new Y.XmlText()
|
||||
const second = new Y.XmlElement('p')
|
||||
const yxml = ydoc.get()
|
||||
const first = new Y.Type()
|
||||
const second = new Y.Type('p')
|
||||
yxml.insert(0, [first, second])
|
||||
t.assert(first.nextSibling === second)
|
||||
t.assert(second.prevSibling === first)
|
||||
t.assert(first.parent === /** @type {Y.AbstractType<any>} */ (yxml))
|
||||
t.assert(yxml.parent === null)
|
||||
t.assert(yxml.firstChild === first)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,13 +90,13 @@ export const testSiblings = _tc => {
|
||||
*/
|
||||
export const testInsertafter = _tc => {
|
||||
const ydoc = new Y.Doc()
|
||||
const yxml = ydoc.getXmlFragment()
|
||||
const first = new Y.XmlText()
|
||||
const second = new Y.XmlElement('p')
|
||||
const third = new Y.XmlElement('p')
|
||||
const yxml = ydoc.get()
|
||||
const first = new Y.Type()
|
||||
const second = new Y.Type('p')
|
||||
const third = new Y.Type('p')
|
||||
|
||||
const deepsecond1 = new Y.XmlElement('span')
|
||||
const deepsecond2 = new Y.XmlText()
|
||||
const deepsecond1 = new Y.Type('span')
|
||||
const deepsecond2 = new Y.Type()
|
||||
second.insertAfter(null, [deepsecond1])
|
||||
second.insertAfter(deepsecond1, [deepsecond2])
|
||||
|
||||
@@ -114,8 +110,8 @@ export const testInsertafter = _tc => {
|
||||
t.compareArrays(yxml.toArray(), [first, second, third])
|
||||
|
||||
t.fails(() => {
|
||||
const el = new Y.XmlElement('p')
|
||||
el.insertAfter(deepsecond1, [new Y.XmlText()])
|
||||
const el = new Y.Type('p')
|
||||
el.insertAfter(deepsecond1, [new Y.Type()])
|
||||
})
|
||||
}
|
||||
|
||||
@@ -124,14 +120,14 @@ export const testInsertafter = _tc => {
|
||||
*/
|
||||
export const testClone = _tc => {
|
||||
const ydoc = new Y.Doc()
|
||||
const yxml = ydoc.getXmlFragment()
|
||||
const first = new Y.XmlText('text')
|
||||
const second = new Y.XmlElement('p')
|
||||
const third = new Y.XmlElement('p')
|
||||
const yxml = ydoc.get()
|
||||
const first = new Y.Type('text')
|
||||
const second = new Y.Type('p')
|
||||
const third = new Y.Type('p')
|
||||
yxml.push([first, second, third])
|
||||
t.compareArrays(yxml.toArray(), [first, second, third])
|
||||
const cloneYxml = yxml.clone()
|
||||
ydoc.getArray('copyarr').insert(0, [cloneYxml])
|
||||
ydoc.get('copyarr').insert(0, [cloneYxml])
|
||||
t.assert(cloneYxml.length === 3)
|
||||
t.compare(cloneYxml.toJSON(), yxml.toJSON())
|
||||
}
|
||||
@@ -141,7 +137,7 @@ export const testClone = _tc => {
|
||||
*/
|
||||
export const testFormattingBug = _tc => {
|
||||
const ydoc = new Y.Doc()
|
||||
const yxml = /** @type {Y.XmlText} */ (ydoc.get('', Y.XmlText))
|
||||
const yxml = ydoc.get()
|
||||
const q = delta.create()
|
||||
.insert('A', { em: {}, strong: {} })
|
||||
.insert('B', { em: {} })
|
||||
@@ -155,9 +151,9 @@ export const testFormattingBug = _tc => {
|
||||
*/
|
||||
export const testElement = _tc => {
|
||||
const ydoc = new Y.Doc()
|
||||
const yxmlel = ydoc.getXmlElement()
|
||||
const text1 = new Y.XmlText('text1')
|
||||
const text2 = new Y.XmlText('text2')
|
||||
const yxmlel = ydoc.get()
|
||||
const text1 = new Y.Type('text1')
|
||||
const text2 = new Y.Type('text2')
|
||||
yxmlel.insert(0, [text1, text2])
|
||||
t.compareArrays(yxmlel.toArray(), [text1, text2])
|
||||
}
|
||||
@@ -167,12 +163,12 @@ export const testElement = _tc => {
|
||||
*/
|
||||
export const testFragmentAttributedContent = _tc => {
|
||||
const ydoc = new Y.Doc({ gc: false })
|
||||
const yfragment = new Y.XmlFragment()
|
||||
const elem1 = new Y.XmlText('hello')
|
||||
const elem2 = new Y.XmlElement()
|
||||
const elem3 = new Y.XmlText('world')
|
||||
const yfragment = new Y.Type()
|
||||
const elem1 = new Y.Type('hello')
|
||||
const elem2 = new Y.Type()
|
||||
const elem3 = new Y.Type('world')
|
||||
yfragment.insert(0, [elem1, elem2])
|
||||
ydoc.getArray().insert(0, [yfragment])
|
||||
ydoc.get().insert(0, [yfragment])
|
||||
let attributionManager = Y.noAttributionsManager
|
||||
ydoc.on('afterTransaction', tr => {
|
||||
// attributionManager = new TwosetAttributionManager(createIdMapFromIdSet(tr.insertSet, [new Y.Attribution('insertedAt', 42), new Y.Attribution('insert', 'kevin')]), createIdMapFromIdSet(tr.deleteSet, [new Y.Attribution('delete', 'kevin')]))
|
||||
@@ -196,10 +192,10 @@ export const testFragmentAttributedContent = _tc => {
|
||||
*/
|
||||
export const testElementAttributedContent = _tc => {
|
||||
const ydoc = new Y.Doc({ gc: false })
|
||||
const yelement = ydoc.getXmlElement('p')
|
||||
const elem1 = new Y.XmlText('hello')
|
||||
const elem2 = new Y.XmlElement('span')
|
||||
const elem3 = new Y.XmlText('world')
|
||||
const yelement = ydoc.get('p')
|
||||
const elem1 = new Y.Type('hello')
|
||||
const elem2 = new Y.Type('span')
|
||||
const elem3 = new Y.Type('world')
|
||||
yelement.insert(0, [elem1, elem2])
|
||||
let attributionManager = Y.noAttributionsManager
|
||||
ydoc.on('afterTransaction', tr => {
|
||||
@@ -210,9 +206,9 @@ export const testElementAttributedContent = _tc => {
|
||||
ydoc.transact(() => {
|
||||
yelement.delete(0, 1)
|
||||
yelement.insert(1, [elem3])
|
||||
yelement.setAttribute('key', '42')
|
||||
yelement.setAttr('key', '42')
|
||||
})
|
||||
const expectedContent = delta.create('UNDEFINED').insert([elem1], null, { delete: [] }).insert([elem2]).insert([elem3], null, { insert: [] }).set('key', '42', { insert: [] })
|
||||
const expectedContent = delta.create('UNDEFINED').insert([elem1], null, { delete: [] }).insert([elem2]).insert([elem3], null, { insert: [] }).setAttr('key', '42', { insert: [] })
|
||||
const attributedContent = yelement.getContent(attributionManager)
|
||||
console.log('children', attributedContent.toJSON())
|
||||
console.log('attributes', attributedContent)
|
||||
@@ -221,15 +217,15 @@ export const testElementAttributedContent = _tc => {
|
||||
t.group('test getContentDeep', () => {
|
||||
const expectedContent = delta.create('UNDEFINED')
|
||||
.insert(
|
||||
[delta.text().insert('hello', null, { delete: [] })],
|
||||
[delta.create().insert('hello', null, { delete: [] })],
|
||||
null,
|
||||
{ delete: [] }
|
||||
)
|
||||
.insert([delta.create('span')])
|
||||
.insert([
|
||||
delta.text().insert('world', null, { insert: [] })
|
||||
delta.create().insert('world', null, { insert: [] })
|
||||
], null, { insert: [] })
|
||||
.set('key', '42', { insert: [] })
|
||||
.setAttr('key', '42', { insert: [] })
|
||||
.done()
|
||||
const attributedContent = yelement.getContentDeep(attributionManager)
|
||||
console.log('children', JSON.stringify(attributedContent.toJSON().children, null, 2))
|
||||
@@ -247,19 +243,19 @@ export const testElementAttributedContent = _tc => {
|
||||
*/
|
||||
export const testElementAttributedContentViaDiffer = _tc => {
|
||||
const ydocV1 = new Y.Doc()
|
||||
ydocV1.getXmlElement('p').insert(0, [new Y.XmlText('hello'), new Y.XmlElement('span')])
|
||||
ydocV1.get('p').insert(0, [new Y.Type('hello'), new Y.Type('span')])
|
||||
const ydoc = new Y.Doc()
|
||||
Y.applyUpdate(ydoc, Y.encodeStateAsUpdate(ydocV1))
|
||||
const yelement = ydoc.getXmlElement('p')
|
||||
const yelement = ydoc.get('p')
|
||||
const elem2 = yelement.get(1) // new Y.XmlElement('span')
|
||||
const elem3 = new Y.XmlText('world')
|
||||
const elem3 = new Y.Type('world')
|
||||
ydoc.transact(() => {
|
||||
yelement.delete(0, 1)
|
||||
yelement.insert(1, [elem3])
|
||||
yelement.setAttribute('key', '42')
|
||||
yelement.setAttr('key', '42')
|
||||
})
|
||||
const attributionManager = Y.createAttributionManagerFromDiff(ydocV1, ydoc)
|
||||
const expectedContent = delta.create('UNDEFINED').insert([delta.create().insert('hello')], null, { delete: [] }).insert([elem2.getContentDeep()]).insert([delta.create().insert('world', null, { insert: [] })], null, { insert: [] }).set('key', '42', { insert: [] })
|
||||
const expectedContent = delta.create('UNDEFINED').insert([delta.create().insert('hello')], null, { delete: [] }).insert([elem2.getContentDeep()]).insert([delta.create().insert('world', null, { insert: [] })], null, { insert: [] }).setAttr('key', '42', { insert: [] })
|
||||
const attributedContent = yelement.getContentDeep(attributionManager)
|
||||
console.log('children', attributedContent.toJSON().children)
|
||||
console.log('attributes', attributedContent.toJSON().attrs)
|
||||
@@ -277,7 +273,7 @@ export const testElementAttributedContentViaDiffer = _tc => {
|
||||
.insert([
|
||||
delta.create().insert('world', null, { insert: [] })
|
||||
], null, { insert: [] })
|
||||
.set('key', '42', { insert: [] })
|
||||
.setAttr('key', '42', { insert: [] })
|
||||
const attributedContent = yelement.getContentDeep(attributionManager)
|
||||
console.log('children', JSON.stringify(attributedContent.toJSON().children, null, 2))
|
||||
console.log('cs expec', JSON.stringify(expectedContent.toJSON(), null, 2))
|
||||
@@ -301,7 +297,7 @@ export const testElementAttributedContentViaDiffer = _tc => {
|
||||
.insert([
|
||||
delta.create().insert('bigworld', null, { insert: [] })
|
||||
], null, { insert: [] })
|
||||
.set('key', '42', { insert: [] })
|
||||
.setAttr('key', '42', { insert: [] })
|
||||
const attributedContent = yelement.getContentDeep(attributionManager)
|
||||
console.log('children', JSON.stringify(attributedContent.toJSON().children, null, 2))
|
||||
console.log('cs expec', JSON.stringify(expectedContent.toJSON(), null, 2))
|
||||
@@ -315,7 +311,7 @@ export const testElementAttributedContentViaDiffer = _tc => {
|
||||
t.info('expecting diffingAttributionManager to auto update itself')
|
||||
const expectedContent = delta.create('UNDEFINED').insert([delta.create('span')]).insert([
|
||||
delta.create().insert('bigworld')
|
||||
]).set('key', '42')
|
||||
]).setAttr('key', '42')
|
||||
const attributedContent = yelement.getContentDeep(attributionManager)
|
||||
console.log('children', JSON.stringify(attributedContent.toJSON().children, null, 2))
|
||||
console.log('cs expec', JSON.stringify(expectedContent.toJSON(), null, 2))
|
||||
@@ -333,21 +329,21 @@ export const testAttributionManagerSimpleExample = _tc => {
|
||||
const ydoc = new Y.Doc()
|
||||
ydoc.clientID = 0
|
||||
// create some initial content
|
||||
ydoc.getXmlFragment().insert(0, [new Y.XmlText('hello world')])
|
||||
ydoc.get().insert(0, [new Y.Type('hello world')])
|
||||
const ydocFork = new Y.Doc()
|
||||
ydocFork.clientID = 1
|
||||
Y.applyUpdate(ydocFork, Y.encodeStateAsUpdate(ydoc))
|
||||
// modify the fork
|
||||
// append a span element
|
||||
ydocFork.getXmlFragment().insert(1, [new Y.XmlElement('span')])
|
||||
const ytext = /** @type {Y.XmlText} */ (ydocFork.getXmlFragment().get(0))
|
||||
ydocFork.get().insert(1, [new Y.Type('span')])
|
||||
const ytext = ydocFork.get().get(0)
|
||||
// make "hello" italic
|
||||
ytext.format(0, 5, { italic: true })
|
||||
ytext.insert(11, 'deleteme')
|
||||
ytext.delete(11, 8)
|
||||
ytext.insert(11, '!')
|
||||
// highlight the changes
|
||||
console.log(JSON.stringify(ydocFork.getXmlFragment().getContentDeep(Y.createAttributionManagerFromDiff(ydoc, ydocFork)), null, 2))
|
||||
console.log(JSON.stringify(ydocFork.get().getContentDeep(Y.createAttributionManagerFromDiff(ydoc, ydocFork)), null, 2))
|
||||
/* =>
|
||||
{
|
||||
"children": {
|
||||
|
||||
Reference in New Issue
Block a user