@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.
- package/LICENSE +22 -0
- package/dist/index.cjs +851 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +683 -0
- package/dist/index.js +843 -0
- package/dist/index.js.map +1 -0
- package/package.json +54 -0
- package/src/create-history-plugin.ts +24 -0
- package/src/history/history-manager.ts +129 -0
- package/src/history/history-service.ts +244 -0
- package/src/history/history-stack.ts +166 -0
- package/src/history/index.ts +12 -0
- package/src/history/stack-operation.ts +112 -0
- package/src/history/types.ts +325 -0
- package/src/history/undo-redo-service.ts +189 -0
- package/src/history-config.ts +14 -0
- package/src/history-container-module.ts +47 -0
- package/src/history-context.ts +19 -0
- package/src/index.ts +10 -0
- package/src/operation/index.ts +9 -0
- package/src/operation/operation-contribution.ts +12 -0
- package/src/operation/operation-registry.ts +58 -0
- package/src/operation/operation-service.ts +105 -0
- package/src/operation/types.ts +109 -0
|
@@ -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,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
|
+
}
|