diff --git a/src/utils/UndoManager.js b/src/utils/UndoManager.js index f6f13ee5..c0dca63d 100644 --- a/src/utils/UndoManager.js +++ b/src/utils/UndoManager.js @@ -191,6 +191,12 @@ export class UndoManager extends Observable { */ this.undoing = false this.redoing = false + /** + * The currently popped stack item if UndoManager.undoing or UndoManager.redoing + * + * @type {StackItem|null} + */ + this.doingStackItem = null this.lastChange = 0 this.ignoreRemoteMapChanges = ignoreRemoteMapChanges this.captureTimeout = captureTimeout @@ -331,10 +337,12 @@ export class UndoManager extends Observable { */ undo () { this.undoing = true + this.doingStackItem = array.last(this.undoStack) ?? null let res try { res = popStackItem(this, this.undoStack, 'undo') } finally { + this.doingStackItem = null this.undoing = false } return res @@ -347,10 +355,12 @@ export class UndoManager extends Observable { */ redo () { this.redoing = true + this.doingStackItem = array.last(this.redoStack) ?? null let res try { res = popStackItem(this, this.redoStack, 'redo') } finally { + this.doingStackItem = null this.redoing = false } return res diff --git a/tests/undo-redo.tests.js b/tests/undo-redo.tests.js index f1dbf428..82c98129 100644 --- a/tests/undo-redo.tests.js +++ b/tests/undo-redo.tests.js @@ -715,3 +715,66 @@ export const testUndoDeleteInMap = (tc) => { undoManager.undo() t.compare(map0.toJSON(), { a: 'a' }) } + +/** + * It should expose the StackItem being processed if undoing + * + * @param {t.TestCase} tc + */ +export const testUndoDoingStackItem = async (tc) => { + const doc = new Y.Doc() + const text = doc.getText('text') + const undoManager = new Y.UndoManager([text]) + + undoManager.on('stack-item-added', /** @param {any} event */ event => { + event.stackItem.meta.set('str', '42') + }) + + const meta = new Promise((resolve) => { + setTimeout(() => resolve('ABORTED'), 50) + text.observe((event) => { + const /** @type {Y.UndoManager} */ origin = event.transaction.origin + if (origin === undoManager && origin.undoing) { + resolve(origin.doingStackItem?.meta.get('str')) + } + }) + }) + + text.insert(0, 'abc') + undoManager.undo() + + t.compare(await meta, '42') + t.compare(undoManager.doingStackItem, null) +} + +/** + * It should expose the StackItem being processed if redoing + * + * @param {t.TestCase} tc + */ +export const testRedoDoingStackItem = async (tc) => { + const doc = new Y.Doc() + const text = doc.getText('text') + const undoManager = new Y.UndoManager([text]) + + undoManager.on('stack-item-added', /** @param {any} event */ event => { + event.stackItem.meta.set('str', '42') + }) + + const meta = new Promise(resolve => { + setTimeout(() => resolve('ABORTED'), 50) + text.observe((event) => { + const /** @type {Y.UndoManager} */ origin = event.transaction.origin + if (origin === undoManager && origin.redoing) { + resolve(origin.doingStackItem?.meta.get('str')) + } + }) + }) + + text.insert(0, 'abc') + undoManager.undo() + undoManager.redo() + + t.compare(await meta, '42') + t.compare(undoManager.doingStackItem, null) +}