Compare commits

...

17 Commits

Author SHA1 Message Date
Kevin Jahns
1e69d650b8 13.6.14 2024-03-01 11:31:21 +01:00
Kevin Jahns
133cfc9cdc allow falsy values in formatting attributes 2024-03-01 11:29:14 +01:00
Kevin Jahns
83db6c814c Merge pull request #619 from jul13579/allow-falsy-attribute-values
Allow falsy attribute values
2024-03-01 11:23:04 +01:00
Julian Lehrhuber
cdbb55818d Allow falsy attribute values 2024-03-01 10:37:51 +01:00
Kevin Jahns
90675be3ab 13.6.13 2024-02-29 17:37:25 +01:00
Kevin Jahns
541306b254 migrate to ObservableV2 2024-02-29 17:08:57 +01:00
Kevin Jahns
53173a9ea7 Merge branch 'mylesj-feat/undomanager-doingstackitem' 2024-02-29 14:47:00 +01:00
Kevin Jahns
29fa60ccf9 [Undo] add UndoManager.currStackItem 2024-02-29 14:46:43 +01:00
Myles J
917261a1ce Facilitate referencing UndoManager StackItem inside Type observers 2024-02-29 00:50:36 +00:00
Kevin Jahns
a9dc72fcc0 Merge pull request #612 from MentalGear/patch-1
docs: fix typo
2024-02-25 10:31:20 +01:00
Kevin Jahns
90a90ab010 add y-fire to provider list #189 2024-02-20 19:52:58 +01:00
MentalGear
009f6ab551 docs: fix typo 2024-02-17 20:38:53 +01:00
Kevin Jahns
a8582442e3 13.6.12 2024-02-09 23:38:50 +01:00
Kevin Jahns
f54ea625e2 Merge branch 'raineorshine-getXmlElement' 2024-02-09 23:31:29 +01:00
Kevin Jahns
ce06b2abec update deps 2024-02-09 23:31:07 +01:00
Kevin Jahns
e1bce03ed8 better typings for ydoc.get 2024-02-09 23:27:24 +01:00
Raine Revere
16d9638bc8 Add ydoc.getXmlElement 2024-02-05 13:35:32 +00:00
12 changed files with 889 additions and 456 deletions

View File

@@ -226,6 +226,10 @@ y-websocket provider.
<dd> <dd>
Like y-indexeddb, but with sub-documents support and fully TypeScript. Like y-indexeddb, but with sub-documents support and fully TypeScript.
</dd> </dd>
<dt><a href="https://github.com/podraven/y-fire">y-fire</a></dt>
<dd>
A database and connection provider for Yjs based on Firestore.
</dd>
</dl> </dl>
# Ports # Ports
@@ -739,6 +743,8 @@ type. Doesn't log types that have not been defined (using
<dd>Define a shared Y.Map type. Is equivalent to <code>y.get(string, Y.Map)</code>.</dd> <dd>Define a shared Y.Map type. Is equivalent to <code>y.get(string, Y.Map)</code>.</dd>
<b><code>getText(string):Y.Text</code></b> <b><code>getText(string):Y.Text</code></b>
<dd>Define a shared Y.Text type. Is equivalent to <code>y.get(string, Y.Text)</code>.</dd> <dd>Define a shared Y.Text type. Is equivalent to <code>y.get(string, Y.Text)</code>.</dd>
<b><code>getXmlElement(string, string):Y.XmlElement</code></b>
<dd>Define a shared Y.XmlElement type. Is equivalent to <code>y.get(string, Y.XmlElement)</code>.</dd>
<b><code>getXmlFragment(string):Y.XmlFragment</code></b> <b><code>getXmlFragment(string):Y.XmlFragment</code></b>
<dd>Define a shared Y.XmlFragment type. Is equivalent to <code>y.get(string, Y.XmlFragment)</code>.</dd> <dd>Define a shared Y.XmlFragment type. Is equivalent to <code>y.get(string, Y.XmlFragment)</code>.</dd>
<b><code>on(string, function)</code></b> <b><code>on(string, function)</code></b>
@@ -876,7 +882,7 @@ ydoc2.getText().toString() // => "00000000000"
#### Using V2 update format #### Using V2 update format
Yjs implements two update formats. By default you are using the V1 update format. Yjs implements two update formats. By default you are using the V1 update format.
You can opt-in into the V2 update format wich provides much better compression. You can opt-in into the V2 update format which provides much better compression.
It is not yet used by all providers. However, you can already use it if It is not yet used by all providers. However, you can already use it if
you are building your own provider. All below functions are available with the you are building your own provider. All below functions are available with the
suffix "V2". E.g. `Y.applyUpdate``Y.applyUpdateV2`. Also when listening to updates suffix "V2". E.g. `Y.applyUpdate``Y.applyUpdateV2`. Also when listening to updates

