From a22b3cdbc1ee975c023d78725f521f250b7020c2 Mon Sep 17 00:00:00 2001 From: Kevin Jahns Date: Sat, 26 Mar 2022 10:29:19 +0100 Subject: [PATCH] add option to UndoManager to ignore remote map changes. implements #390 --- src/structs/Item.js | 7 ++++--- src/utils/UndoManager.js | 6 ++++-- tests/undo-redo.tests.js | 23 +++++++++++++++++++++++ 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/structs/Item.js b/src/structs/Item.js index 7ac80f5d..0b39a246 100644 --- a/src/structs/Item.js +++ b/src/structs/Item.js @@ -127,12 +127,13 @@ export const splitItem = (transaction, leftItem, diff) => { * @param {Item} item * @param {Set} 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. diff --git a/src/utils/UndoManager.js b/src/utils/UndoManager.js index c2a4dcc8..21892ff2 100644 --- a/src/utils/UndoManager.js +++ b/src/utils/UndoManager.js @@ -101,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. @@ -137,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} [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..). */ /** @@ -153,7 +154,7 @@ export class UndoManager extends Observable { * @param {AbstractType|Array>} 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() /** * @type {Array>} @@ -180,6 +181,7 @@ export class UndoManager extends Observable { this.redoing = false this.doc = /** @type {Doc} */ (this.scope[0].doc) this.lastChange = 0 + this.ignoreRemoteMapChanges = ignoreRemoteMapChanges /** * @param {Transaction} transaction */ diff --git a/tests/undo-redo.tests.js b/tests/undo-redo.tests.js index 3ce65d5e..c1889d9f 100644 --- a/tests/undo-redo.tests.js +++ b/tests/undo-redo.tests.js @@ -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) +}