Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be1449a7af | ||
|
|
a22b3cdbc1 | ||
|
|
e9a0dc4ed2 | ||
|
|
b0b276d964 | ||
|
|
d3e117702c | ||
|
|
ff5067e149 | ||
|
|
f80e39a477 | ||
|
|
f70198333a | ||
|
|
3c31b22a92 | ||
|
|
6b8cef29e2 | ||
|
|
4a06492fb1 | ||
|
|
46fbce0de8 | ||
|
|
239703fe5c | ||
|
|
5e907e3281 | ||
|
|
6aea35246b | ||
|
|
5058189a46 | ||
|
|
4db3439bb1 | ||
|
|
aa5463b06d |
20
README.md
20
README.md
@@ -49,6 +49,7 @@ Support the project and receive more attention on your tickets.
|
||||
collaboratively organize radio broadcasts. :star:
|
||||
* [Alldone](https://alldone.app/) A next-gen project management and
|
||||
collaboration platform.
|
||||
* [Living Spec](https://livingspec.com/) A modern way for product teams to collaborate.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
@@ -81,6 +82,7 @@ are implemented in separate modules.
|
||||
| [Monaco](https://microsoft.github.io/monaco-editor/) | ✔ | [y-monaco](https://github.com/yjs/y-monaco) | [demo](https://demos.yjs.dev/monaco/monaco.html) |
|
||||
| [Slate](https://github.com/ianstormtaylor/slate) | ✔ | [slate-yjs](https://github.com/bitphinix/slate-yjs) | [demo](https://bitphinix.github.io/slate-yjs-example) |
|
||||
| [valtio](https://github.com/pmndrs/valtio) | | [valtio-yjs](https://github.com/dai-shi/valtio-yjs) | [demo](https://codesandbox.io/s/valtio-yjs-demo-ox3iy) |
|
||||
| [immer](https://github.com/immerjs/immer) | | [immer-yjs](https://github.com/sep2/immer-yjs) | [demo](https://codesandbox.io/s/immer-yjs-demo-6e0znb) |
|
||||
| React / Vue / Svelte / MobX | | [SyncedStore](https://syncedstore.org) | [demo](https://syncedstore.org/docs/react) |
|
||||
|
||||
### Providers
|
||||
@@ -871,6 +873,16 @@ undo- or the redo-stack.
|
||||
</dd>
|
||||
<b>
|
||||
<code>
|
||||
on('stack-item-updated', { stackItem: { meta: Map<any,any> }, type: 'undo'
|
||||
| 'redo' })
|
||||
</code>
|
||||
</b>
|
||||
<dd>
|
||||
Register an event that is called when an existing <code>StackItem</code> is updated.
|
||||
This happens when two changes happen within a "captureInterval".
|
||||
</dd>
|
||||
<b>
|
||||
<code>
|
||||
on('stack-item-popped', { stackItem: { meta: Map<any,any> }, type: 'undo'
|
||||
| 'redo' })
|
||||
</code>
|
||||
@@ -879,6 +891,14 @@ on('stack-item-popped', { stackItem: { meta: Map<any,any> }, type: 'undo'
|
||||
Register an event that is called when a <code>StackItem</code> is popped from
|
||||
the undo- or the redo-stack.
|
||||
</dd>
|
||||
<b>
|
||||
<code>
|
||||
on('stack-cleared', { undoStackCleared: boolean, redoStackCleared: boolean })
|
||||
</code>
|
||||
</b>
|
||||
<dd>
|
||||
Register an event that is called when the undo- and/or the redo-stack is cleared.
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
#### Example: Stop Capturing
|
||||
|
||||
18
package-lock.json
generated
18
package-lock.json
generated
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"name": "yjs",
|
||||
"version": "13.5.28",
|
||||
"version": "13.5.32",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "yjs",
|
||||
"version": "13.5.28",
|
||||
"version": "13.5.32",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lib0": "^0.2.43"
|
||||
"lib0": "^0.2.47"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^17.0.0",
|
||||
@@ -2210,9 +2210,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lib0": {
|
||||
"version": "0.2.46",
|
||||
"resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.46.tgz",
|
||||
"integrity": "sha512-U8V4Nc91EUSXLewvlJb0uSm9JI5rbiZKXBKHqWwPHX4g5cHx09dWAeD//UEZplm96xCrZ1BKDh5QaFHUIsnGIg==",
|
||||
"version": "0.2.47",
|
||||
"resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.47.tgz",
|
||||
"integrity": "sha512-RXprIyaflw7OmFNMpb8HmvDhuRVUFXYCXrmynQN8OGbGevgMx9u6tjQG/yB0dOoDcuB1XXgqFn8Oy3RlKF/Qhg==",
|
||||
"dependencies": {
|
||||
"isomorphic.js": "^0.2.4"
|
||||
},
|
||||
@@ -5591,9 +5591,9 @@
|
||||
}
|
||||
},
|
||||
"lib0": {
|
||||
"version": "0.2.46",
|
||||
"resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.46.tgz",
|
||||
"integrity": "sha512-U8V4Nc91EUSXLewvlJb0uSm9JI5rbiZKXBKHqWwPHX4g5cHx09dWAeD//UEZplm96xCrZ1BKDh5QaFHUIsnGIg==",
|
||||
"version": "0.2.47",
|
||||
"resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.47.tgz",
|
||||
"integrity": "sha512-RXprIyaflw7OmFNMpb8HmvDhuRVUFXYCXrmynQN8OGbGevgMx9u6tjQG/yB0dOoDcuB1XXgqFn8Oy3RlKF/Qhg==",
|
||||
"requires": {
|
||||
"isomorphic.js": "^0.2.4"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "yjs",
|
||||
"version": "13.5.28",
|
||||
"version": "13.5.32",
|
||||
"description": "Shared Editing Library",
|
||||
"main": "./dist/yjs.cjs",
|
||||
"module": "./dist/yjs.mjs",
|
||||
@@ -73,7 +73,7 @@
|
||||
},
|
||||
"homepage": "https://docs.yjs.dev",
|
||||
"dependencies": {
|
||||
"lib0": "^0.2.43"
|
||||
"lib0": "^0.2.47"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^17.0.0",
|
||||
|
||||
@@ -100,4 +100,4 @@ export class ContentFormat {
|
||||
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
|
||||
* @return {ContentFormat}
|
||||
*/
|
||||
export const readContentFormat = decoder => new ContentFormat(decoder.readString(), decoder.readJSON())
|
||||
export const readContentFormat = decoder => new ContentFormat(decoder.readKey(), decoder.readJSON())
|
||||
|
||||
@@ -127,12 +127,13 @@ export const splitItem = (transaction, leftItem, diff) => {
|
||||
* @param {Item} item
|
||||
* @param {Set<Item>} redoitems
|
||||
* @param {DeleteSet} itemsToDelete
|
||||
* @param {boolean} ignoreRemoteMapChanges
|
||||
*
|
||||
* @return {Item|null}
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
export const redoItem = (transaction, item, redoitems, itemsToDelete) => {
|
||||
export const redoItem = (transaction, item, redoitems, itemsToDelete, ignoreRemoteMapChanges) => {
|
||||
const doc = transaction.doc
|
||||
const store = doc.store
|
||||
const ownClientID = doc.clientID
|
||||
@@ -152,7 +153,7 @@ export const redoItem = (transaction, item, redoitems, itemsToDelete) => {
|
||||
// make sure that parent is redone
|
||||
if (parentItem !== null && parentItem.deleted === true) {
|
||||
// try to undo parent if it will be undone anyway
|
||||
if (parentItem.redone === null && (!redoitems.has(parentItem) || redoItem(transaction, parentItem, redoitems, itemsToDelete) === null)) {
|
||||
if (parentItem.redone === null && (!redoitems.has(parentItem) || redoItem(transaction, parentItem, redoitems, itemsToDelete, ignoreRemoteMapChanges) === null)) {
|
||||
return null
|
||||
}
|
||||
while (parentItem.redone !== null) {
|
||||
@@ -198,7 +199,7 @@ export const redoItem = (transaction, item, redoitems, itemsToDelete) => {
|
||||
}
|
||||
} else {
|
||||
right = null
|
||||
if (item.right) {
|
||||
if (item.right && !ignoreRemoteMapChanges) {
|
||||
left = item
|
||||
// Iterate right while right is in itemsToDelete
|
||||
// If it is intended to delete right while item is redone, we can expect that item should replace right.
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from '../internals.js'
|
||||
|
||||
import * as time from 'lib0/time'
|
||||
import * as array from 'lib0/array'
|
||||
import { Observable } from 'lib0/observable'
|
||||
|
||||
class StackItem {
|
||||
@@ -30,6 +31,18 @@ class StackItem {
|
||||
this.meta = new Map()
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @param {Transaction} tr
|
||||
* @param {UndoManager} um
|
||||
* @param {StackItem} stackItem
|
||||
*/
|
||||
const clearUndoManagerStackItem = (tr, um, stackItem) => {
|
||||
iterateDeletedStructs(tr, stackItem.deletions, item => {
|
||||
if (item instanceof Item && um.scope.some(type => isParentOf(type, item))) {
|
||||
keepItem(item, false)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {UndoManager} undoManager
|
||||
@@ -88,7 +101,7 @@ const popStackItem = (undoManager, stack, eventType) => {
|
||||
}
|
||||
})
|
||||
itemsToRedo.forEach(struct => {
|
||||
performedChange = redoItem(transaction, struct, itemsToRedo, stackItem.insertions) !== null || performedChange
|
||||
performedChange = redoItem(transaction, struct, itemsToRedo, stackItem.insertions, undoManager.ignoreRemoteMapChanges) !== null || performedChange
|
||||
})
|
||||
// We want to delete in reverse order so that children are deleted before
|
||||
// parents, so we have more information available when items are filtered.
|
||||
@@ -124,6 +137,7 @@ const popStackItem = (undoManager, stack, eventType) => {
|
||||
* filter returns false, the type/item won't be deleted even it is in the
|
||||
* undo/redo scope.
|
||||
* @property {Set<any>} [UndoManagerOptions.trackedOrigins=new Set([null])]
|
||||
* @property {boolean} [ignoreRemoteMapChanges] Experimental. By default, the UndoManager will never overwrite remote changes. Enable this property to enable overwriting remote changes on key-value changes (Y.Map, properties on Y.Xml, etc..).
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -133,16 +147,20 @@ const popStackItem = (undoManager, stack, eventType) => {
|
||||
* 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`.
|
||||
*
|
||||
* @extends {Observable<'stack-item-added'|'stack-item-popped'>}
|
||||
* @extends {Observable<'stack-item-added'|'stack-item-popped'|'stack-cleared'|'stack-item-updated'>}
|
||||
*/
|
||||
export class UndoManager extends Observable {
|
||||
/**
|
||||
* @param {AbstractType<any>|Array<AbstractType<any>>} typeScope Accepts either a single type, or an array of types
|
||||
* @param {UndoManagerOptions} options
|
||||
*/
|
||||
constructor (typeScope, { captureTimeout = 500, deleteFilter = () => true, trackedOrigins = new Set([null]) } = {}) {
|
||||
constructor (typeScope, { captureTimeout = 500, deleteFilter = () => true, trackedOrigins = new Set([null]), ignoreRemoteMapChanges = false } = {}) {
|
||||
super()
|
||||
this.scope = typeScope instanceof Array ? typeScope : [typeScope]
|
||||
/**
|
||||
* @type {Array<AbstractType<any>>}
|
||||
*/
|
||||
this.scope = []
|
||||
this.addToScope(typeScope)
|
||||
this.deleteFilter = deleteFilter
|
||||
trackedOrigins.add(this)
|
||||
this.trackedOrigins = trackedOrigins
|
||||
@@ -163,7 +181,11 @@ export class UndoManager extends Observable {
|
||||
this.redoing = false
|
||||
this.doc = /** @type {Doc} */ (this.scope[0].doc)
|
||||
this.lastChange = 0
|
||||
this.doc.on('afterTransaction', /** @param {Transaction} transaction */ transaction => {
|
||||
this.ignoreRemoteMapChanges = ignoreRemoteMapChanges
|
||||
/**
|
||||
* @param {Transaction} transaction
|
||||
*/
|
||||
this.afterTransactionHandler = transaction => {
|
||||
// Only track certain transactions
|
||||
if (!this.scope.some(type => transaction.changedParentTypes.has(type)) || (!this.trackedOrigins.has(transaction.origin) && (!transaction.origin || !this.trackedOrigins.has(transaction.origin.constructor)))) {
|
||||
return
|
||||
@@ -175,7 +197,7 @@ export class UndoManager extends Observable {
|
||||
this.stopCapturing() // next undo should not be appended to last stack item
|
||||
} else if (!redoing) {
|
||||
// neither undoing nor redoing: delete redoStack
|
||||
this.redoStack = []
|
||||
this.clear(false, true)
|
||||
}
|
||||
const insertions = new DeleteSet()
|
||||
transaction.afterState.forEach((endClock, client) => {
|
||||
@@ -206,29 +228,59 @@ export class UndoManager extends Observable {
|
||||
keepItem(item, true)
|
||||
}
|
||||
})
|
||||
const changeEvent = [{ stackItem: stack[stack.length - 1], origin: transaction.origin, type: undoing ? 'redo' : 'undo', changedParentTypes: transaction.changedParentTypes }, this]
|
||||
if (didAdd) {
|
||||
this.emit('stack-item-added', [{ stackItem: stack[stack.length - 1], origin: transaction.origin, type: undoing ? 'redo' : 'undo', changedParentTypes: transaction.changedParentTypes }, this])
|
||||
this.emit('stack-item-added', changeEvent)
|
||||
} else {
|
||||
this.emit('stack-item-updated', changeEvent)
|
||||
}
|
||||
}
|
||||
this.doc.on('afterTransaction', this.afterTransactionHandler)
|
||||
this.doc.on('destroy', () => {
|
||||
this.destroy()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Array<AbstractType<any>> | AbstractType<any>} ytypes
|
||||
*/
|
||||
addToScope (ytypes) {
|
||||
ytypes = array.isArray(ytypes) ? ytypes : [ytypes]
|
||||
ytypes.forEach(ytype => {
|
||||
if (this.scope.every(yt => yt !== ytype)) {
|
||||
this.scope.push(ytype)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
clear () {
|
||||
this.doc.transact(transaction => {
|
||||
/**
|
||||
* @param {StackItem} stackItem
|
||||
*/
|
||||
const clearItem = stackItem => {
|
||||
iterateDeletedStructs(transaction, stackItem.deletions, item => {
|
||||
if (item instanceof Item && this.scope.some(type => isParentOf(type, item))) {
|
||||
keepItem(item, false)
|
||||
}
|
||||
})
|
||||
}
|
||||
this.undoStack.forEach(clearItem)
|
||||
this.redoStack.forEach(clearItem)
|
||||
})
|
||||
this.undoStack = []
|
||||
this.redoStack = []
|
||||
/**
|
||||
* @param {any} origin
|
||||
*/
|
||||
addTrackedOrigin (origin) {
|
||||
this.trackedOrigins.add(origin)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} origin
|
||||
*/
|
||||
removeTrackedOrigin (origin) {
|
||||
this.trackedOrigins.delete(origin)
|
||||
}
|
||||
|
||||
clear (clearUndoStack = true, clearRedoStack = true) {
|
||||
if ((clearUndoStack && this.canUndo()) || (clearRedoStack && this.canRedo())) {
|
||||
this.doc.transact(tr => {
|
||||
if (clearUndoStack) {
|
||||
this.undoStack.forEach(item => clearUndoManagerStackItem(tr, this, item))
|
||||
this.undoStack = []
|
||||
}
|
||||
if (clearRedoStack) {
|
||||
this.redoStack.forEach(item => clearUndoManagerStackItem(tr, this, item))
|
||||
this.redoStack = []
|
||||
}
|
||||
this.emit('stack-cleared', [{ undoStackCleared: clearUndoStack, redoStackCleared: clearRedoStack }])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -304,4 +356,10 @@ export class UndoManager extends Observable {
|
||||
canRedo () {
|
||||
return this.redoStack.length > 0
|
||||
}
|
||||
|
||||
destroy () {
|
||||
this.trackedOrigins.delete(this)
|
||||
this.doc.off('afterTransaction', this.afterTransactionHandler)
|
||||
super.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,10 +298,24 @@ export class UpdateEncoderV2 extends DSEncoderV2 {
|
||||
writeKey (key) {
|
||||
const clock = this.keyMap.get(key)
|
||||
if (clock === undefined) {
|
||||
/**
|
||||
* @todo uncomment to introduce this feature finally
|
||||
*
|
||||
* Background. The ContentFormat object was always encoded using writeKey, but the decoder used to use readString.
|
||||
* Furthermore, I forgot to set the keyclock. So everything was working fine.
|
||||
*
|
||||
* However, this feature here is basically useless as it is not being used (it actually only consumes extra memory).
|
||||
*
|
||||
* I don't know yet how to reintroduce this feature..
|
||||
*
|
||||
* Older clients won't be able to read updates when we reintroduce this feature. So this should probably be done using a flag.
|
||||
*
|
||||
*/
|
||||
// this.keyMap.set(key, this.keyClock)
|
||||
this.keyClockEncoder.write(this.keyClock++)
|
||||
this.stringEncoder.write(key)
|
||||
} else {
|
||||
this.keyClockEncoder.write(this.keyClock++)
|
||||
this.keyClockEncoder.write(clock)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ export const decodeUpdate = (update) => decodeUpdateV2(update, UpdateDecoderV1)
|
||||
*/
|
||||
export const decodeUpdateV2 = (update, YDecoder = UpdateDecoderV2) => {
|
||||
const structs = []
|
||||
const updateDecoder = new UpdateDecoderV1(decoding.createDecoder(update))
|
||||
const updateDecoder = new YDecoder(decoding.createDecoder(update))
|
||||
const lazyDecoder = new LazyStructReader(updateDecoder, false)
|
||||
for (let curr = lazyDecoder.curr; curr !== null; curr = lazyDecoder.next()) {
|
||||
structs.push(curr)
|
||||
|
||||
@@ -565,3 +565,26 @@ export const testUndoDeleteTextFormat = tc => {
|
||||
t.compare(text.toDelta(), expect)
|
||||
t.compare(text2.toDelta(), expect)
|
||||
}
|
||||
|
||||
/**
|
||||
* Undo text formatting delete should not corrupt peer state.
|
||||
*
|
||||
* @see https://github.com/yjs/yjs/issues/392
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testBehaviorOfIgnoreremotemapchangesProperty = tc => {
|
||||
const doc = new Y.Doc()
|
||||
const doc2 = new Y.Doc()
|
||||
doc.on('update', update => Y.applyUpdate(doc2, update, doc))
|
||||
doc2.on('update', update => Y.applyUpdate(doc, update, doc2))
|
||||
const map1 = doc.getMap()
|
||||
const map2 = doc2.getMap()
|
||||
const um1 = new Y.UndoManager(map1, { ignoreRemoteMapChanges: true })
|
||||
map1.set('x', 1)
|
||||
map2.set('x', 2)
|
||||
map1.set('x', 3)
|
||||
map2.set('x', 4)
|
||||
um1.undo()
|
||||
t.assert(map1.get('x') === 2)
|
||||
t.assert(map2.get('x') === 2)
|
||||
}
|
||||
|
||||
@@ -112,6 +112,24 @@ export const testMergeUpdates = tc => {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testKeyEncoding = tc => {
|
||||
const { users, text0, text1 } = init(tc, { users: 2 })
|
||||
|
||||
text0.insert(0, 'a', { italic: true })
|
||||
text0.insert(0, 'b')
|
||||
text0.insert(0, 'c', { italic: true })
|
||||
|
||||
const update = Y.encodeStateAsUpdateV2(users[0])
|
||||
Y.applyUpdateV2(users[1], update)
|
||||
|
||||
t.compare(text1.toDelta(), [{ insert: 'c', attributes: { italic: true } }, { insert: 'b' }, { insert: 'a', attributes: { italic: true } }])
|
||||
|
||||
compare(users)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Y.Doc} ydoc
|
||||
* @param {Array<Uint8Array>} updates - expecting at least 4 updates
|
||||
|
||||
Reference in New Issue
Block a user