mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-25 02:13:00 +08:00
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:
@@ -411,7 +411,7 @@ export class CodeBlockComponent extends CaptionedBlockComponent<CodeBlockModel>
|
|||||||
})}
|
})}
|
||||||
.yText=${this.model.props.text.yText}
|
.yText=${this.model.props.text.yText}
|
||||||
.inlineEventSource=${this.topContenteditableElement ?? nothing}
|
.inlineEventSource=${this.topContenteditableElement ?? nothing}
|
||||||
.undoManager=${this.store.history}
|
.undoManager=${this.store.history.undoManager}
|
||||||
.attributesSchema=${this.inlineManager.getSchema()}
|
.attributesSchema=${this.inlineManager.getSchema()}
|
||||||
.attributeRenderer=${this.inlineManager.getRenderer()}
|
.attributeRenderer=${this.inlineManager.getRenderer()}
|
||||||
.readonly=${this.store.readonly}
|
.readonly=${this.store.readonly}
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ export class ListBlockComponent extends CaptionedBlockComponent<ListBlockModel>
|
|||||||
<rich-text
|
<rich-text
|
||||||
.yText=${this.model.props.text.yText}
|
.yText=${this.model.props.text.yText}
|
||||||
.inlineEventSource=${this.topContenteditableElement ?? nothing}
|
.inlineEventSource=${this.topContenteditableElement ?? nothing}
|
||||||
.undoManager=${this.store.history}
|
.undoManager=${this.store.history.undoManager}
|
||||||
.attributeRenderer=${this.attributeRenderer}
|
.attributeRenderer=${this.attributeRenderer}
|
||||||
.attributesSchema=${this.attributesSchema}
|
.attributesSchema=${this.attributesSchema}
|
||||||
.markdownMatches=${this.inlineManager?.markdownMatches}
|
.markdownMatches=${this.inlineManager?.markdownMatches}
|
||||||
|
|||||||
@@ -293,7 +293,7 @@ export class ParagraphBlockComponent extends CaptionedBlockComponent<ParagraphBl
|
|||||||
<rich-text
|
<rich-text
|
||||||
.yText=${this.model.props.text.yText}
|
.yText=${this.model.props.text.yText}
|
||||||
.inlineEventSource=${this.topContenteditableElement ?? nothing}
|
.inlineEventSource=${this.topContenteditableElement ?? nothing}
|
||||||
.undoManager=${this.store.history}
|
.undoManager=${this.store.history.undoManager}
|
||||||
.attributesSchema=${this.attributesSchema}
|
.attributesSchema=${this.attributesSchema}
|
||||||
.attributeRenderer=${this.attributeRenderer}
|
.attributeRenderer=${this.attributeRenderer}
|
||||||
.markdownMatches=${this.inlineManager?.markdownMatches}
|
.markdownMatches=${this.inlineManager?.markdownMatches}
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ export class DocTitle extends WithDisposable(ShadowlessElement) {
|
|||||||
>
|
>
|
||||||
<rich-text
|
<rich-text
|
||||||
.yText=${this._rootModel?.props.title.yText}
|
.yText=${this._rootModel?.props.title.yText}
|
||||||
.undoManager=${this.doc.history}
|
.undoManager=${this.doc.history.undoManager}
|
||||||
.verticalScrollContainerGetter=${() => this._viewport}
|
.verticalScrollContainerGetter=${() => this._viewport}
|
||||||
.readonly=${this.doc.readonly}
|
.readonly=${this.doc.readonly}
|
||||||
.enableFormat=${false}
|
.enableFormat=${false}
|
||||||
|
|||||||
@@ -89,14 +89,14 @@ function notifyWithUndoActionImpl(
|
|||||||
options.abort?.addEventListener('abort', abort);
|
options.abort?.addEventListener('abort', abort);
|
||||||
|
|
||||||
const clearOnClose = () => {
|
const clearOnClose = () => {
|
||||||
store.history.off('stack-item-added', addHandler);
|
store.history.undoManager.off('stack-item-added', addHandler);
|
||||||
store.history.off('stack-item-popped', popHandler);
|
store.history.undoManager.off('stack-item-popped', popHandler);
|
||||||
disposable.unsubscribe();
|
disposable.unsubscribe();
|
||||||
options.abort?.removeEventListener('abort', abort);
|
options.abort?.removeEventListener('abort', abort);
|
||||||
};
|
};
|
||||||
|
|
||||||
const addHandler = store.history.on('stack-item-added', abort);
|
const addHandler = store.history.undoManager.on('stack-item-added', abort);
|
||||||
const popHandler = store.history.on('stack-item-popped', abort);
|
const popHandler = store.history.undoManager.on('stack-item-popped', abort);
|
||||||
const disposable = provider
|
const disposable = provider
|
||||||
.get(EditorLifeCycleExtension)
|
.get(EditorLifeCycleExtension)
|
||||||
.slots.unmounted.subscribe(() => abort());
|
.slots.unmounted.subscribe(() => abort());
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ abstract class ToolbarContextBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get history() {
|
get history() {
|
||||||
return this.store.history;
|
return this.store.history.undoManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
get view() {
|
get view() {
|
||||||
|
|||||||
@@ -725,13 +725,13 @@ Check if the store can undo
|
|||||||
|
|
||||||
#### Get Signature
|
#### Get Signature
|
||||||
|
|
||||||
> **get** **history**(): `UndoManager`
|
> **get** **history**(): `HistoryExtension`
|
||||||
|
|
||||||
Get the Y.UndoManager instance for current store.
|
Get the Y.UndoManager instance for current store.
|
||||||
|
|
||||||
##### Returns
|
##### Returns
|
||||||
|
|
||||||
`UndoManager`
|
`HistoryExtension`
|
||||||
|
|
||||||
***
|
***
|
||||||
|
|
||||||
|
|||||||
@@ -32,14 +32,6 @@ The payload can have three types:
|
|||||||
|
|
||||||
***
|
***
|
||||||
|
|
||||||
### historyUpdated
|
|
||||||
|
|
||||||
> **historyUpdated**: `Subject`\<`void`\>
|
|
||||||
|
|
||||||
This fires when the history is updated.
|
|
||||||
|
|
||||||
***
|
|
||||||
|
|
||||||
### ready
|
### ready
|
||||||
|
|
||||||
> **ready**: `Subject`\<`void`\>
|
> **ready**: `Subject`\<`void`\>
|
||||||
|
|||||||
@@ -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 './extension';
|
||||||
|
export * from './history';
|
||||||
export * from './schema';
|
export * from './schema';
|
||||||
export * from './selection';
|
export * from './selection';
|
||||||
export * from './store-extension';
|
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.undoManager.on('stack-item-added', this._itemAdded);
|
||||||
this.store.history.on('stack-item-popped', this._itemPopped);
|
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() {
|
get value() {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import type { ExtensionType } from '../../extension/extension.js';
|
|||||||
import {
|
import {
|
||||||
BlockSchemaIdentifier,
|
BlockSchemaIdentifier,
|
||||||
type Doc,
|
type Doc,
|
||||||
|
HistoryExtension,
|
||||||
StoreExtensionIdentifier,
|
StoreExtensionIdentifier,
|
||||||
StoreSelectionExtension,
|
StoreSelectionExtension,
|
||||||
} from '../../extension/index.js';
|
} from '../../extension/index.js';
|
||||||
@@ -163,10 +164,6 @@ export type StoreSlots = {
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
blockUpdated: Subject<StoreBlockUpdatedPayloads>;
|
blockUpdated: Subject<StoreBlockUpdatedPayloads>;
|
||||||
/**
|
|
||||||
* This fires when the history is updated.
|
|
||||||
*/
|
|
||||||
historyUpdated: Subject<void>;
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
yBlockUpdated: Subject<
|
yBlockUpdated: Subject<
|
||||||
| {
|
| {
|
||||||
@@ -182,7 +179,7 @@ export type StoreSlots = {
|
|||||||
>;
|
>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const internalExtensions = [StoreSelectionExtension];
|
const internalExtensions = [StoreSelectionExtension, HistoryExtension];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Core store class that manages blocks and their lifecycle in BlockSuite
|
* Core store class that manages blocks and their lifecycle in BlockSuite
|
||||||
@@ -231,10 +228,6 @@ export class Store {
|
|||||||
|
|
||||||
private readonly _schema: Schema;
|
private readonly _schema: Schema;
|
||||||
|
|
||||||
private readonly _canRedo = signal(false);
|
|
||||||
|
|
||||||
private readonly _canUndo = signal(false);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the id of the store.
|
* Get the id of the store.
|
||||||
*
|
*
|
||||||
@@ -310,7 +303,7 @@ export class Store {
|
|||||||
if (this.readonly) {
|
if (this.readonly) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return this._canRedo.peek();
|
return this._history.canRedo;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -322,7 +315,7 @@ export class Store {
|
|||||||
if (this.readonly) {
|
if (this.readonly) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return this._canUndo.peek();
|
return this._history.canUndo;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -330,35 +323,35 @@ export class Store {
|
|||||||
*
|
*
|
||||||
* @category History
|
* @category History
|
||||||
*/
|
*/
|
||||||
undo() {
|
undo = () => {
|
||||||
if (this.readonly) {
|
if (this.readonly) {
|
||||||
console.error('cannot undo in readonly mode');
|
console.error('cannot undo in readonly mode');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._history.undo();
|
this._history.undoManager.undo();
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Redo the last undone transaction.
|
* Redo the last undone transaction.
|
||||||
*
|
*
|
||||||
* @category History
|
* @category History
|
||||||
*/
|
*/
|
||||||
redo() {
|
redo = () => {
|
||||||
if (this.readonly) {
|
if (this.readonly) {
|
||||||
console.error('cannot undo in readonly mode');
|
console.error('cannot undo in readonly mode');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._history.redo();
|
this._history.undoManager.redo();
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset the history of the store.
|
* Reset the history of the store.
|
||||||
*
|
*
|
||||||
* @category History
|
* @category History
|
||||||
*/
|
*/
|
||||||
resetHistory() {
|
resetHistory = () => {
|
||||||
return this._history.clear();
|
return this._history.undoManager.clear();
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute a transaction.
|
* Execute a transaction.
|
||||||
@@ -374,7 +367,7 @@ export class Store {
|
|||||||
* @category History
|
* @category History
|
||||||
*/
|
*/
|
||||||
transact(fn: () => void, shouldTransact: boolean = this._shouldTransact) {
|
transact(fn: () => void, shouldTransact: boolean = this._shouldTransact) {
|
||||||
const spaceDoc = this.doc.spaceDoc;
|
const spaceDoc = this._doc.spaceDoc;
|
||||||
spaceDoc.transact(
|
spaceDoc.transact(
|
||||||
() => {
|
() => {
|
||||||
try {
|
try {
|
||||||
@@ -425,9 +418,9 @@ export class Store {
|
|||||||
*
|
*
|
||||||
* @category History
|
* @category History
|
||||||
*/
|
*/
|
||||||
captureSync() {
|
captureSync = () => {
|
||||||
this._history.stopCapturing();
|
this._history.undoManager.stopCapturing();
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the {@link Workspace} instance for current store.
|
* Get the {@link Workspace} instance for current store.
|
||||||
@@ -555,7 +548,9 @@ export class Store {
|
|||||||
|
|
||||||
private _isDisposed = false;
|
private _isDisposed = false;
|
||||||
|
|
||||||
private readonly _history!: Y.UndoManager;
|
private get _history() {
|
||||||
|
return this._provider.get(HistoryExtension);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
@@ -568,7 +563,6 @@ export class Store {
|
|||||||
rootAdded: new Subject(),
|
rootAdded: new Subject(),
|
||||||
rootDeleted: new Subject(),
|
rootDeleted: new Subject(),
|
||||||
blockUpdated: new Subject(),
|
blockUpdated: new Subject(),
|
||||||
historyUpdated: new Subject(),
|
|
||||||
yBlockUpdated: new Subject(),
|
yBlockUpdated: new Subject(),
|
||||||
};
|
};
|
||||||
this._schema = new Schema();
|
this._schema = new Schema();
|
||||||
@@ -609,35 +603,9 @@ export class Store {
|
|||||||
this._onBlockAdded(id, false, true);
|
this._onBlockAdded(id, false, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
this._history = new Y.UndoManager([this._yBlocks], {
|
|
||||||
trackedOrigins: new Set([this.doc.spaceDoc.clientID]),
|
|
||||||
});
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
this._subscribeToSlots();
|
this._subscribeToSlots();
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly _historyObserver = () => {
|
|
||||||
this._updateCanUndoRedoSignals();
|
|
||||||
this.slots.historyUpdated.next();
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly _subscribeToSlots = () => {
|
private readonly _subscribeToSlots = () => {
|
||||||
this.disposableGroup.add(
|
this.disposableGroup.add(
|
||||||
this.slots.yBlockUpdated.subscribe(({ type, id, isLocal }) => {
|
this.slots.yBlockUpdated.subscribe(({ type, id, isLocal }) => {
|
||||||
@@ -1270,14 +1238,13 @@ export class Store {
|
|||||||
this._provider.getAll(StoreExtensionIdentifier).forEach(ext => {
|
this._provider.getAll(StoreExtensionIdentifier).forEach(ext => {
|
||||||
ext.disposed();
|
ext.disposed();
|
||||||
});
|
});
|
||||||
if (this.doc.ready) {
|
if (this._doc.ready) {
|
||||||
this._yBlocks.unobserveDeep(this._handleYEvents);
|
this._yBlocks.unobserveDeep(this._handleYEvents);
|
||||||
}
|
}
|
||||||
this.slots.ready.complete();
|
this.slots.ready.complete();
|
||||||
this.slots.rootAdded.complete();
|
this.slots.rootAdded.complete();
|
||||||
this.slots.rootDeleted.complete();
|
this.slots.rootDeleted.complete();
|
||||||
this.slots.blockUpdated.complete();
|
this.slots.blockUpdated.complete();
|
||||||
this.slots.historyUpdated.complete();
|
|
||||||
this.slots.yBlockUpdated.complete();
|
this.slots.yBlockUpdated.complete();
|
||||||
this.disposableGroup.dispose();
|
this.disposableGroup.dispose();
|
||||||
this._isDisposed = true;
|
this._isDisposed = true;
|
||||||
|
|||||||
@@ -746,7 +746,7 @@ export class StarterDebugMenu extends ShadowlessElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override firstUpdated() {
|
override firstUpdated() {
|
||||||
this.doc.slots.historyUpdated.subscribe(() => {
|
this.doc.history.onUpdated.subscribe(() => {
|
||||||
this._canUndo = this.doc.canUndo;
|
this._canUndo = this.doc.canUndo;
|
||||||
this._canRedo = this.doc.canRedo;
|
this._canRedo = this.doc.canRedo;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -56,6 +56,6 @@ export function initDocFromProps(
|
|||||||
paragraphId: paragraphBlockId,
|
paragraphId: paragraphBlockId,
|
||||||
surfaceId,
|
surfaceId,
|
||||||
});
|
});
|
||||||
doc.history.clear();
|
doc.history.undoManager.clear();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ const renderRichText = ({
|
|||||||
|
|
||||||
const richText = new RichText();
|
const richText = new RichText();
|
||||||
richText.yText = text;
|
richText.yText = text;
|
||||||
richText.undoManager = doc.history;
|
richText.undoManager = doc.history.undoManager;
|
||||||
richText.readonly = doc.readonly;
|
richText.readonly = doc.readonly;
|
||||||
richText.attributesSchema = inlineManager.getSchema() as any;
|
richText.attributesSchema = inlineManager.getSchema() as any;
|
||||||
richText.attributeRenderer = inlineManager.getRenderer();
|
richText.attributeRenderer = inlineManager.getRenderer();
|
||||||
|
|||||||
Reference in New Issue
Block a user