refactor(editor): history as a store extension (#12214)

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **Refactor**
  - Improved history and undo/redo management across the app by introducing a dedicated history extension. Undo/redo operations now use a more focused undo manager, resulting in clearer and more consistent behavior.
- **Documentation**
  - Updated API documentation to reflect changes in history management, including revised method signatures and removal of outdated event references.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Saul-Mirone
2025-05-12 01:50:57 +00:00
parent e91e0e1812
commit 6fb7f51ea2
16 changed files with 124 additions and 78 deletions
@@ -0,0 +1,79 @@
import { signal } from '@preact/signals-core';
import { Subject } from 'rxjs';
import * as Y from 'yjs';
import type { Store } from '../../model';
import { StoreExtension } from '../store-extension';
export class HistoryExtension extends StoreExtension {
static override readonly key = 'history';
private readonly _history!: Y.UndoManager;
private readonly _canRedo = signal(false);
private readonly _canUndo = signal(false);
readonly onUpdated = new Subject<void>();
constructor(store: Store) {
super(store);
this._history = new Y.UndoManager([this.store.doc.yBlocks], {
trackedOrigins: new Set([this.store.doc.spaceDoc.clientID]),
});
}
private readonly _updateCanUndoRedoSignals = () => {
const canRedo = this._history.canRedo();
const canUndo = this._history.canUndo();
if (this._canRedo.peek() !== canRedo) {
this._canRedo.value = canRedo;
}
if (this._canUndo.peek() !== canUndo) {
this._canUndo.value = canUndo;
}
};
get canRedo() {
return this._canRedo.peek();
}
get canUndo() {
return this._canUndo.peek();
}
get canRedo$() {
return this._canRedo;
}
get canUndo$() {
return this._canUndo;
}
get undoManager() {
return this._history;
}
override loaded() {
this._updateCanUndoRedoSignals();
this._history.on('stack-cleared', this._historyObserver);
this._history.on('stack-item-added', this._historyObserver);
this._history.on('stack-item-popped', this._historyObserver);
this._history.on('stack-item-updated', this._historyObserver);
}
private readonly _historyObserver = () => {
this._updateCanUndoRedoSignals();
this.onUpdated.next();
};
override disposed() {
super.disposed();
this._history.off('stack-cleared', this._historyObserver);
this._history.off('stack-item-added', this._historyObserver);
this._history.off('stack-item-popped', this._historyObserver);
this._history.off('stack-item-updated', this._historyObserver);
this.onUpdated.complete();
}
}
@@ -0,0 +1 @@
export * from './history-extension';
@@ -1,4 +1,5 @@
export * from './extension';
export * from './history';
export * from './schema';
export * from './selection';
export * from './store-extension';
@@ -94,8 +94,14 @@ export class StoreSelectionExtension extends StoreExtension {
}
);
this.store.history.on('stack-item-added', this._itemAdded);
this.store.history.on('stack-item-popped', this._itemPopped);
this.store.history.undoManager.on('stack-item-added', this._itemAdded);
this.store.history.undoManager.on('stack-item-popped', this._itemPopped);
}
override disposed() {
super.disposed();
this.store.history.undoManager.off('stack-item-added', this._itemAdded);
this.store.history.undoManager.off('stack-item-popped', this._itemPopped);
}
get value() {