1129
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "yjs", "name": "yjs",
"version": "13.6.11", "version": "13.6.14",
"description": "Shared Editing Library", "description": "Shared Editing Library",
"main": "./dist/yjs.cjs", "main": "./dist/yjs.cjs",
"module": "./dist/yjs.mjs", "module": "./dist/yjs.mjs",
@@ -12,9 +12,10 @@
"url": "https://github.com/sponsors/dmonad" "url": "https://github.com/sponsors/dmonad"
}, },
"scripts": { "scripts": {
"clean": "rm -rf dist docs",
"test": "npm run dist && node ./dist/tests.cjs --repetition-time 50", "test": "npm run dist && node ./dist/tests.cjs --repetition-time 50",
"test-extensive": "npm run lint && npm run dist && node ./dist/tests.cjs --production --repetition-time 10000", "test-extensive": "npm run lint && npm run dist && node ./dist/tests.cjs --production --repetition-time 10000",
"dist": "rm -rf dist && rollup -c && tsc", "dist": "npm run clean && rollup -c && tsc",
"watch": "rollup -wc", "watch": "rollup -wc",
"lint": "markdownlint README.md && standard && tsc", "lint": "markdownlint README.md && standard && tsc",
"docs": "rm -rf docs; jsdoc --configure ./.jsdoc.json --verbose --readme ./README.md --package ./package.json || true", "docs": "rm -rf docs; jsdoc --configure ./.jsdoc.json --verbose --readme ./README.md --package ./package.json || true",

View File

@@ -201,7 +201,7 @@ const minimizeAttributeChanges = (currPos, attributes) => {
while (true) { while (true) {
if (currPos.right === null) { if (currPos.right === null) {
break break
} else if (currPos.right.deleted || (currPos.right.content.constructor === ContentFormat && equalAttrs(attributes[(/** @type {ContentFormat} */ (currPos.right.content)).key] || null, /** @type {ContentFormat} */ (currPos.right.content).value))) { } else if (currPos.right.deleted || (currPos.right.content.constructor === ContentFormat && equalAttrs(attributes[(/** @type {ContentFormat} */ (currPos.right.content)).key] ?? null, /** @type {ContentFormat} */ (currPos.right.content).value))) {
// //
} else { } else {
break break
@@ -227,7 +227,7 @@ const insertAttributes = (transaction, parent, currPos, attributes) => {
// insert format-start items // insert format-start items
for (const key in attributes) { for (const key in attributes) {
const val = attributes[key] const val = attributes[key]
const currentVal = currPos.currentAttributes.get(key) || null const currentVal = currPos.currentAttributes.get(key) ?? null
if (!equalAttrs(currentVal, val)) { if (!equalAttrs(currentVal, val)) {
// save negated attribute (set null if currentVal undefined) // save negated attribute (set null if currentVal undefined)
negatedAttributes.set(key, currentVal) negatedAttributes.set(key, currentVal)
@@ -389,12 +389,12 @@ const cleanupFormattingGap = (transaction, start, curr, startAttributes, currAtt
switch (content.constructor) { switch (content.constructor) {
case ContentFormat: { case ContentFormat: {
const { key, value } = /** @type {ContentFormat} */ (content) const { key, value } = /** @type {ContentFormat} */ (content)
const startAttrValue = startAttributes.get(key) || null const startAttrValue = startAttributes.get(key) ?? null
if (endFormats.get(key) !== content || startAttrValue === value) { if (endFormats.get(key) !== content || startAttrValue === value) {
// Either this format is overwritten or it is not necessary because the attribute already existed. // Either this format is overwritten or it is not necessary because the attribute already existed.
start.delete(transaction) start.delete(transaction)
cleanups++ cleanups++
if (!reachedCurr && (currAttributes.get(key) || null) === value && startAttrValue !== value) { if (!reachedCurr && (currAttributes.get(key) ?? null) === value && startAttrValue !== value) {
if (startAttrValue === null) { if (startAttrValue === null) {
currAttributes.delete(key) currAttributes.delete(key)
} else { } else {
@@ -769,12 +769,12 @@ export class YTextEvent extends YEvent {
const { key, value } = /** @type {ContentFormat} */ (item.content) const { key, value } = /** @type {ContentFormat} */ (item.content)
if (this.adds(item)) { if (this.adds(item)) {
if (!this.deletes(item)) { if (!this.deletes(item)) {
const curVal = currentAttributes.get(key) || null const curVal = currentAttributes.get(key) ?? null
if (!equalAttrs(curVal, value)) { if (!equalAttrs(curVal, value)) {
if (action === 'retain') { if (action === 'retain') {
addOp() addOp()
} }
if (equalAttrs(value, (oldAttributes.get(key) || null))) { if (equalAttrs(value, (oldAttributes.get(key) ?? null))) {
delete attributes[key] delete attributes[key]
} else { } else {
attributes[key] = value attributes[key] = value
@@ -785,7 +785,7 @@ export class YTextEvent extends YEvent {
} }
} else if (this.deletes(item)) { } else if (this.deletes(item)) {
oldAttributes.set(key, value) oldAttributes.set(key, value)
const curVal = currentAttributes.get(key) || null const curVal = currentAttributes.get(key) ?? null
if (!equalAttrs(curVal, value)) { if (!equalAttrs(curVal, value)) {
if (action === 'retain') { if (action === 'retain') {
addOp() addOp()

View File

@@ -20,7 +20,7 @@ import {
/** /**
* An YXmlElement imitates the behavior of a * An YXmlElement imitates the behavior of a
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}. * https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element
* *
* * An YXmlElement has attributes (key value pairs) * * An YXmlElement has attributes (key value pairs)
* * An YXmlElement has childElements that must inherit from YXmlElement * * An YXmlElement has childElements that must inherit from YXmlElement

View File

@@ -1,4 +1,4 @@
import { Observable } from 'lib0/observable' import { ObservableV2 } from 'lib0/observable'
import { import {
Doc // eslint-disable-line Doc // eslint-disable-line
@@ -10,9 +10,9 @@ import {
* @note This interface is experimental and it is not advised to actually inherit this class. * @note This interface is experimental and it is not advised to actually inherit this class.
* It just serves as typing information. * It just serves as typing information.
* *
* @extends {Observable<any>} * @extends {ObservableV2<any>}
*/ */
export class AbstractConnector extends Observable { export class AbstractConnector extends ObservableV2 {
/** /**
* @param {Doc} ydoc * @param {Doc} ydoc
* @param {any} awareness * @param {any} awareness

View File

@@ -8,12 +8,13 @@ import {
YArray, YArray,
YText, YText,
YMap, YMap,
YXmlElement,
YXmlFragment, YXmlFragment,
transact, transact,
ContentDoc, Item, Transaction, YEvent // eslint-disable-line ContentDoc, Item, Transaction, YEvent // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import { Observable } from 'lib0/observable' import { ObservableV2 } from 'lib0/observable'
import * as random from 'lib0/random' import * as random from 'lib0/random'
import * as map from 'lib0/map' import * as map from 'lib0/map'
import * as array from 'lib0/array' import * as array from 'lib0/array'
@@ -33,10 +34,26 @@ export const generateNewClientId = random.uint32
*/ */
/** /**
* A Yjs instance handles the state of shared data. * @typedef {Object} DocEvents
* @extends Observable<string> * @property {function(Doc):void} DocEvents.destroy
* @property {function(Doc):void} DocEvents.load
* @property {function(boolean, Doc):void} DocEvents.sync
* @property {function(Uint8Array, any, Doc, Transaction):void} DocEvents.update
* @property {function(Uint8Array, any, Doc, Transaction):void} DocEvents.updateV2
* @property {function(Doc):void} DocEvents.beforeAllTransactions
* @property {function(Transaction, Doc):void} DocEvents.beforeTransaction
* @property {function(Transaction, Doc):void} DocEvents.beforeObserverCalls
* @property {function(Transaction, Doc):void} DocEvents.afterTransaction
* @property {function(Transaction, Doc):void} DocEvents.afterTransactionCleanup
* @property {function(Doc, Array<Transaction>):void} DocEvents.afterAllTransactions
* @property {function({ loaded: Set<Doc>, added: Set<Doc>, removed: Set<Doc> }, Doc, Transaction):void} DocEvents.subdocs
*/ */
export class Doc extends Observable {
/**
* A Yjs instance handles the state of shared data.
* @extends ObservableV2<DocEvents>
*/
export class Doc extends ObservableV2 {
/** /**
* @param {DocOpts} opts configuration * @param {DocOpts} opts configuration
*/ */
@@ -114,7 +131,7 @@ export class Doc extends Observable {
} }
this.isSynced = isSynced === undefined || isSynced === true this.isSynced = isSynced === undefined || isSynced === true
if (this.isSynced && !this.isLoaded) { if (this.isSynced && !this.isLoaded) {
this.emit('load', []) this.emit('load', [this])
} }
}) })
/** /**
@@ -180,6 +197,7 @@ export class Doc extends Observable {
* Define all types right after the Yjs instance is created and store them in a separate object. * Define all types right after the Yjs instance is created and store them in a separate object.
* Also use the typed methods `getText(name)`, `getArray(name)`, .. * Also use the typed methods `getText(name)`, `getArray(name)`, ..
* *
* @template {typeof AbstractType<any>} Type
* @example * @example
* const y = new Y(..) * const y = new Y(..)
* const appState = { * const appState = {
@@ -188,12 +206,12 @@ export class Doc extends Observable {
* } * }
* *
* @param {string} name * @param {string} name
* @param {Function} TypeConstructor The constructor of the type definition. E.g. Y.Text, Y.Array, Y.Map, ... * @param {Type} TypeConstructor The constructor of the type definition. E.g. Y.Text, Y.Array, Y.Map, ...
* @return {AbstractType<any>} The created type. Constructed with TypeConstructor * @return {InstanceType<Type>} The created type. Constructed with TypeConstructor
* *
* @public * @public
*/ */
get (name, TypeConstructor = AbstractType) { get (name, TypeConstructor = /** @type {any} */ (AbstractType)) {
const type = map.setIfUndefined(this.share, name, () => { const type = map.setIfUndefined(this.share, name, () => {
// @ts-ignore // @ts-ignore
const t = new TypeConstructor() const t = new TypeConstructor()
@@ -219,12 +237,12 @@ export class Doc extends Observable {
t._length = type._length t._length = type._length
this.share.set(name, t) this.share.set(name, t)
t._integrate(this, null) t._integrate(this, null)
return t return /** @type {InstanceType<Type>} */ (t)
} else { } else {
throw new Error(`Type with the name ${name} has already been defined with a different constructor`) throw new Error(`Type with the name ${name} has already been defined with a different constructor`)
} }
} }
return type return /** @type {InstanceType<Type>} */ (type)
} }
/** /**
@@ -235,8 +253,7 @@ export class Doc extends Observable {
* @public * @public
*/ */
getArray (name = '') { getArray (name = '') {
// @ts-ignore return /** @type {YArray<T>} */ (this.get(name, YArray))
return this.get(name, YArray)
} }
/** /**
@@ -246,7 +263,6 @@ export class Doc extends Observable {
* @public * @public
*/ */
getText (name = '') { getText (name = '') {
// @ts-ignore
return this.get(name, YText) return this.get(name, YText)
} }
@@ -258,8 +274,17 @@ export class Doc extends Observable {
* @public * @public
*/ */
getMap (name = '') { getMap (name = '') {
// @ts-ignore return /** @type {YMap<T>} */ (this.get(name, YMap))
return this.get(name, YMap) }
/**
* @param {string} [name]
* @return {YXmlElement}
*
* @public
*/
getXmlElement (name = '') {
return /** @type {YXmlElement<{[key:string]:string}>} */ (this.get(name, YXmlElement))
} }
/** /**
@@ -269,7 +294,6 @@ export class Doc extends Observable {
* @public * @public
*/ */
getXmlFragment (name = '') { getXmlFragment (name = '') {
// @ts-ignore
return this.get(name, YXmlFragment) return this.get(name, YXmlFragment)
} }
@@ -313,24 +337,9 @@ export class Doc extends Observable {
transaction.subdocsRemoved.add(this) transaction.subdocsRemoved.add(this)
}, null, true) }, null, true)
} }
this.emit('destroyed', [true]) // @ts-ignore
this.emit('destroyed', [true]) // DEPRECATED!
this.emit('destroy', [this]) this.emit('destroy', [this])
super.destroy() super.destroy()
} }
/**
* @param {string} eventName
* @param {function(...any):any} f
*/
on (eventName, f) {
super.on(eventName, f)
}
/**
* @param {string} eventName
* @param {function} f
*/
off (eventName, f) {
super.off(eventName, f)
}
} }

View File

@@ -10,13 +10,13 @@ import {
getItemCleanStart, getItemCleanStart,
isDeleted, isDeleted,
addToDeleteSet, addToDeleteSet,
Transaction, Doc, Item, GC, DeleteSet, AbstractType // eslint-disable-line YEvent, Transaction, Doc, Item, GC, DeleteSet, AbstractType // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as time from 'lib0/time' import * as time from 'lib0/time'
import * as array from 'lib0/array' import * as array from 'lib0/array'
import * as logging from 'lib0/logging' import * as logging from 'lib0/logging'
import { Observable } from 'lib0/observable' import { ObservableV2 } from 'lib0/observable'
export class StackItem { export class StackItem {
/** /**
@@ -48,15 +48,10 @@ const clearUndoManagerStackItem = (tr, um, stackItem) => {
/** /**
* @param {UndoManager} undoManager * @param {UndoManager} undoManager
* @param {Array<StackItem>} stack * @param {Array<StackItem>} stack
* @param {string} eventType * @param {'undo'|'redo'} eventType
* @return {StackItem?} * @return {StackItem?}
*/ */
const popStackItem = (undoManager, stack, eventType) => { const popStackItem = (undoManager, stack, eventType) => {
/**
* Whether a change happened
* @type {StackItem?}
*/
let result = null
/** /**
* Keep a reference to the transaction so we can fire the event with the changedParentTypes * Keep a reference to the transaction so we can fire the event with the changedParentTypes
* @type {any} * @type {any}
@@ -65,7 +60,7 @@ const popStackItem = (undoManager, stack, eventType) => {
const doc = undoManager.doc const doc = undoManager.doc
const scope = undoManager.scope const scope = undoManager.scope
transact(doc, transaction => { transact(doc, transaction => {
while (stack.length > 0 && result === null) { while (stack.length > 0 && undoManager.currStackItem === null) {
const store = doc.store const store = doc.store
const stackItem = /** @type {StackItem} */ (stack.pop()) const stackItem = /** @type {StackItem} */ (stack.pop())
/** /**
@@ -113,7 +108,7 @@ const popStackItem = (undoManager, stack, eventType) => {
performedChange = true performedChange = true
} }
} }
result = performedChange ? stackItem : null undoManager.currStackItem = performedChange ? stackItem : null
} }
transaction.changed.forEach((subProps, type) => { transaction.changed.forEach((subProps, type) => {
// destroy search marker if necessary // destroy search marker if necessary
@@ -123,11 +118,12 @@ const popStackItem = (undoManager, stack, eventType) => {
}) })
_tr = transaction _tr = transaction
}, undoManager) }, undoManager)
if (result != null) { if (undoManager.currStackItem != null) {
const changedParentTypes = _tr.changedParentTypes const changedParentTypes = _tr.changedParentTypes
undoManager.emit('stack-item-popped', [{ stackItem: result, type: eventType, changedParentTypes }, undoManager]) undoManager.emit('stack-item-popped', [{ stackItem: undoManager.currStackItem, type: eventType, changedParentTypes, origin: undoManager }, undoManager])
undoManager.currStackItem = null
} }
return result return undoManager.currStackItem
} }
/** /**
@@ -143,6 +139,14 @@ const popStackItem = (undoManager, stack, eventType) => {
* @property {Doc} [doc] The document that this UndoManager operates on. Only needed if typeScope is empty. * @property {Doc} [doc] The document that this UndoManager operates on. Only needed if typeScope is empty.
*/ */
/**
* @typedef {Object} StackItemEvent
* @property {StackItem} StackItemEvent.stackItem
* @property {any} StackItemEvent.origin
* @property {'undo'|'redo'} StackItemEvent.type
* @property {Map<AbstractType<YEvent<any>>,Array<YEvent<any>>>} StackItemEvent.changedParentTypes
*/
/** /**
* Fires 'stack-item-added' event when a stack item was added to either the undo- or * Fires 'stack-item-added' event when a stack item was added to either the undo- or
* the redo-stack. You may store additional stack information via the * the redo-stack. You may store additional stack information via the
@@ -150,9 +154,9 @@ const popStackItem = (undoManager, stack, eventType) => {
* Fires 'stack-item-popped' event when a stack item was popped from either the * Fires 'stack-item-popped' event when a stack item was popped from either the
* undo- or the redo-stack. You may restore the saved stack information from `event.stackItem.meta`. * undo- or the redo-stack. You may restore the saved stack information from `event.stackItem.meta`.
* *
* @extends {Observable<'stack-item-added'|'stack-item-popped'|'stack-cleared'|'stack-item-updated'>} * @extends {ObservableV2<{'stack-item-added':function(StackItemEvent, UndoManager):void, 'stack-item-popped': function(StackItemEvent, UndoManager):void, 'stack-cleared': function({ undoStackCleared: boolean, redoStackCleared: boolean }):void, 'stack-item-updated': function(StackItemEvent, UndoManager):void }>}
*/ */
export class UndoManager extends Observable { export class UndoManager extends ObservableV2 {
/** /**
* @param {AbstractType<any>|Array<AbstractType<any>>} typeScope Accepts either a single type, or an array of types * @param {AbstractType<any>|Array<AbstractType<any>>} typeScope Accepts either a single type, or an array of types
* @param {UndoManagerOptions} options * @param {UndoManagerOptions} options
@@ -191,6 +195,12 @@ export class UndoManager extends Observable {
*/ */
this.undoing = false this.undoing = false
this.redoing = false this.redoing = false
/**
* The currently popped stack item if UndoManager.undoing or UndoManager.redoing
*
* @type {StackItem|null}
*/
this.currStackItem = null
this.lastChange = 0 this.lastChange = 0
this.ignoreRemoteMapChanges = ignoreRemoteMapChanges this.ignoreRemoteMapChanges = ignoreRemoteMapChanges
this.captureTimeout = captureTimeout this.captureTimeout = captureTimeout
@@ -244,6 +254,9 @@ export class UndoManager extends Observable {
keepItem(item, true) keepItem(item, true)
} }
}) })
/**
* @type {[StackItemEvent, UndoManager]}
*/
const changeEvent = [{ stackItem: stack[stack.length - 1], origin: transaction.origin, type: undoing ? 'redo' : 'undo', changedParentTypes: transaction.changedParentTypes }, this] const changeEvent = [{ stackItem: stack[stack.length - 1], origin: transaction.origin, type: undoing ? 'redo' : 'undo', changedParentTypes: transaction.changedParentTypes }, this]
if (didAdd) { if (didAdd) {
this.emit('stack-item-added', changeEvent) this.emit('stack-item-added', changeEvent)

View File

@@ -34,7 +34,7 @@ export const encV1 = {
mergeUpdates: Y.mergeUpdates, mergeUpdates: Y.mergeUpdates,
applyUpdate: Y.applyUpdate, applyUpdate: Y.applyUpdate,
logUpdate: Y.logUpdate, logUpdate: Y.logUpdate,
updateEventName: 'update', updateEventName: /** @type {'update'} */ ('update'),
diffUpdate: Y.diffUpdate diffUpdate: Y.diffUpdate
} }
@@ -43,7 +43,7 @@ export const encV2 = {
mergeUpdates: Y.mergeUpdatesV2, mergeUpdates: Y.mergeUpdatesV2,
applyUpdate: Y.applyUpdateV2, applyUpdate: Y.applyUpdateV2,
logUpdate: Y.logUpdateV2, logUpdate: Y.logUpdateV2,
updateEventName: 'updateV2', updateEventName: /** @type {'updateV2'} */ ('updateV2'),
diffUpdate: Y.diffUpdateV2 diffUpdate: Y.diffUpdateV2
} }

View File

@@ -715,3 +715,33 @@ export const testUndoDeleteInMap = (tc) => {
undoManager.undo() undoManager.undo()
t.compare(map0.toJSON(), { a: 'a' }) t.compare(map0.toJSON(), { a: 'a' })
} }
/**
* It should expose the StackItem being processed if undoing
*
* @param {t.TestCase} _tc
*/
export const testUndoDoingStackItem = async (_tc) => {
const doc = new Y.Doc()
const text = doc.getText('text')
const undoManager = new Y.UndoManager([text])
undoManager.on('stack-item-added', /** @param {any} event */ event => {
event.stackItem.meta.set('str', '42')
})
let metaUndo = /** @type {any} */ (null)
let metaRedo = /** @type {any} */ (null)
text.observe((event) => {
const /** @type {Y.UndoManager} */ origin = event.transaction.origin
if (origin === undoManager && origin.undoing) {
metaUndo = origin.currStackItem?.meta.get('str')
} else if (origin === undoManager && origin.redoing) {
metaRedo = origin.currStackItem?.meta.get('str')
}
})
text.insert(0, 'abc')
undoManager.undo()
undoManager.redo()
t.compare(metaUndo, '42', 'currStackItem is accessible while undoing')
t.compare(metaRedo, '42', 'currStackItem is accessible while redoing')
t.compare(undoManager.currStackItem, null, 'currStackItem is null after observe/transaction')
}

View File

@@ -15,7 +15,7 @@ import * as object from 'lib0/object'
* @property {function(Uint8Array):{from:Map<number,number>,to:Map<number,number>}} Enc.parseUpdateMeta * @property {function(Uint8Array):{from:Map<number,number>,to:Map<number,number>}} Enc.parseUpdateMeta
* @property {function(Y.Doc):Uint8Array} Enc.encodeStateVector * @property {function(Y.Doc):Uint8Array} Enc.encodeStateVector
* @property {function(Uint8Array):Uint8Array} Enc.encodeStateVectorFromUpdate * @property {function(Uint8Array):Uint8Array} Enc.encodeStateVectorFromUpdate
* @property {string} Enc.updateEventName * @property {'update'|'updateV2'} Enc.updateEventName
* @property {string} Enc.description * @property {string} Enc.description
* @property {function(Uint8Array, Uint8Array):Uint8Array} Enc.diffUpdate * @property {function(Uint8Array, Uint8Array):Uint8Array} Enc.diffUpdate
*/ */
@@ -169,7 +169,7 @@ const checkUpdateCases = (ydoc, updates, enc, hasDeletes) => {
// t.info('Target State: ') // t.info('Target State: ')
// enc.logUpdate(targetState) // enc.logUpdate(targetState)
cases.forEach((mergedUpdates, i) => { cases.forEach((mergedUpdates) => {
// t.info('State Case $' + i + ':') // t.info('State Case $' + i + ':')
// enc.logUpdate(updates) // enc.logUpdate(updates)
const merged = new Y.Doc({ gc: false }) const merged = new Y.Doc({ gc: false })
@@ -218,10 +218,10 @@ const checkUpdateCases = (ydoc, updates, enc, hasDeletes) => {
} }
/** /**
* @param {t.TestCase} tc * @param {t.TestCase} _tc
*/ */
export const testMergeUpdates1 = tc => { export const testMergeUpdates1 = _tc => {
encoders.forEach((enc, i) => { encoders.forEach((enc) => {
t.info(`Using encoder: ${enc.description}`) t.info(`Using encoder: ${enc.description}`)
const ydoc = new Y.Doc({ gc: false }) const ydoc = new Y.Doc({ gc: false })
const updates = /** @type {Array<Uint8Array>} */ ([]) const updates = /** @type {Array<Uint8Array>} */ ([])
@@ -299,16 +299,16 @@ export const testMergePendingUpdates = tc => {
Y.applyUpdate(yDoc5, update4) Y.applyUpdate(yDoc5, update4)
Y.applyUpdate(yDoc5, serverUpdates[4]) Y.applyUpdate(yDoc5, serverUpdates[4])
// @ts-ignore // @ts-ignore
const update5 = Y.encodeStateAsUpdate(yDoc5) // eslint-disable-line const _update5 = Y.encodeStateAsUpdate(yDoc5) // eslint-disable-line
const yText5 = yDoc5.getText('textBlock') const yText5 = yDoc5.getText('textBlock')
t.compareStrings(yText5.toString(), 'nenor') t.compareStrings(yText5.toString(), 'nenor')
} }
/** /**
* @param {t.TestCase} tc * @param {t.TestCase} _tc
*/ */
export const testObfuscateUpdates = tc => { export const testObfuscateUpdates = _tc => {
const ydoc = new Y.Doc() const ydoc = new Y.Doc()
const ytext = ydoc.getText('text') const ytext = ydoc.getText('text')
const ymap = ydoc.getMap('map') const ymap = ydoc.getMap('map')

View File

@@ -189,7 +189,6 @@ export const testClone = _tc => {
const third = new Y.XmlElement('p') const third = new Y.XmlElement('p')
yxml.push([first, second, third]) yxml.push([first, second, third])
t.compareArrays(yxml.toArray(), [first, second, third]) t.compareArrays(yxml.toArray(), [first, second, third])
const cloneYxml = yxml.clone() const cloneYxml = yxml.clone()
ydoc.getArray('copyarr').insert(0, [cloneYxml]) ydoc.getArray('copyarr').insert(0, [cloneYxml])
t.assert(cloneYxml.length === 3) t.assert(cloneYxml.length === 3)
@@ -210,3 +209,15 @@ export const testFormattingBug = _tc => {
yxml.applyDelta(delta) yxml.applyDelta(delta)
t.compare(yxml.toDelta(), delta) t.compare(yxml.toDelta(), delta)
} }
/**
* @param {t.TestCase} _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')
yxmlel.insert(0, [text1, text2])
t.compareArrays(yxmlel.toArray(), [text1, text2])
}