Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5244755879 | ||
|
|
3a7a324a24 | ||
|
|
9e98fec504 | ||
|
|
c67428d715 | ||
|
|
45a9af96af | ||
|
|
249c4f9c45 | ||
|
|
cdc7d3ffe6 | ||
|
|
ac6a0e7667 | ||
|
|
12881e2be7 | ||
|
|
77958da657 | ||
|
|
8a8a60efde | ||
|
|
7a1d648e79 | ||
|
|
3af420e790 | ||
|
|
4f2d13e3ce | ||
|
|
e0b76cd2f4 | ||
|
|
d812636c5b | ||
|
|
21fee0fe96 |
15
README.md
15
README.md
@@ -320,6 +320,8 @@ or any of its children.
|
|||||||
<dl>
|
<dl>
|
||||||
<b><code>parent:Y.AbstractType|null</code></b>
|
<b><code>parent:Y.AbstractType|null</code></b>
|
||||||
<dd></dd>
|
<dd></dd>
|
||||||
|
<b><code>size: number</code></b>
|
||||||
|
<dd>Total number of key/value pairs.</dd>
|
||||||
<b><code>get(key:string):object|boolean|string|number|Uint8Array|Y.Type</code></b>
|
<b><code>get(key:string):object|boolean|string|number|Uint8Array|Y.Type</code></b>
|
||||||
<dd></dd>
|
<dd></dd>
|
||||||
<b><code>set(key:string, value:object|boolean|string|number|Uint8Array|Y.Type)</code></b>
|
<b><code>set(key:string, value:object|boolean|string|number|Uint8Array|Y.Type)</code></b>
|
||||||
@@ -410,7 +412,7 @@ YTextEvents compute changes as deltas.
|
|||||||
<dd></dd>
|
<dd></dd>
|
||||||
<b><code>format(index:number, length:number, formattingAttributes:Object<string,string>)</code></b>
|
<b><code>format(index:number, length:number, formattingAttributes:Object<string,string>)</code></b>
|
||||||
<dd>Assign formatting attributes to a range in the text</dd>
|
<dd>Assign formatting attributes to a range in the text</dd>
|
||||||
<b><code>applyDelta(delta, opts:Object<string,any>)</code></b>
|
<b><code>applyDelta(delta: Delta, opts:Object<string,any>)</code></b>
|
||||||
<dd>
|
<dd>
|
||||||
See <a href="https://quilljs.com/docs/delta/">Quill Delta</a>
|
See <a href="https://quilljs.com/docs/delta/">Quill Delta</a>
|
||||||
Can set options for preventing remove ending newLines, default is true.
|
Can set options for preventing remove ending newLines, default is true.
|
||||||
@@ -483,6 +485,8 @@ or any of its children.
|
|||||||
<dd>Get the XML serialization of all descendants.</dd>
|
<dd>Get the XML serialization of all descendants.</dd>
|
||||||
<b><code>toJSON():string</code></b>
|
<b><code>toJSON():string</code></b>
|
||||||
<dd>See <code>toString</code>.</dd>
|
<dd>See <code>toString</code>.</dd>
|
||||||
|
<b><code>createTreeWalker(filter: function(AbstractType<any>):boolean):Iterable</code></b>
|
||||||
|
<dd>Create an Iterable that walks through the children.</dd>
|
||||||
<b><code>observe(function(YXmlEvent, Transaction):void)</code></b>
|
<b><code>observe(function(YXmlEvent, Transaction):void)</code></b>
|
||||||
<dd>
|
<dd>
|
||||||
Adds an event listener to this type that will be called synchronously every time
|
Adds an event listener to this type that will be called synchronously every time
|
||||||
@@ -540,7 +544,7 @@ content and be actually XML compliant.
|
|||||||
<dd></dd>
|
<dd></dd>
|
||||||
<b><code>getAttribute(attributeName:string):string</code></b>
|
<b><code>getAttribute(attributeName:string):string</code></b>
|
||||||
<dd></dd>
|
<dd></dd>
|
||||||
<b><code>getAttributes(attributeName:string):Object<string,string></code></b>
|
<b><code>getAttributes():Object<string,string></code></b>
|
||||||
<dd></dd>
|
<dd></dd>
|
||||||
<b><code>get(i:number):Y.XmlElement|Y.XmlText</code></b>
|
<b><code>get(i:number):Y.XmlElement|Y.XmlText</code></b>
|
||||||
<dd>Retrieve the i-th element.</dd>
|
<dd>Retrieve the i-th element.</dd>
|
||||||
@@ -609,7 +613,10 @@ parameter that is stored on <code>transaction.origin</code> and
|
|||||||
</dd>
|
</dd>
|
||||||
<b><code>toJSON():any</code></b>
|
<b><code>toJSON():any</code></b>
|
||||||
<dd>
|
<dd>
|
||||||
Converts the entire document into a js object, recursively traversing each yjs type.
|
Deprecated: It is recommended to call toJSON directly on the shared types.
|
||||||
|
Converts the entire document into a js object, recursively traversing each yjs
|
||||||
|
type. Doesn't log types that have not been defined (using
|
||||||
|
<code>ydoc.getType(..)</code>).
|
||||||
</dd>
|
</dd>
|
||||||
<b><code>get(string, Y.[TypeClass]):[Type]</code></b>
|
<b><code>get(string, Y.[TypeClass]):[Type]</code></b>
|
||||||
<dd>Define a shared type.</dd>
|
<dd>Define a shared type.</dd>
|
||||||
@@ -691,7 +698,7 @@ Y.applyUpdate(ydoc2, state1)
|
|||||||
This example shows how to sync two clients with the minimal amount of exchanged
|
This example shows how to sync two clients with the minimal amount of exchanged
|
||||||
data by computing only the differences using the state vector of the remote
|
data by computing only the differences using the state vector of the remote
|
||||||
client. Syncing clients using the state vector requires another roundtrip, but
|
client. Syncing clients using the state vector requires another roundtrip, but
|
||||||
can safe a lot of bandwidth.
|
can save a lot of bandwidth.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const stateVector1 = Y.encodeStateVector(ydoc1)
|
const stateVector1 = Y.encodeStateVector(ydoc1)
|
||||||
|
|||||||
2
package-lock.json
generated
2
package-lock.json
generated
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "yjs",
|
"name": "yjs",
|
||||||
"version": "13.5.1",
|
"version": "13.5.5",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
12
package.json
12
package.json
@@ -1,10 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "yjs",
|
"name": "yjs",
|
||||||
"version": "13.5.1",
|
"version": "13.5.5",
|
||||||
"description": "Shared Editing Library",
|
"description": "Shared Editing Library",
|
||||||
"main": "./dist/yjs.cjs",
|
"main": "./dist/yjs.cjs",
|
||||||
"module": "./dist/yjs.mjs",
|
"module": "./dist/yjs.mjs",
|
||||||
"unpkg": "./dist/yjs.mjs",
|
|
||||||
"types": "./dist/src/index.d.ts",
|
"types": "./dist/src/index.d.ts",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"funding": {
|
"funding": {
|
||||||
@@ -12,14 +11,14 @@
|
|||||||
"url": "https://github.com/sponsors/dmonad"
|
"url": "https://github.com/sponsors/dmonad"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "npm run dist && node ./dist/tests.cjs --repitition-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 --repitition-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": "rm -rf dist && 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",
|
||||||
"serve-docs": "npm run docs && http-server ./docs/",
|
"serve-docs": "npm run docs && http-server ./docs/",
|
||||||
"preversion": "npm run lint && PRODUCTION=1 npm run dist && npm run docs && node ./dist/tests.cjs --repitition-time 1000 && test -e dist/src/index.d.ts && test -e dist/yjs.cjs && test -e dist/yjs.cjs",
|
"preversion": "npm run lint && PRODUCTION=1 npm run dist && npm run docs && node ./dist/tests.cjs --repetition-time 1000 && test -e dist/src/index.d.ts && test -e dist/yjs.cjs && test -e dist/yjs.cjs",
|
||||||
"debug": "concurrently 'http-server -o test.html' 'npm run watch'",
|
"debug": "concurrently 'http-server -o test.html' 'npm run watch'",
|
||||||
"trace-deopt": "clear && rollup -c && node --trace-deopt dist/test.cjs",
|
"trace-deopt": "clear && rollup -c && node --trace-deopt dist/test.cjs",
|
||||||
"trace-opt": "clear && rollup -c && node --trace-opt dist/test.cjs",
|
"trace-opt": "clear && rollup -c && node --trace-opt dist/test.cjs",
|
||||||
@@ -31,7 +30,8 @@
|
|||||||
"require": "./dist/yjs.cjs"
|
"require": "./dist/yjs.cjs"
|
||||||
},
|
},
|
||||||
"./src/index.js": "./src/index.js",
|
"./src/index.js": "./src/index.js",
|
||||||
"./tests/testHelper.js": "./tests/testHelper.js"
|
"./tests/testHelper.js": "./tests/testHelper.js",
|
||||||
|
"./package.json": "./package.json"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist/yjs.*",
|
"dist/yjs.*",
|
||||||
|
|||||||
@@ -502,14 +502,6 @@ const deleteText = (transaction, currPos, length) => {
|
|||||||
* @typedef {Object} TextAttributes
|
* @typedef {Object} TextAttributes
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {Object} DeltaItem
|
|
||||||
* @property {number|undefined} DeltaItem.delete
|
|
||||||
* @property {number|undefined} DeltaItem.retain
|
|
||||||
* @property {string|undefined} DeltaItem.insert
|
|
||||||
* @property {Object<string,any>} DeltaItem.attributes
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Event that describes the changes on a YText type.
|
* Event that describes the changes on a YText type.
|
||||||
*/
|
*/
|
||||||
@@ -521,10 +513,6 @@ export class YTextEvent extends YEvent {
|
|||||||
*/
|
*/
|
||||||
constructor (ytext, transaction, subs) {
|
constructor (ytext, transaction, subs) {
|
||||||
super(ytext, transaction)
|
super(ytext, transaction)
|
||||||
/**
|
|
||||||
* @type {Array<DeltaItem>|null}
|
|
||||||
*/
|
|
||||||
this._delta = null
|
|
||||||
/**
|
/**
|
||||||
* Whether the children changed.
|
* Whether the children changed.
|
||||||
* @type {Boolean}
|
* @type {Boolean}
|
||||||
@@ -545,20 +533,41 @@ export class YTextEvent extends YEvent {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {{added:Set<Item>,deleted:Set<Item>,keys:Map<string,{action:'add'|'update'|'delete',oldValue:any}>,delta:Array<{insert?:Array<any>|string, delete?:number, retain?:number}>}}
|
||||||
|
*/
|
||||||
|
get changes () {
|
||||||
|
if (this._changes === null) {
|
||||||
|
/**
|
||||||
|
* @type {{added:Set<Item>,deleted:Set<Item>,keys:Map<string,{action:'add'|'update'|'delete',oldValue:any}>,delta:Array<{insert?:Array<any>|string, delete?:number, retain?:number}>}}
|
||||||
|
*/
|
||||||
|
const changes = {
|
||||||
|
keys: this.keys,
|
||||||
|
delta: this.delta,
|
||||||
|
added: new Set(),
|
||||||
|
deleted: new Set()
|
||||||
|
}
|
||||||
|
this._changes = changes
|
||||||
|
}
|
||||||
|
return /** @type {any} */ (this._changes)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compute the changes in the delta format.
|
* Compute the changes in the delta format.
|
||||||
* A {@link https://quilljs.com/docs/delta/|Quill Delta}) that represents the changes on the document.
|
* A {@link https://quilljs.com/docs/delta/|Quill Delta}) that represents the changes on the document.
|
||||||
*
|
*
|
||||||
* @type {Array<DeltaItem>}
|
* @type {Array<{insert?:string, delete?:number, retain?:number, attributes?: Object<string,any>}>}
|
||||||
*
|
*
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
get delta () {
|
get delta () {
|
||||||
if (this._delta === null) {
|
if (this._delta === null) {
|
||||||
const y = /** @type {Doc} */ (this.target.doc)
|
const y = /** @type {Doc} */ (this.target.doc)
|
||||||
this._delta = []
|
/**
|
||||||
|
* @type {Array<{insert?:string, delete?:number, retain?:number, attributes?: Object<string,any>}>}
|
||||||
|
*/
|
||||||
|
const delta = []
|
||||||
transact(y, transaction => {
|
transact(y, transaction => {
|
||||||
const delta = /** @type {Array<DeltaItem>} */ (this._delta)
|
|
||||||
const currentAttributes = new Map() // saves all current attributes for insert
|
const currentAttributes = new Map() // saves all current attributes for insert
|
||||||
const oldAttributes = new Map()
|
const oldAttributes = new Map()
|
||||||
let item = this.target._start
|
let item = this.target._start
|
||||||
@@ -728,8 +737,9 @@ export class YTextEvent extends YEvent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
this._delta = delta
|
||||||
}
|
}
|
||||||
return this._delta
|
return /** @type {any} */ (this._delta)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -805,6 +815,7 @@ export class YText extends AbstractType {
|
|||||||
super._callObserver(transaction, parentSubs)
|
super._callObserver(transaction, parentSubs)
|
||||||
const event = new YTextEvent(this, transaction, parentSubs)
|
const event = new YTextEvent(this, transaction, parentSubs)
|
||||||
const doc = transaction.doc
|
const doc = transaction.doc
|
||||||
|
callTypeObservers(this, transaction, event)
|
||||||
// If a remote change happened, we try to cleanup potential formatting duplicates.
|
// If a remote change happened, we try to cleanup potential formatting duplicates.
|
||||||
if (!transaction.local) {
|
if (!transaction.local) {
|
||||||
// check if another formatting item was inserted
|
// check if another formatting item was inserted
|
||||||
@@ -853,7 +864,6 @@ export class YText extends AbstractType {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
callTypeObservers(this, transaction, event)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ export class YXmlTreeWalker {
|
|||||||
* @type {Item|null}
|
* @type {Item|null}
|
||||||
*/
|
*/
|
||||||
let n = this._currentNode
|
let n = this._currentNode
|
||||||
let type = /** @type {any} */ (n.content).type
|
let type = n && n.content && /** @type {any} */ (n.content).type
|
||||||
if (n !== null && (!this._firstCall || n.deleted || !this._filter(type))) { // if first call, we check if we can use the first item
|
if (n !== null && (!this._firstCall || n.deleted || !this._filter(type))) { // if first call, we check if we can use the first item
|
||||||
do {
|
do {
|
||||||
type = /** @type {any} */ (n.content).type
|
type = /** @type {any} */ (n.content).type
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import {
|
|||||||
transact,
|
transact,
|
||||||
createID,
|
createID,
|
||||||
redoItem,
|
redoItem,
|
||||||
iterateStructs,
|
|
||||||
isParentOf,
|
isParentOf,
|
||||||
followRedone,
|
followRedone,
|
||||||
getItemCleanStart,
|
getItemCleanStart,
|
||||||
getState,
|
isDeleted,
|
||||||
|
addToDeleteSet,
|
||||||
Transaction, Doc, Item, GC, DeleteSet, AbstractType, YEvent // eslint-disable-line
|
Transaction, Doc, Item, GC, DeleteSet, AbstractType, YEvent // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
@@ -18,14 +18,12 @@ import { Observable } from 'lib0/observable.js'
|
|||||||
|
|
||||||
class StackItem {
|
class StackItem {
|
||||||
/**
|
/**
|
||||||
* @param {DeleteSet} ds
|
* @param {DeleteSet} deletions
|
||||||
* @param {Map<number,number>} beforeState
|
* @param {DeleteSet} insertions
|
||||||
* @param {Map<number,number>} afterState
|
|
||||||
*/
|
*/
|
||||||
constructor (ds, beforeState, afterState) {
|
constructor (deletions, insertions) {
|
||||||
this.ds = ds
|
this.insertions = insertions
|
||||||
this.beforeState = beforeState
|
this.deletions = deletions
|
||||||
this.afterState = afterState
|
|
||||||
/**
|
/**
|
||||||
* Use this to save and restore metadata like selection range
|
* Use this to save and restore metadata like selection range
|
||||||
*/
|
*/
|
||||||
@@ -65,48 +63,26 @@ const popStackItem = (undoManager, stack, eventType) => {
|
|||||||
*/
|
*/
|
||||||
const itemsToDelete = []
|
const itemsToDelete = []
|
||||||
let performedChange = false
|
let performedChange = false
|
||||||
stackItem.afterState.forEach((endClock, client) => {
|
iterateDeletedStructs(transaction, stackItem.insertions, struct => {
|
||||||
const startClock = stackItem.beforeState.get(client) || 0
|
if (struct instanceof Item) {
|
||||||
const len = endClock - startClock
|
if (struct.redone !== null) {
|
||||||
// @todo iterateStructs should not need the structs parameter
|
let { item, diff } = followRedone(store, struct.id)
|
||||||
const structs = /** @type {Array<GC|Item>} */ (store.clients.get(client))
|
if (diff > 0) {
|
||||||
if (startClock !== endClock) {
|
item = getItemCleanStart(transaction, createID(item.id.client, item.id.clock + diff))
|
||||||
// make sure structs don't overlap with the range of created operations [stackItem.start, stackItem.start + stackItem.end)
|
|
||||||
// this must be executed before deleted structs are iterated.
|
|
||||||
getItemCleanStart(transaction, createID(client, startClock))
|
|
||||||
if (endClock < getState(doc.store, client)) {
|
|
||||||
getItemCleanStart(transaction, createID(client, endClock))
|
|
||||||
}
|
|
||||||
iterateStructs(transaction, structs, startClock, len, struct => {
|
|
||||||
if (struct instanceof Item) {
|
|
||||||
if (struct.redone !== null) {
|
|
||||||
let { item, diff } = followRedone(store, struct.id)
|
|
||||||
if (diff > 0) {
|
|
||||||
item = getItemCleanStart(transaction, createID(item.id.client, item.id.clock + diff))
|
|
||||||
}
|
|
||||||
if (item.length > len) {
|
|
||||||
getItemCleanStart(transaction, createID(item.id.client, endClock))
|
|
||||||
}
|
|
||||||
struct = item
|
|
||||||
}
|
|
||||||
if (!struct.deleted && scope.some(type => isParentOf(type, /** @type {Item} */ (struct)))) {
|
|
||||||
itemsToDelete.push(struct)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
struct = item
|
||||||
|
}
|
||||||
|
if (!struct.deleted && scope.some(type => isParentOf(type, /** @type {Item} */ (struct)))) {
|
||||||
|
itemsToDelete.push(struct)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
iterateDeletedStructs(transaction, stackItem.ds, struct => {
|
iterateDeletedStructs(transaction, stackItem.deletions, struct => {
|
||||||
const id = struct.id
|
|
||||||
const clock = id.clock
|
|
||||||
const client = id.client
|
|
||||||
const startClock = stackItem.beforeState.get(client) || 0
|
|
||||||
const endClock = stackItem.afterState.get(client) || 0
|
|
||||||
if (
|
if (
|
||||||
struct instanceof Item &&
|
struct instanceof Item &&
|
||||||
scope.some(type => isParentOf(type, struct)) &&
|
scope.some(type => isParentOf(type, struct)) &&
|
||||||
// Never redo structs in [stackItem.start, stackItem.start + stackItem.end) because they were created and deleted in the same capture interval.
|
// Never redo structs in stackItem.insertions because they were created and deleted in the same capture interval.
|
||||||
!(clock >= startClock && clock < endClock)
|
!isDeleted(stackItem.insertions, struct.id)
|
||||||
) {
|
) {
|
||||||
itemsToRedo.add(struct)
|
itemsToRedo.add(struct)
|
||||||
}
|
}
|
||||||
@@ -123,7 +99,7 @@ const popStackItem = (undoManager, stack, eventType) => {
|
|||||||
performedChange = true
|
performedChange = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result = stackItem
|
result = performedChange ? stackItem : null
|
||||||
}
|
}
|
||||||
transaction.changed.forEach((subProps, type) => {
|
transaction.changed.forEach((subProps, type) => {
|
||||||
// destroy search marker if necessary
|
// destroy search marker if necessary
|
||||||
@@ -201,17 +177,23 @@ export class UndoManager extends Observable {
|
|||||||
// neither undoing nor redoing: delete redoStack
|
// neither undoing nor redoing: delete redoStack
|
||||||
this.redoStack = []
|
this.redoStack = []
|
||||||
}
|
}
|
||||||
const beforeState = transaction.beforeState
|
const insertions = new DeleteSet()
|
||||||
const afterState = transaction.afterState
|
transaction.afterState.forEach((endClock, client) => {
|
||||||
|
const startClock = transaction.beforeState.get(client) || 0
|
||||||
|
const len = endClock - startClock
|
||||||
|
if (len > 0) {
|
||||||
|
addToDeleteSet(insertions, client, startClock, len)
|
||||||
|
}
|
||||||
|
})
|
||||||
const now = time.getUnixTime()
|
const now = time.getUnixTime()
|
||||||
if (now - this.lastChange < captureTimeout && stack.length > 0 && !undoing && !redoing) {
|
if (now - this.lastChange < captureTimeout && stack.length > 0 && !undoing && !redoing) {
|
||||||
// append change to last stack op
|
// append change to last stack op
|
||||||
const lastOp = stack[stack.length - 1]
|
const lastOp = stack[stack.length - 1]
|
||||||
lastOp.ds = mergeDeleteSets([lastOp.ds, transaction.deleteSet])
|
lastOp.deletions = mergeDeleteSets([lastOp.deletions, transaction.deleteSet])
|
||||||
lastOp.afterState = afterState
|
lastOp.insertions = mergeDeleteSets([lastOp.insertions, insertions])
|
||||||
} else {
|
} else {
|
||||||
// create a new stack op
|
// create a new stack op
|
||||||
stack.push(new StackItem(transaction.deleteSet, beforeState, afterState))
|
stack.push(new StackItem(transaction.deleteSet, insertions))
|
||||||
}
|
}
|
||||||
if (!undoing && !redoing) {
|
if (!undoing && !redoing) {
|
||||||
this.lastChange = now
|
this.lastChange = now
|
||||||
@@ -232,7 +214,7 @@ export class UndoManager extends Observable {
|
|||||||
* @param {StackItem} stackItem
|
* @param {StackItem} stackItem
|
||||||
*/
|
*/
|
||||||
const clearItem = stackItem => {
|
const clearItem = stackItem => {
|
||||||
iterateDeletedStructs(transaction, stackItem.ds, item => {
|
iterateDeletedStructs(transaction, stackItem.deletions, item => {
|
||||||
if (item instanceof Item && this.scope.some(type => isParentOf(type, item))) {
|
if (item instanceof Item && this.scope.some(type => isParentOf(type, item))) {
|
||||||
keepItem(item, false)
|
keepItem(item, false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,14 @@ export class YEvent {
|
|||||||
* @type {Object|null}
|
* @type {Object|null}
|
||||||
*/
|
*/
|
||||||
this._changes = null
|
this._changes = null
|
||||||
|
/**
|
||||||
|
* @type {null | Map<string, { action: 'add' | 'update' | 'delete', oldValue: any, newValue: any }>}
|
||||||
|
*/
|
||||||
|
this._keys = null
|
||||||
|
/**
|
||||||
|
* @type {null | Array<{ insert?: string | Array<any>, retain?: number, delete?: number, attributes?: Object<string, any> }>}
|
||||||
|
*/
|
||||||
|
this._delta = null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -67,6 +75,66 @@ export class YEvent {
|
|||||||
return isDeleted(this.transaction.deleteSet, struct.id)
|
return isDeleted(this.transaction.deleteSet, struct.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Map<string, { action: 'add' | 'update' | 'delete', oldValue: any, newValue: any }>}
|
||||||
|
*/
|
||||||
|
get keys () {
|
||||||
|
if (this._keys === null) {
|
||||||
|
const keys = new Map()
|
||||||
|
const target = this.target
|
||||||
|
const changed = /** @type Set<string|null> */ (this.transaction.changed.get(target))
|
||||||
|
changed.forEach(key => {
|
||||||
|
if (key !== null) {
|
||||||
|
const item = /** @type {Item} */ (target._map.get(key))
|
||||||
|
/**
|
||||||
|
* @type {'delete' | 'add' | 'update'}
|
||||||
|
*/
|
||||||
|
let action
|
||||||
|
let oldValue
|
||||||
|
if (this.adds(item)) {
|
||||||
|
let prev = item.left
|
||||||
|
while (prev !== null && this.adds(prev)) {
|
||||||
|
prev = prev.left
|
||||||
|
}
|
||||||
|
if (this.deletes(item)) {
|
||||||
|
if (prev !== null && this.deletes(prev)) {
|
||||||
|
action = 'delete'
|
||||||
|
oldValue = array.last(prev.content.getContent())
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (prev !== null && this.deletes(prev)) {
|
||||||
|
action = 'update'
|
||||||
|
oldValue = array.last(prev.content.getContent())
|
||||||
|
} else {
|
||||||
|
action = 'add'
|
||||||
|
oldValue = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (this.deletes(item)) {
|
||||||
|
action = 'delete'
|
||||||
|
oldValue = array.last(/** @type {Item} */ item.content.getContent())
|
||||||
|
} else {
|
||||||
|
return // nop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
keys.set(key, { action, oldValue })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this._keys = keys
|
||||||
|
}
|
||||||
|
return this._keys
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Array<{insert?: string | Array<any>, retain?: number, delete?: number, attributes?: Object<string, any>}>}
|
||||||
|
*/
|
||||||
|
get delta () {
|
||||||
|
return this.changes.delta
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a struct is added by this event.
|
* Check if a struct is added by this event.
|
||||||
*
|
*
|
||||||
@@ -80,7 +148,7 @@ export class YEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return {{added:Set<Item>,deleted:Set<Item>,keys:Map<string,{action:'add'|'update'|'delete',oldValue:any}>,delta:Array<{insert:Array<any>}|{delete:number}|{retain:number}>}}
|
* @type {{added:Set<Item>,deleted:Set<Item>,keys:Map<string,{action:'add'|'update'|'delete',oldValue:any}>,delta:Array<{insert?:Array<any>|string, delete?:number, retain?:number}>}}
|
||||||
*/
|
*/
|
||||||
get changes () {
|
get changes () {
|
||||||
let changes = this._changes
|
let changes = this._changes
|
||||||
@@ -92,12 +160,11 @@ export class YEvent {
|
|||||||
* @type {Array<{insert:Array<any>}|{delete:number}|{retain:number}>}
|
* @type {Array<{insert:Array<any>}|{delete:number}|{retain:number}>}
|
||||||
*/
|
*/
|
||||||
const delta = []
|
const delta = []
|
||||||
/**
|
|
||||||
* @type {Map<string,{ action: 'add' | 'update' | 'delete', oldValue: any}>}
|
|
||||||
*/
|
|
||||||
const keys = new Map()
|
|
||||||
changes = {
|
changes = {
|
||||||
added, deleted, delta, keys
|
added,
|
||||||
|
deleted,
|
||||||
|
delta,
|
||||||
|
keys: this.keys
|
||||||
}
|
}
|
||||||
const changed = /** @type Set<string|null> */ (this.transaction.changed.get(target))
|
const changed = /** @type Set<string|null> */ (this.transaction.changed.get(target))
|
||||||
if (changed.has(null)) {
|
if (changed.has(null)) {
|
||||||
@@ -141,46 +208,6 @@ export class YEvent {
|
|||||||
packOp()
|
packOp()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
changed.forEach(key => {
|
|
||||||
if (key !== null) {
|
|
||||||
const item = /** @type {Item} */ (target._map.get(key))
|
|
||||||
/**
|
|
||||||
* @type {'delete' | 'add' | 'update'}
|
|
||||||
*/
|
|
||||||
let action
|
|
||||||
let oldValue
|
|
||||||
if (this.adds(item)) {
|
|
||||||
let prev = item.left
|
|
||||||
while (prev !== null && this.adds(prev)) {
|
|
||||||
prev = prev.left
|
|
||||||
}
|
|
||||||
if (this.deletes(item)) {
|
|
||||||
if (prev !== null && this.deletes(prev)) {
|
|
||||||
action = 'delete'
|
|
||||||
oldValue = array.last(prev.content.getContent())
|
|
||||||
} else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (prev !== null && this.deletes(prev)) {
|
|
||||||
action = 'update'
|
|
||||||
oldValue = array.last(prev.content.getContent())
|
|
||||||
} else {
|
|
||||||
action = 'add'
|
|
||||||
oldValue = undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (this.deletes(item)) {
|
|
||||||
action = 'delete'
|
|
||||||
oldValue = array.last(/** @type {Item} */ item.content.getContent())
|
|
||||||
} else {
|
|
||||||
return // nop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
keys.set(key, { action, oldValue })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
this._changes = changes
|
this._changes = changes
|
||||||
}
|
}
|
||||||
return /** @type {any} */ (changes)
|
return /** @type {any} */ (changes)
|
||||||
|
|||||||
@@ -270,3 +270,34 @@ export const testUndoDeleteFilter = tc => {
|
|||||||
array0.get(0)
|
array0.get(0)
|
||||||
t.assert(Array.from(array0.get(0).keys()).length === 1)
|
t.assert(Array.from(array0.get(0).keys()).length === 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This issue has been reported in https://discuss.yjs.dev/t/undomanager-with-external-updates/454/6
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testUndoUntilChangePerformed = tc => {
|
||||||
|
const doc = new Y.Doc()
|
||||||
|
const doc2 = new Y.Doc()
|
||||||
|
doc.on('update', update => Y.applyUpdate(doc2, update))
|
||||||
|
doc2.on('update', update => Y.applyUpdate(doc, update))
|
||||||
|
|
||||||
|
const yArray = doc.getArray('array')
|
||||||
|
const yArray2 = doc2.getArray('array')
|
||||||
|
const yMap = new Y.Map()
|
||||||
|
yMap.set('hello', 'world')
|
||||||
|
yArray.push([yMap])
|
||||||
|
const yMap2 = new Y.Map()
|
||||||
|
yMap2.set('key', 'value')
|
||||||
|
yArray.push([yMap2])
|
||||||
|
|
||||||
|
const undoManager = new Y.UndoManager([yArray], { trackedOrigins: new Set([doc.clientID]) })
|
||||||
|
const undoManager2 = new Y.UndoManager([doc2.get('array')], { trackedOrigins: new Set([doc2.clientID]) })
|
||||||
|
|
||||||
|
Y.transact(doc, () => yMap2.set('key', 'value modified'), doc.clientID)
|
||||||
|
undoManager.stopCapturing()
|
||||||
|
Y.transact(doc, () => yMap.set('hello', 'world modified'), doc.clientID)
|
||||||
|
Y.transact(doc2, () => yArray2.delete(0), doc2.clientID)
|
||||||
|
undoManager2.undo()
|
||||||
|
undoManager.undo()
|
||||||
|
t.compareStrings(yMap2.get('key'), 'value')
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,23 @@ import * as math from 'lib0/math.js'
|
|||||||
|
|
||||||
const { init, compare } = Y
|
const { init, compare } = Y
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testDeltaAfterConcurrentFormatting = tc => {
|
||||||
|
const { text0, text1, testConnector } = init(tc, { users: 2 })
|
||||||
|
text0.insert(0, 'abcde')
|
||||||
|
testConnector.flushAllMessages()
|
||||||
|
text0.format(0, 3, { bold: true })
|
||||||
|
text1.format(2, 2, { bold: true })
|
||||||
|
let delta = null
|
||||||
|
text1.observe(event => {
|
||||||
|
delta = event.delta
|
||||||
|
})
|
||||||
|
testConnector.flushAllMessages()
|
||||||
|
t.compare(delta, [])
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user