import EventEmitter from '@antv/event-emitter'; import { GraphEvent } from '../../constants'; import { HistoryEvent } from '../../constants/events/history'; import type { RuntimeContext } from '../../runtime/types'; import { DataChange, Loosen } from '../../types'; import type { Command } from '../../types/history'; import type { GraphLifeCycleEvent } from '../../utils/event'; import { idsOf } from '../../utils/id'; import type { BasePluginOptions } from '../base-plugin'; import { BasePlugin } from '../base-plugin'; import { parseCommand } from './util'; /** * 历史记录配置项 * * History options */ export interface HistoryOptions extends BasePluginOptions { /** * 最多记录该数据长度的历史记录 * * The maximum number of history records * @defaultValue 0(不做限制) */ stackSize?: number; /** * 当一个命令被添加到 Undo/Redo 队列前被调用,如果该方法返回 false,那么这个命令将不会被添加到队列中。revert 为 true 时表示撤销操作,为 false 时表示重做操作 * * Called before a command is added to the Undo/Redo queue. If this method returns false, the command will not be added to the queue. revert is true for undo operations and false for redo operations */ beforeAddCommand?: (cmd: Command, revert: boolean) => boolean | void; /** * 当一个命令被添加到 Undo/Redo 队列后被调用。revert 为 true 时表示撤销操作,为 false 时表示重做操作 * * Called after a command is added to the Undo/Redo queue. revert is true for undo operations and false for redo operations */ afterAddCommand?: (cmd: Command, revert: boolean) => void; /** * 执行命令时的回调函数 * * Callback function when executing a command */ executeCommand?: (cmd: Command) => void; } /** * 历史记录 * * History * @remarks * 历史记录用于记录图的数据变化,支持撤销和重做等操作。 * * History is used to record data changes in the graph and supports operations such as undo and redo. */ export class History extends BasePlugin { static defaultOptions: Partial = { stackSize: 0, }; private emitter: EventEmitter; private batchChanges: DataChange[][] | null = null; private batchAnimation = false; public undoStack: Command[] = []; public redoStack: Command[] = []; private freezed = false; constructor(context: RuntimeContext, options: HistoryOptions) { super(context, Object.assign({}, History.defaultOptions, options)); this.emitter = new EventEmitter(); const { graph } = this.context; graph.on(GraphEvent.AFTER_DRAW, this.addCommand); graph.on(GraphEvent.BATCH_START, this.initBatchCommand); graph.on(GraphEvent.BATCH_END, this.addCommand); } /** * 是否可以执行撤销操作 * * Whether undo can be done * @returns 是否可以执行撤销操作 | Whether undo can be done */ public canUndo() { return this.undoStack.length > 0; } /** * 是否可以执行重做操作 * * Whether redo can be done * @returns 是否可以执行重做操作 | Whether redo can be done */ public canRedo() { return this.redoStack.length > 0; } /** * 执行撤销 * * Execute undo * @returns 返回当前实例 | Return the current instance */ public undo() { const cmd = this.undoStack.pop(); if (cmd) { this.executeCommand(cmd); const before = this.options.beforeAddCommand?.(cmd, false); if (before === false) return; this.redoStack.push(cmd); this.options.afterAddCommand?.(cmd, false); this.notify(HistoryEvent.UNDO, cmd); } return this; } /** * 执行重做 * * Execute redo * @returns 返回当前实例 | Return the current instance */ public redo() { const cmd = this.redoStack.pop(); if (cmd) { this.executeCommand(cmd, false); this.undoStackPush(cmd); this.notify(HistoryEvent.REDO, cmd); } return this; } /** * 执行撤销且不计入历史记录 * * Execute undo and do not record in history * @returns 返回当前实例 | Return the current instance */ public undoAndCancel() { const cmd = this.undoStack.pop(); if (cmd) { this.executeCommand(cmd, false); this.redoStack = []; this.notify(HistoryEvent.CANCEL, cmd); } return this; } private executeCommand = (cmd: Command, revert = true) => { this.freezed = true; this.options.executeCommand?.(cmd); const values = revert ? cmd.original : cmd.current; this.context.graph.addData(values.add); this.context.graph.updateData(values.update); this.context.graph.removeData(idsOf(values.remove, false)); this.context.element?.draw({ silence: true, animation: cmd.animation }); this.freezed = false; }; private addCommand = (event: GraphLifeCycleEvent) => { if (this.freezed) return; if (event.type === GraphEvent.AFTER_DRAW) { const { dataChanges = [], animation = true } = (event as GraphLifeCycleEvent).data; if (this.context.batch?.isBatching) { if (!this.batchChanges) return; this.batchChanges.push(dataChanges); this.batchAnimation &&= animation; return; } this.batchChanges = [dataChanges]; this.batchAnimation = animation; } this.undoStackPush(parseCommand(this.batchChanges!.flat(), this.batchAnimation, this.context)); this.notify(HistoryEvent.ADD, this.undoStack[this.undoStack.length - 1]); }; private initBatchCommand = (event: GraphLifeCycleEvent) => { const { initiate } = event.data; this.batchAnimation = false; if (initiate) { this.batchChanges = []; } else { const cmd = this.undoStack.pop(); if (!cmd) this.batchChanges = null; } }; private undoStackPush(cmd: Command): void { const { stackSize } = this.options; if (stackSize !== 0 && this.undoStack.length >= stackSize) { this.undoStack.shift(); } const before = this.options.beforeAddCommand?.(cmd, true); if (before === false) return; this.undoStack.push(cmd); this.options.afterAddCommand?.(cmd, true); } /** * 清空历史记录 * * Clear history */ public clear(): void { this.undoStack = []; this.redoStack = []; this.batchChanges = null; this.batchAnimation = false; this.notify(HistoryEvent.CLEAR, null); } private notify(event: Loosen, cmd: Command | null) { this.emitter.emit(event, { cmd }); this.emitter.emit(HistoryEvent.CHANGE, { cmd }); } /** * 监听历史记录事件 * * Listen to history events * @param event - 事件名称 | Event name * @param handler - 事件处理函数 | Event handler */ public on(event: Loosen, handler: (e: { cmd?: Command | null }) => void): void { this.emitter.on(event, handler); } /** * 销毁 * * Destroy * @internal */ public destroy(): void { const { graph } = this.context; graph.off(GraphEvent.AFTER_DRAW, this.addCommand); graph.off(GraphEvent.BATCH_START, this.initBatchCommand); graph.off(GraphEvent.BATCH_END, this.addCommand); this.emitter.off(); super.destroy(); this.undoStack = []; this.redoStack = []; } }