import { observable, action, runInAction } from 'mobx'; import { Without } from '../../Utils'; function getBatchName(target: any, key: string | symbol): string { const keyName = key.toString(); if (target && target.constructor && target.constructor.name) { return `${target.constructor.name}.${keyName}`; } return keyName; } function propertyDecorator(target: any, key: string | symbol) { Object.defineProperty(target, key, { configurable: true, enumerable: false, get: function () { return 5; }, set: function (value: any) { Object.defineProperty(this, key, { enumerable: false, writable: true, configurable: true, value: function (...args: any[]) { const batch = UndoManager.StartBatch(getBatchName(target, key)); try { return value.apply(this, args); } finally { batch.end(); } }, }); }, }); } export function undoBatch(target: any, key: string | symbol, descriptor?: TypedPropertyDescriptor): any; export function undoBatch(fn: (...args: any[]) => any): (...args: any[]) => any; export function undoBatch(target: any, key?: string | symbol, descriptor?: TypedPropertyDescriptor): any { if (!key) { return function () { const batch = UndoManager.StartBatch(''); try { return target.apply(undefined, arguments); } finally { batch.end(); } }; } if (!descriptor) { propertyDecorator(target, key); return; } const oldFunction = descriptor.value; descriptor.value = function (...args: any[]) { const batch = UndoManager.StartBatch(getBatchName(target, key)); try { return oldFunction.apply(this, args); } finally { batch.end(); } }; return descriptor; } export namespace UndoManager { export interface UndoEvent { undo: () => void; redo: () => void; prop: string; } type UndoBatch = UndoEvent[]; export let undoStack: UndoBatch[] = observable([]); export let redoStack: UndoBatch[] = observable([]); let currentBatch: UndoBatch | undefined; export let batchCounter = 0; let undoing = false; let tempEvents: UndoEvent[] | undefined = undefined; export function AddEvent(event: UndoEvent): void { if (currentBatch && batchCounter && !undoing) { currentBatch.push(event); tempEvents?.push(event); } } export function CanUndo(): boolean { return undoStack.length > 0; } export function CanRedo(): boolean { return redoStack.length > 0; } export function PrintBatches(): void { console.log('Open Undo Batches:'); GetOpenBatches().forEach(batch => console.log(batch.batchName)); } const openBatches: Batch[] = []; export function GetOpenBatches(): Without[] { return openBatches; } export function FilterBatches(fieldTypes: string[]) { const fieldCounts: { [key: string]: number } = {}; const lastStack = UndoManager.undoStack.slice(-1)[0]; //.lastElement(); if (lastStack) { lastStack.forEach(ev => fieldTypes.includes(ev.prop) && (fieldCounts[ev.prop] = (fieldCounts[ev.prop] || 0) + 1)); const fieldCount2: { [key: string]: number } = {}; runInAction( () => (UndoManager.undoStack[UndoManager.undoStack.length - 1] = lastStack.filter(ev => { if (fieldTypes.includes(ev.prop)) { fieldCount2[ev.prop] = (fieldCount2[ev.prop] || 0) + 1; if (fieldCount2[ev.prop] === 1 || fieldCount2[ev.prop] === fieldCounts[ev.prop]) return true; return false; } return true; })) ); } } export function TraceOpenBatches() { console.log(`Open batches:\n\t${openBatches.map(batch => batch.batchName).join('\n\t')}\n`); } export class Batch { private disposed: boolean = false; constructor(readonly batchName: string) { openBatches.push(this); } private dispose = (cancel: boolean) => { if (this.disposed) { throw new Error('Cannot dispose an already disposed batch'); } this.disposed = true; openBatches.splice(openBatches.indexOf(this)); return EndBatch(cancel); }; end = () => this.dispose(false); cancel = () => this.dispose(true); } export function StartBatch(batchName: string): Batch { // console.log("Start " + batchCounter + " " + batchName); batchCounter++; if (batchCounter > 0 && currentBatch === undefined) { currentBatch = []; } return new Batch(batchName); } const EndBatch = action((cancel: boolean = false) => { batchCounter--; // console.log("End " + batchCounter); if (batchCounter === 0 && currentBatch?.length) { // console.log("------ended----") if (!cancel) { undoStack.push(currentBatch); } redoStack.length = 0; currentBatch = undefined; return true; } return false; }); export function RunInTempBatch(fn: () => T) { tempEvents = []; try { const success = runInAction(fn); if (!success) UndoManager.UndoTempBatch(); return success; } finally { tempEvents = undefined; } } //TODO Make this return the return value export function RunInBatch(fn: () => T, batchName: string) { const batch = StartBatch(batchName); try { return runInAction(fn); } finally { batch.end(); } } export const UndoTempBatch = action(() => { if (tempEvents) { undoing = true; for (let i = tempEvents.length - 1; i >= 0; i--) { tempEvents[i].undo(); } undoing = false; } tempEvents = undefined; }); export const Undo = action(() => { if (undoStack.length === 0) { return; } const commands = undoStack.pop(); if (!commands) { return; } undoing = true; for (let i = commands.length - 1; i >= 0; i--) { commands[i].undo(); } undoing = false; redoStack.push(commands); }); export const Redo = action(() => { if (redoStack.length === 0) { return; } const commands = redoStack.pop(); if (!commands) { return; } undoing = true; for (const command of commands) { command.redo(); } undoing = false; undoStack.push(commands); }); }