From 251c8aaefcd8b0f626c2ab23ed19aada49aed04b Mon Sep 17 00:00:00 2001 From: Kevin Jahns Date: Tue, 20 Aug 2019 22:28:49 +0200 Subject: [PATCH] UndoManager configuration to filter deletes --- README.v13.md | 2 +- src/utils/UndoManager.js | 39 ++++++++++++++++++++++++++++++--------- tests/undo-redo.tests.js | 21 ++++++++++++++++++++- 3 files changed, 51 insertions(+), 11 deletions(-) diff --git a/README.v13.md b/README.v13.md index 8adcab73..fc2afb51 100644 --- a/README.v13.md +++ b/README.v13.md @@ -662,7 +662,7 @@ ytext.toString() // => 'abc'
constructor(scope:Y.AbstractType|Array<Y.AbstractType>, - [trackedTransactionOrigins:Set<any>, [{captureTimeout: number}]]) + [[{captureTimeout:number,trackedOrigins:Set<any>,deleteFilter:function(item):boolean}]])
Accepts either single type as scope or an array of types.
undo()
diff --git a/src/utils/UndoManager.js b/src/utils/UndoManager.js index 4a28205a..789321a2 100644 --- a/src/utils/UndoManager.js +++ b/src/utils/UndoManager.js @@ -61,6 +61,10 @@ const popStackItem = (undoManager, stack, eventType) => { performedChange = redoItem(transaction, item, itemsToRedo) !== null || performedChange }) const structs = /** @type {Array} */ (store.clients.get(doc.clientID)) + /** + * @type {Array} + */ + const itemsToDelete = [] iterateStructs(transaction, structs, stackItem.start, stackItem.len, struct => { if (struct instanceof Item && !struct.deleted && scope.some(type => isParentOf(type, /** @type {Item} */ (struct)))) { if (struct.redone !== null) { @@ -73,11 +77,18 @@ const popStackItem = (undoManager, stack, eventType) => { } struct = item } - keepItem(struct) - struct.delete(transaction) - performedChange = true + itemsToDelete.push(struct) } }) + // We want to delete in reverse order so that children are deleted before + // parents, so we have more information available when items are filtered. + for (let i = itemsToDelete.length - 1; i >= 0; i--) { + const item = itemsToDelete[i] + if (undoManager.deleteFilter(item)) { + item.delete(transaction) + performedChange = true + } + } result = stackItem if (result != null) { undoManager.emit('stack-item-popped', [{ stackItem: result, type: eventType }, undoManager]) @@ -87,6 +98,16 @@ const popStackItem = (undoManager, stack, eventType) => { return result } +/** + * @typedef {Object} UndoManagerOptions + * @property {number} [UndoManagerOptions.captureTimeout=500] + * @property {function(Item):boolean} [UndoManagerOptions.deleteFilter=()=>true] Sometimes + * it is necessary to filter whan an Undo/Redo operation can delete. If this + * 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])] + */ + /** * 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 @@ -99,17 +120,17 @@ const popStackItem = (undoManager, stack, eventType) => { export class UndoManager extends Observable { /** * @param {AbstractType|Array>} typeScope Accepts either a single type, or an array of types - * @param {Set} [trackedTransactionOrigins=new Set([null])] - * @param {object} [options={captureTimeout=500}] + * @param {UndoManagerOptions} options */ - constructor (typeScope, trackedTransactionOrigins = new Set([null]), { captureTimeout } = {}) { + constructor (typeScope, { captureTimeout, deleteFilter = () => true, trackedOrigins = new Set([null]) } = {}) { if (captureTimeout == null) { captureTimeout = 500 } super() this.scope = typeScope instanceof Array ? typeScope : [typeScope] - trackedTransactionOrigins.add(this) - this.trackedTransactionOrigins = trackedTransactionOrigins + this.deleteFilter = deleteFilter + trackedOrigins.add(this) + this.trackedOrigins = trackedOrigins /** * @type {Array} */ @@ -129,7 +150,7 @@ export class UndoManager extends Observable { this.lastChange = 0 this.doc.on('afterTransaction', /** @param {Transaction} transaction */ transaction => { // Only track certain transactions - if (!this.scope.some(type => transaction.changedParentTypes.has(type)) || (!this.trackedTransactionOrigins.has(transaction.origin) && (!transaction.origin || !this.trackedTransactionOrigins.has(transaction.origin.constructor)))) { + if (!this.scope.some(type => transaction.changedParentTypes.has(type)) || (!this.trackedOrigins.has(transaction.origin) && (!transaction.origin || !this.trackedOrigins.has(transaction.origin.constructor)))) { return } const undoing = this.undoing diff --git a/tests/undo-redo.tests.js b/tests/undo-redo.tests.js index ca4051f8..cc4ee027 100644 --- a/tests/undo-redo.tests.js +++ b/tests/undo-redo.tests.js @@ -172,7 +172,7 @@ export const testUndoEvents = tc => { export const testTrackClass = tc => { const { users, text0 } = init(tc, { users: 3 }) // only track origins that are numbers - const undoManager = new UndoManager(text0, new Set([Number])) + const undoManager = new UndoManager(text0, { trackedOrigins: new Set([Number]) }) users[0].transact(() => { text0.insert(0, 'abc') }, 42) @@ -201,3 +201,22 @@ export const testTypeScope = tc => { undoManagerBoth.undo() t.assert(text1.toString() === '') } + +/** + * @param {t.TestCase} tc + */ +export const testUndoDeleteFilter = tc => { + /** + * @type {Array>} + */ + const array0 = /** @type {any} */ (init(tc, { users: 3 }).array0) + const undoManager = new UndoManager(array0, { deleteFilter: item => !(item instanceof Y.Item) || (item.content instanceof Y.ContentType && item.content.type._map.size === 0) }) + const map0 = new Y.Map() + map0.set('hi', 1) + const map1 = new Y.Map() + array0.insert(0, [map0, map1]) + undoManager.undo() + t.assert(array0.length === 1) + array0.get(0) + t.assert(Array.from(array0.get(0).keys()).length === 1) +}