@flowgram-vue/command 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/src/command.ts ADDED
@@ -0,0 +1,387 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ import { injectable, multiInject, optional } from 'inversify';
7
+ import { Disposable, DisposableCollection, Emitter } from '@flowgram-vue/utils';
8
+
9
+ import { type CommandService } from './command-service';
10
+
11
+ export interface Command {
12
+ /**
13
+ * id,唯一 key
14
+ */
15
+ id: string;
16
+ /**
17
+ * 展示用 label
18
+ */
19
+ label?: string;
20
+ /**
21
+ * 在一些明确的场景下,部分只展示简短的 label 即可
22
+ */
23
+ shortLabel?: string;
24
+
25
+ /**
26
+ * 展示用 command icon
27
+ */
28
+ icon?: string | object | ((props: any) => any);
29
+ /**
30
+ * 暂不使用
31
+ */
32
+ category?: string;
33
+ }
34
+ export namespace Command {
35
+ export enum Default {
36
+ ZOOM_IN = 'ZOOM_IN',
37
+ ZOOM_OUT = 'ZOOM_OUT',
38
+ DELETE = 'DELETE',
39
+ COPY = 'COPY',
40
+ PASTE = 'PASTE',
41
+ UNDO = 'UNDO',
42
+ REDO = 'REDO',
43
+ VIEW_CLOSE_ALL_WIDGET = 'view.closeAllWidget',
44
+ VIEW_CLOSE_CURRENT_WIDGET = 'view.closeCurrentWidget',
45
+ VIEW_REOPEN_LAST_WIDGET = 'view.reopenLastWidget',
46
+ VIEW_CLOSE_OTHER_WIDGET = 'view.closeOtherWidget',
47
+ VIEW_CLOSE_BOTTOM_PANEL = 'view.closeBottomPannel',
48
+ VIEW_OPEN_NEXT_TAB = 'view.openNextTab',
49
+ VIEW_OEPN_LAST_TAB = 'view.openLastTab',
50
+ VIEW_FULL_SCREEN = 'view.fullScreen',
51
+ VIEW_SAVING_WIDGET_CLOSE_CONFIRM = 'view.savingWidgetCloseConfirm',
52
+ VIEW_SHORTCUTS = 'view.shortcuts',
53
+ VIEW_PREFERENCES = 'view.preferences',
54
+ VIEW_LOG = 'view.log',
55
+ VIEW_PROBLEMS = 'view.problems',
56
+ }
57
+
58
+ /**
59
+ * 判断是否是 command
60
+ */
61
+ export function is(arg: Command | any): arg is Command {
62
+ return !!arg && arg === Object(arg) && 'id' in arg;
63
+ }
64
+ }
65
+
66
+ export interface CommandHandler {
67
+ /**
68
+ * handler 执行函数
69
+ */
70
+ execute(...args: any[]): any;
71
+
72
+ /**
73
+ * 该 handler 是否可以执行
74
+ */
75
+ isEnabled?(...args: any[]): boolean;
76
+
77
+ /**
78
+ * 预留 contextMenu 用,该 handler 是否可见
79
+ */
80
+ isVisible?(...args: any[]): boolean;
81
+
82
+ /**
83
+ * 预留 contextMenu 用,该 handler 是否可以触发
84
+ */
85
+ isToggled?(...args: any[]): boolean;
86
+ }
87
+
88
+ export interface CommandEvent {
89
+ /**
90
+ * commandId
91
+ */
92
+ commandId: string;
93
+ /**
94
+ * 参数
95
+ */
96
+ args: any[];
97
+ }
98
+ export const CommandContribution = Symbol('CommandContribution');
99
+
100
+ export interface CommandContribution {
101
+ /**
102
+ * 注册 command
103
+ */
104
+ registerCommands(commands: CommandService): void;
105
+ }
106
+
107
+ /**
108
+ * 当前正在运行的 command
109
+ */
110
+ interface CommandExecuting {
111
+ /**
112
+ * commandid
113
+ */
114
+ id: string;
115
+ /**
116
+ * 参数
117
+ */
118
+ args: any[];
119
+ /**
120
+ * 正在进行的 promise
121
+ */
122
+ promise?: Promise<any>;
123
+ }
124
+
125
+ namespace CommandExecuting {
126
+ /**
127
+ * 获取正在运行的 command 单个实例
128
+ */
129
+ export function findSimple(
130
+ arrs: Set<CommandExecuting>,
131
+ newCmd: CommandExecuting,
132
+ ): CommandExecuting | undefined {
133
+ for (const item of arrs.values()) {
134
+ if (
135
+ item.id === newCmd.id &&
136
+ item.args.length === newCmd.args.length &&
137
+ item.args.every((arg, index) => (newCmd as any)[index] === arg)
138
+ ) {
139
+ return item;
140
+ }
141
+ }
142
+ }
143
+ }
144
+
145
+ export const CommandRegistryFactory = 'CommandRegistryFactory';
146
+
147
+ @injectable()
148
+ export class CommandRegistry implements CommandService {
149
+ protected readonly _handlers: { [id: string]: CommandHandler[] } = {};
150
+
151
+ protected readonly _commands: { [id: string]: Command } = {};
152
+
153
+ protected readonly _commandExecutings = new Set<CommandExecuting>();
154
+
155
+ protected readonly toUnregisterCommands = new Map<string, Disposable>();
156
+
157
+ protected readonly onDidExecuteCommandEmitter = new Emitter<CommandEvent>();
158
+
159
+ readonly onDidExecuteCommand = this.onDidExecuteCommandEmitter.event;
160
+
161
+ protected readonly onWillExecuteCommandEmitter = new Emitter<CommandEvent>();
162
+
163
+ readonly onWillExecuteCommand = this.onWillExecuteCommandEmitter.event;
164
+
165
+ @multiInject(CommandContribution)
166
+ @optional()
167
+ protected readonly contributions: CommandContribution[];
168
+
169
+ init() {
170
+ for (const contrib of this.contributions) {
171
+ contrib.registerCommands(this);
172
+ }
173
+ }
174
+
175
+ /**
176
+ * 当前所有 command
177
+ */
178
+ get commands(): Command[] {
179
+ const commands: Command[] = [];
180
+ for (const id of this.commandIds) {
181
+ const cmd = this.getCommand(id);
182
+ if (cmd) {
183
+ commands.push(cmd);
184
+ }
185
+ }
186
+ return commands;
187
+ }
188
+
189
+ /**
190
+ * 当前所有 commandid
191
+ */
192
+ get commandIds(): string[] {
193
+ return Object.keys(this._commands);
194
+ }
195
+
196
+ /**
197
+ * registerCommand
198
+ */
199
+ registerCommand(id: string, handler?: CommandHandler): Disposable;
200
+
201
+ registerCommand(command: Command, handler?: CommandHandler): Disposable;
202
+
203
+ registerCommand(commandOrId: string | Command, handler?: CommandHandler): Disposable {
204
+ const command: Command = typeof commandOrId === 'string' ? { id: commandOrId } : commandOrId;
205
+
206
+ if (this._commands[command.id]) {
207
+ console.warn(`A command ${command.id} is already registered.`);
208
+ return Disposable.NULL;
209
+ }
210
+ const toDispose = new DisposableCollection(this.doRegisterCommand(command));
211
+ if (handler) {
212
+ toDispose.push(this.registerHandler(command.id, handler));
213
+ }
214
+ this.toUnregisterCommands.set(command.id, toDispose);
215
+ toDispose.push(Disposable.create(() => this.toUnregisterCommands.delete(command.id)));
216
+ return toDispose;
217
+ }
218
+
219
+ /**
220
+ * unregisterCommand
221
+ */
222
+ unregisterCommand(command: Command): void;
223
+
224
+ unregisterCommand(id: string): void;
225
+
226
+ unregisterCommand(commandOrId: Command | string): void {
227
+ const id = Command.is(commandOrId) ? commandOrId.id : commandOrId;
228
+ const toUnregister = this.toUnregisterCommands.get(id);
229
+ if (toUnregister) {
230
+ toUnregister.dispose();
231
+ }
232
+ }
233
+
234
+ /**
235
+ * 注册 handler
236
+ */
237
+ registerHandler(commandId: string, handler: CommandHandler): Disposable {
238
+ let handlers = this._handlers[commandId];
239
+ if (!handlers) {
240
+ this._handlers[commandId] = handlers = [];
241
+ }
242
+ handlers.unshift(handler);
243
+ return {
244
+ dispose: () => {
245
+ const idx = handlers.indexOf(handler);
246
+ if (idx >= 0) {
247
+ handlers.splice(idx, 1);
248
+ }
249
+ },
250
+ };
251
+ }
252
+
253
+ /**
254
+ * 预留 contextMenu 用,该 handler 是否可见
255
+ */
256
+ isVisible(command: string, ...args: any[]): boolean {
257
+ return typeof this.getVisibleHandler(command, ...args) !== 'undefined';
258
+ }
259
+
260
+ /**
261
+ * command 是否可用
262
+ */
263
+ isEnabled(command: string, ...args: any[]): boolean {
264
+ return typeof this.getActiveHandler(command, ...args) !== 'undefined';
265
+ }
266
+
267
+ /**
268
+ * 预留 contextMenu 用,该 handler 是否可以触发
269
+ */
270
+ isToggled(command: string, ...args: any[]): boolean {
271
+ return typeof this.getToggledHandler(command, ...args) !== 'undefined';
272
+ }
273
+
274
+ /**
275
+ * 执行 command,会先判断是否可以执行,不会重复执行
276
+ */
277
+ async executeCommand<T>(commandId: string, ...args: any[]): Promise<T | undefined> {
278
+ const handler = this.getActiveHandler(commandId, ...args);
279
+ const execInfo: CommandExecuting = { id: commandId, args };
280
+ const simpleExecInfo = CommandExecuting.findSimple(this._commandExecutings, execInfo);
281
+ if (simpleExecInfo) {
282
+ return execInfo.promise;
283
+ }
284
+ if (handler) {
285
+ try {
286
+ this._commandExecutings.add(execInfo);
287
+ this.onWillExecuteCommandEmitter.fire({ commandId, args });
288
+ const promise = handler.execute(...args);
289
+ execInfo.promise = promise;
290
+ const result = await promise;
291
+ this.onDidExecuteCommandEmitter.fire({ commandId, args });
292
+ return result;
293
+ } finally {
294
+ this._commandExecutings.delete(execInfo);
295
+ }
296
+ }
297
+ }
298
+
299
+ getVisibleHandler(commandId: string, ...args: any[]): CommandHandler | undefined {
300
+ const handlers = this._handlers[commandId];
301
+ if (handlers) {
302
+ for (const handler of handlers) {
303
+ try {
304
+ if (!handler.isVisible || handler.isVisible(...args)) {
305
+ return handler;
306
+ }
307
+ } catch (error) {
308
+ console.error(error);
309
+ }
310
+ }
311
+ }
312
+ return undefined;
313
+ }
314
+
315
+ getActiveHandler(commandId: string, ...args: any[]): CommandHandler | undefined {
316
+ const handlers = this._handlers[commandId];
317
+ if (handlers) {
318
+ for (const handler of handlers) {
319
+ try {
320
+ if (!handler.isEnabled || handler.isEnabled(...args)) {
321
+ return handler;
322
+ }
323
+ } catch (error) {
324
+ console.error(error);
325
+ }
326
+ }
327
+ }
328
+ return undefined;
329
+ }
330
+
331
+ /**
332
+ * 获取 command 对应的所有 handler
333
+ */
334
+ getAllHandlers(commandId: string): CommandHandler[] {
335
+ const handlers = this._handlers[commandId];
336
+ return handlers ? handlers.slice() : [];
337
+ }
338
+
339
+ getToggledHandler(commandId: string, ...args: any[]): CommandHandler | undefined {
340
+ const handlers = this._handlers[commandId];
341
+ if (handlers) {
342
+ for (const handler of handlers) {
343
+ try {
344
+ if (handler.isToggled && handler.isToggled(...args)) {
345
+ return handler;
346
+ }
347
+ } catch (error) {
348
+ console.error(error);
349
+ }
350
+ }
351
+ }
352
+ return undefined;
353
+ }
354
+
355
+ /**
356
+ * 获取 command
357
+ */
358
+ getCommand(id: string): Command | undefined {
359
+ return this._commands[id];
360
+ }
361
+
362
+ protected doRegisterCommand(command: Command): Disposable {
363
+ this._commands[command.id] = command;
364
+ return {
365
+ dispose: () => {
366
+ delete this._commands[command.id];
367
+ },
368
+ };
369
+ }
370
+
371
+ /**
372
+ * 更新 command
373
+ */
374
+ public updateCommand(id: string, command: Partial<Omit<Command, 'id'>>) {
375
+ if (this._commands[id]) {
376
+ this._commands[id] = {
377
+ ...this._commands[id],
378
+ ...command,
379
+ };
380
+ }
381
+ }
382
+
383
+ dispose() {
384
+ this.onWillExecuteCommandEmitter.dispose();
385
+ this.onDidExecuteCommandEmitter.dispose();
386
+ }
387
+ }
package/src/index.ts ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+
6
+ export {
7
+ CommandContribution,
8
+ type CommandHandler,
9
+ CommandRegistry,
10
+ Command,
11
+ CommandRegistryFactory,
12
+ } from './command';
13
+ export { CommandService } from './command-service';
14
+ export { CommandContainerModule } from './command-container-module';