Compare commits

...

13 Commits

Author SHA1 Message Date
Kevin Jahns
5244755879 13.5.5 2021-04-13 22:06:55 +02:00
Kevin Jahns
3a7a324a24 fix #291 2021-04-13 22:05:30 +02:00
Kevin Jahns
9e98fec504 Perform undo until change was applied 2021-04-13 13:04:49 +02:00
Kevin Jahns
c67428d715 13.5.4 2021-04-02 23:13:24 +02:00
Kevin Jahns
45a9af96af Merge pull request #289 from jhaynie/main
fix crash when the walker didn't match because n will be null
2021-04-02 23:11:48 +02:00
Jeff Haynie
249c4f9c45 switch order for type to get picked up 2021-04-01 14:59:34 -05:00
Jeff Haynie
cdc7d3ffe6 fix crash when the walker didnt match because n will be null 2021-04-01 14:54:37 -05:00
Kevin Jahns
ac6a0e7667 Merge pull request #285 from damz/patch-1
Trivial editing of README.md
2021-03-25 14:02:38 +01:00
Kevin Jahns
12881e2be7 13.5.3 2021-03-21 21:32:38 +01:00
Kevin Jahns
77958da657 unify Y.Array & Y.Text deltas so event.changes.delta is equal to event.delta 2021-03-21 21:31:28 +01:00
Kevin Jahns
8a8a60efde remove unpkg entry 2021-03-21 15:31:17 +01:00
Damien Tournoud
7a1d648e79 Trivial editing of README.md 2021-03-15 07:24:36 -07:00
Kevin Jahns
3af420e790 add package.json to exports so other packages can consume it 2021-03-15 15:08:19 +01:00
9 changed files with 155 additions and 70 deletions

View File

@@ -698,7 +698,7 @@ Y.applyUpdate(ydoc2, state1)
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
client. Syncing clients using the state vector requires another roundtrip, but
can safe a lot of bandwidth.
can save a lot of bandwidth.
```js
const stateVector1 = Y.encodeStateVector(ydoc1)

2
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "yjs",
"version": "13.5.2",
"version": "13.5.5",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@@ -1,10 +1,9 @@
{
"name": "yjs",
"version": "13.5.2",
"version": "13.5.5",
"description": "Shared Editing Library",
"main": "./dist/yjs.cjs",
"module": "./dist/yjs.mjs",
"unpkg": "./dist/yjs.mjs",
"types": "./dist/src/index.d.ts",
"sideEffects": false,
"funding": {
@@ -31,7 +30,8 @@
"require": "./dist/yjs.cjs"
},
"./src/index.js": "./src/index.js",
"./tests/testHelper.js": "./tests/testHelper.js"
"./tests/testHelper.js": "./tests/testHelper.js",
"./package.json": "./package.json"
},
"files": [
"dist/yjs.*",

View File

@@ -502,14 +502,6 @@ const deleteText = (transaction, currPos, length) => {
* @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.
*/
@@ -521,10 +513,6 @@ export class YTextEvent extends YEvent {
*/
constructor (ytext, transaction, subs) {
super(ytext, transaction)
/**
* @type {Array<DeltaItem>|null}
*/
this._delta = null
/**
* Whether the children changed.
* @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.
* 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
*/
get delta () {
if (this._delta === null) {
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 => {
const delta = /** @type {Array<DeltaItem>} */ (this._delta)
const currentAttributes = new Map() // saves all current attributes for insert
const oldAttributes = new Map()
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)
const event = new YTextEvent(this, transaction, parentSubs)
const doc = transaction.doc
callTypeObservers(this, transaction, event)
// If a remote change happened, we try to cleanup potential formatting duplicates.
if (!transaction.local) {
// check if another formatting item was inserted
@@ -853,7 +864,6 @@ export class YText extends AbstractType {
}
})
}
callTypeObservers(this, transaction, event)
}
/**

View File

@@ -83,7 +83,7 @@ export class YXmlTreeWalker {
* @type {Item|null}
*/
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
do {
type = /** @type {any} */ (n.content).type

View File

@@ -99,7 +99,7 @@ const popStackItem = (undoManager, stack, eventType) => {
performedChange = true
}
}
result = stackItem
result = performedChange ? stackItem : null
}
transaction.changed.forEach((subProps, type) => {
// destroy search marker if necessary

View File

@@ -35,6 +35,14 @@ export class YEvent {
* @type {Object|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)
}
/**
* @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.
*
@@ -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 () {
let changes = this._changes
@@ -92,12 +160,11 @@ export class YEvent {
* @type {Array<{insert:Array<any>}|{delete:number}|{retain:number}>}
*/
const delta = []
/**
* @type {Map<string,{ action: 'add' | 'update' | 'delete', oldValue: any}>}
*/
const keys = new Map()
changes = {
added, deleted, delta, keys
added,
deleted,
delta,
keys: this.keys
}
const changed = /** @type Set<string|null> */ (this.transaction.changed.get(target))
if (changed.has(null)) {
@@ -141,46 +208,6 @@ export class YEvent {
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
}
return /** @type {any} */ (changes)

View File

@@ -270,3 +270,34 @@ export const testUndoDeleteFilter = tc => {
array0.get(0)
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')
}

View File

@@ -5,6 +5,23 @@ import * as math from 'lib0/math.js'
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
*/