@flowgram-vue/history 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { injectable } from 'inversify';
7
+ import { Emitter, DisposableCollection } from '@flowgram-vue/utils';
8
+
9
+ import {
10
+ IUndoRedoElement,
11
+ IUndoRedoService,
12
+ UndoRedoChangeType,
13
+ UndoRedoChangeEvent,
14
+ UndoRedoClearEvent,
15
+ } from './types';
16
+
17
+ @injectable()
18
+ export class UndoRedoService implements IUndoRedoService {
19
+ private _undoStack: IUndoRedoElement[];
20
+
21
+ private _redoStack: IUndoRedoElement[];
22
+
23
+ private _undoing: boolean = false;
24
+
25
+ private _redoing: boolean = false;
26
+
27
+ private _limit: number = 100;
28
+
29
+ protected onChangeEmitter = new Emitter<UndoRedoChangeEvent>();
30
+
31
+ readonly onChange = this.onChangeEmitter.event;
32
+
33
+ readonly _toDispose = new DisposableCollection();
34
+
35
+ constructor() {
36
+ this._undoStack = [];
37
+ this._redoStack = [];
38
+ this._toDispose.push(this.onChangeEmitter);
39
+ }
40
+
41
+ setLimit(limit: number) {
42
+ this._limit = limit;
43
+ }
44
+
45
+ pushElement(element: IUndoRedoElement): void {
46
+ this._redoStack = [];
47
+ this._stackPush(this._undoStack, element);
48
+ this._toDispose.push(element);
49
+ this._emitChange(UndoRedoChangeType.PUSH, element);
50
+ }
51
+
52
+ getUndoStack() {
53
+ return this._undoStack;
54
+ }
55
+
56
+ getRedoStack() {
57
+ return this._redoStack;
58
+ }
59
+
60
+ getLastElement() {
61
+ return this._undoStack[this._undoStack.length - 1];
62
+ }
63
+
64
+ /**
65
+ * 执行undo
66
+ * @returns void
67
+ */
68
+ async undo(): Promise<void> {
69
+ if (!this.canUndo()) {
70
+ return;
71
+ }
72
+
73
+ if (this._undoing) {
74
+ return;
75
+ }
76
+ this._undoing = true;
77
+
78
+ const item = this._undoStack.pop() as IUndoRedoElement;
79
+
80
+ try {
81
+ await item.undo();
82
+ } finally {
83
+ this._stackPush(this._redoStack, item);
84
+ this._emitChange(UndoRedoChangeType.UNDO, item);
85
+ this._undoing = false;
86
+ }
87
+ }
88
+
89
+ /**
90
+ * 执行redo
91
+ * @returns void
92
+ */
93
+ async redo(): Promise<void> {
94
+ if (!this.canRedo()) {
95
+ return;
96
+ }
97
+
98
+ if (this._redoing) {
99
+ return;
100
+ }
101
+ this._redoing = true;
102
+
103
+ const item = this._redoStack.pop() as IUndoRedoElement;
104
+
105
+ try {
106
+ await item.redo();
107
+ } finally {
108
+ this._stackPush(this._undoStack, item);
109
+ this._emitChange(UndoRedoChangeType.REDO, item);
110
+ this._redoing = false;
111
+ }
112
+ }
113
+
114
+ /**
115
+ * 是否可undo
116
+ * @returns true代表可以,false代表不可以
117
+ */
118
+ canUndo(): boolean {
119
+ return this._undoStack.length > 0;
120
+ }
121
+
122
+ /**
123
+ * 是否可redo
124
+ * @returns true代表可以,false代表不可以
125
+ */
126
+ canRedo(): boolean {
127
+ return this._redoStack.length > 0;
128
+ }
129
+
130
+ /**
131
+ * 是否可以push
132
+ * @returns true代表可以,false代表不可以
133
+ */
134
+ canPush(): boolean {
135
+ return !this._redoing && !this._undoing;
136
+ }
137
+
138
+ /**
139
+ * 清空
140
+ */
141
+ clear() {
142
+ this.clearRedoStack();
143
+ this.clearUndoStack();
144
+ this._emitChange(UndoRedoChangeType.CLEAR);
145
+ }
146
+
147
+ /**
148
+ * 清空redo栈
149
+ */
150
+ clearRedoStack(): void {
151
+ this._redoStack.forEach(element => {
152
+ element.dispose();
153
+ });
154
+ this._redoStack = [];
155
+ }
156
+
157
+ /**
158
+ * 清空undo栈
159
+ */
160
+ clearUndoStack(): void {
161
+ this._undoStack.forEach(element => {
162
+ element.dispose();
163
+ });
164
+ this._undoStack = [];
165
+ }
166
+
167
+ /**
168
+ * 销毁
169
+ */
170
+ dispose(): void {
171
+ this.clear();
172
+ this._toDispose.dispose();
173
+ }
174
+
175
+ private _stackPush(stack: IUndoRedoElement[], element: IUndoRedoElement) {
176
+ stack.push(element);
177
+ if (stack.length > this._limit) {
178
+ stack.shift();
179
+ }
180
+ }
181
+
182
+ private _emitChange(type: UndoRedoChangeType, element?: IUndoRedoElement) {
183
+ if (element) {
184
+ this.onChangeEmitter.fire({ type, element });
185
+ } else {
186
+ this.onChangeEmitter.fire({ type } as UndoRedoClearEvent);
187
+ }
188
+ }
189
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { nanoid } from 'nanoid';
7
+ import { injectable } from 'inversify';
8
+
9
+ @injectable()
10
+ export class HistoryConfig {
11
+ generateId: () => string = () => nanoid();
12
+
13
+ getSnapshot: () => unknown = () => '';
14
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { ContainerModule } from 'inversify';
7
+
8
+ import { OperationRegistry, OperationService } from './operation';
9
+ import { HistoryContext } from './history-context';
10
+ import {
11
+ HistoryService,
12
+ HistoryStack,
13
+ HistoryManager,
14
+ UndoRedoService,
15
+ HistoryConfig,
16
+ } from './history';
17
+
18
+ export const HistoryContainerModule = new ContainerModule(
19
+ (bind, _unbind, _isBound, _rebind, _unbindAsync, onActivation, _onDeactivation) => {
20
+ bind(OperationRegistry).toSelf().inSingletonScope();
21
+ bind(OperationService).toSelf().inSingletonScope();
22
+ bind(UndoRedoService).toSelf().inSingletonScope();
23
+ bind(HistoryService).toSelf().inSingletonScope();
24
+ bind(HistoryContext).toSelf().inSingletonScope();
25
+ bind(HistoryManager).toSelf().inSingletonScope();
26
+ bind(HistoryStack).toSelf().inSingletonScope();
27
+ bind(HistoryConfig).toSelf().inSingletonScope();
28
+
29
+ onActivation(HistoryService, (ctx, historyService) => {
30
+ let historyManager;
31
+
32
+ if (ctx.container?.parent?.isBound(HistoryManager)) {
33
+ historyManager = ctx.container?.parent?.get(HistoryManager);
34
+ } else {
35
+ historyManager = ctx.container.get(HistoryManager);
36
+ }
37
+
38
+ if (!historyManager) {
39
+ return historyService;
40
+ }
41
+
42
+ historyService.historyManager = historyManager;
43
+ historyManager.registerHistoryService(historyService);
44
+ return historyService;
45
+ });
46
+ }
47
+ );
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { injectable } from 'inversify';
7
+
8
+ @injectable()
9
+ export class HistoryContext {
10
+ /**
11
+ * 所属uri
12
+ */
13
+ uri?: string;
14
+
15
+ /**
16
+ * 操作触发的源对象,如编辑器对象
17
+ */
18
+ source?: unknown;
19
+ }
package/src/index.ts ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ export * from './operation';
7
+ export * from './history';
8
+ export * from './history-container-module';
9
+ export * from './create-history-plugin';
10
+ export * from './history-context';
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ export * from './operation-contribution';
7
+ export * from './operation-registry';
8
+ export * from './operation-service';
9
+ export * from './types';
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { OperationRegistry } from './operation-registry';
7
+
8
+ export const OperationContribution = Symbol('OperationContribution');
9
+
10
+ export interface OperationContribution {
11
+ registerOperationMeta?(operationRegistry: OperationRegistry): void;
12
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { injectable, multiInject, optional, postConstruct } from 'inversify';
7
+ import { Disposable, DisposableCollection } from '@flowgram-vue/utils';
8
+
9
+ import { OperationMeta } from './types';
10
+ import { OperationContribution } from './operation-contribution';
11
+
12
+ @injectable()
13
+ export class OperationRegistry {
14
+ private readonly _operationMetas: Map<string, OperationMeta> = new Map();
15
+
16
+ @multiInject(OperationContribution)
17
+ @optional()
18
+ protected readonly contributions: OperationContribution[] = [];
19
+
20
+ @postConstruct()
21
+ protected init() {
22
+ for (const contrib of this.contributions) {
23
+ contrib.registerOperationMeta?.(this);
24
+ }
25
+ }
26
+
27
+ /**
28
+ * 注册操作的元数据
29
+ * @param operationMeta 操作的元数据
30
+ * @returns 销毁函数
31
+ */
32
+ registerOperationMeta(operationMeta: OperationMeta): Disposable {
33
+ if (this._operationMetas.has(operationMeta.type)) {
34
+ console.warn(`A operation meta ${operationMeta.type} is already registered.`);
35
+ return Disposable.NULL;
36
+ }
37
+ const toDispose = new DisposableCollection(this._doRegisterOperationMetaMeta(operationMeta));
38
+ return toDispose;
39
+ }
40
+
41
+ /**
42
+ * 获取操作的元数据
43
+ * @param type 操作类型
44
+ * @returns 操作的元数据
45
+ */
46
+ getOperationMeta(type: string): OperationMeta | undefined {
47
+ return this._operationMetas.get(type);
48
+ }
49
+
50
+ private _doRegisterOperationMetaMeta(operationMeta: OperationMeta): Disposable {
51
+ this._operationMetas.set(operationMeta.type, operationMeta);
52
+ return {
53
+ dispose: () => {
54
+ this._operationMetas.delete(operationMeta.type);
55
+ },
56
+ };
57
+ }
58
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { injectable, inject, postConstruct } from 'inversify';
7
+ import { DisposableCollection, Emitter } from '@flowgram-vue/utils';
8
+
9
+ import { HistoryContext } from '../history-context';
10
+ import { HistoryConfig } from '../history-config';
11
+ import { Operation } from './types';
12
+ import { OperationRegistry } from './operation-registry';
13
+
14
+ @injectable()
15
+ export class OperationService {
16
+ @inject(OperationRegistry)
17
+ readonly operationRegistry: OperationRegistry;
18
+
19
+ @inject(HistoryContext)
20
+ readonly context: HistoryContext;
21
+
22
+ @inject(HistoryConfig)
23
+ config: HistoryConfig;
24
+
25
+ readonly applyEmitter = new Emitter<Operation>();
26
+
27
+ readonly onApply = this.applyEmitter.event;
28
+
29
+ private _toDispose = new DisposableCollection();
30
+
31
+ @postConstruct()
32
+ init() {
33
+ this._toDispose.push(this.applyEmitter);
34
+ }
35
+
36
+ /**
37
+ * 执行操作
38
+ * @param op
39
+ * @returns
40
+ */
41
+ applyOperation(op: Operation, options?: { noApply?: boolean }): any {
42
+ const meta = this.operationRegistry.getOperationMeta(op.type);
43
+
44
+ if (!meta) {
45
+ throw new Error(`Operation meta ${op.type} has not registered.`);
46
+ }
47
+
48
+ let res;
49
+ if (!options?.noApply) {
50
+ res = meta.apply(op, this.context.source);
51
+ }
52
+
53
+ this.applyEmitter.fire(op);
54
+
55
+ return res;
56
+ }
57
+
58
+ /**
59
+ * 根据操作类型获取操作的label
60
+ * @param operation 操作
61
+ * @returns
62
+ */
63
+ getOperationLabel(operation: Operation): string | undefined {
64
+ const operationMeta = this.operationRegistry.getOperationMeta(operation.type);
65
+
66
+ if (operationMeta && operationMeta.getLabel) {
67
+ return operationMeta.getLabel(operation, this.context.source);
68
+ }
69
+ }
70
+
71
+ /**
72
+ * 根据操作类型获取操作的description
73
+ * @param operation 操作
74
+ * @returns
75
+ */
76
+ getOperationDescription(operation: Operation): string | undefined {
77
+ const operationMeta = this.operationRegistry.getOperationMeta(operation.type);
78
+
79
+ if (operationMeta && operationMeta.getDescription) {
80
+ return operationMeta.getDescription(operation, this.context.source);
81
+ }
82
+ }
83
+
84
+ /**
85
+ * 操作取反
86
+ * @param operations
87
+ * @returns
88
+ */
89
+ inverseOperations(operations: Operation[]) {
90
+ return operations.map(op => this.inverseOperation(op)).reverse();
91
+ }
92
+
93
+ inverseOperation(op: Operation): Operation {
94
+ const meta = this.operationRegistry.getOperationMeta(op.type);
95
+
96
+ if (!meta) {
97
+ throw new Error(`Operation meta ${op.type} has not registered.`);
98
+ }
99
+ return meta.inverse(op);
100
+ }
101
+
102
+ dispose() {
103
+ this._toDispose.dispose();
104
+ }
105
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { StackOperation } from '../history';
7
+
8
+ /**
9
+ * 操作
10
+ */
11
+ export interface Operation<OperationValue = unknown> {
12
+ /**
13
+ * 操作的类型 如insert_node, move_node等
14
+ */
15
+ type: string;
16
+ /**
17
+ * 操作的值 外部自定义
18
+ */
19
+ value: OperationValue;
20
+ /**
21
+ * 资源唯一标志
22
+ */
23
+ uri?: string;
24
+ /**
25
+ * 操作触发源头
26
+ */
27
+ origin?: string | Symbol;
28
+ }
29
+
30
+ export type OperationWithId = Operation & { id: string };
31
+
32
+ /**
33
+ * push操作配置
34
+ */
35
+ export interface PushOperationOptions {
36
+ noApply?: boolean;
37
+ }
38
+
39
+ /**
40
+ * 操作历史
41
+ */
42
+ export interface HistoryOperation extends Operation {
43
+ /**
44
+ * 唯一id
45
+ */
46
+ id: string;
47
+ /**
48
+ * 显示名称
49
+ */
50
+ label?: string;
51
+ /**
52
+ * 描述
53
+ */
54
+ description?: string;
55
+ /**
56
+ * 时间戳
57
+ */
58
+ timestamp: number;
59
+ }
60
+
61
+ /**
62
+ * 操作元数据
63
+ */
64
+ export interface OperationMeta<OperationValue = any, Source = any, ApplyResult = any> {
65
+ /**
66
+ * 操作类型 需要唯一
67
+ */
68
+ type: string;
69
+ /**
70
+ * 将一个操作转换成另一个逆操作, 如insert转成delete
71
+ * @param op 操作
72
+ * @returns 逆操作
73
+ */
74
+ inverse: (op: Operation<OperationValue>) => Operation<OperationValue>;
75
+ /**
76
+ * 判断是否可以合并
77
+ * @param op 操作
78
+ * @param prev 上一个操作
79
+ * @returns true表示可以合并 返回一个操作表示直接用新操作替换之前的操作
80
+ */
81
+ shouldMerge?: (
82
+ op: Operation<OperationValue>,
83
+ prev: Operation<OperationValue> | undefined,
84
+ stackItem: StackOperation
85
+ ) => boolean | Operation;
86
+ /**
87
+ * 判断是否需要保存,如选中等操作可以不保存
88
+ * @param op 操作
89
+ * @returns true表示可以保存
90
+ */
91
+ shouldSave?: (op: Operation<OperationValue>) => boolean;
92
+ /**
93
+ * 执行操作
94
+ * @param operation 操作
95
+ */
96
+ apply(operation: Operation<OperationValue>, source: Source): ApplyResult | Promise<ApplyResult>;
97
+ /**
98
+ * 获取标签
99
+ */
100
+ getLabel?: (operation: Operation<OperationValue>, source: Source) => string;
101
+ /**
102
+ * 获取描述
103
+ */
104
+ getDescription?: (operation: Operation<OperationValue>, source: Source) => string;
105
+ /**
106
+ * 获取uri
107
+ */
108
+ getURI?: (operation: Operation<OperationValue>, source: Source) => string | undefined;
109
+ }