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)
+}