/* eslint-disable prefer-spread */ /* eslint-disable no-use-before-define */ import { action, observable, runInAction } from 'mobx'; import { Without } from '../../Utils'; import { RichTextField } from '../../fields/RichTextField'; import { SnappingManager } from './SnappingManager'; function getBatchName(target: any, key: string | symbol): string { const keyName = key.toString(); if (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 undoable(fn: (...args: any[]) => any, batchName: string): (...args: any[]) => any { return function (...fargs) { const batch = UndoManager.StartBatch(batchName); try { // eslint-disable-next-line prefer-rest-params return fn.apply(undefined, fargs); } finally { batch.end(); } }; } export function undoBatch(target: any, key: string | symbol, descriptor?: TypedPropertyDescriptor): any; // eslint-disable-next-line no-redeclare export function undoBatch(fn: (...args: any[]) => any): (...args: any[]) => any; // eslint-disable-next-line no-redeclare export function undoBatch(target: any, key?: string | symbol, descriptor?: TypedPropertyDescriptor): any { if (!key) { return function (...fargs: any[]) { const batch = UndoManager.StartBatch(''); try { return target.apply(undefined, fargs); } finally { batch.end(); } }; } if (!descriptor) { propertyDecorator(target, key); return undefined; } 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[]; let currentBatch: UndoBatch | undefined; let undoing = false; let tempEvents: UndoEvent[] | undefined; export const undoStackNames: string[] = observable([]); export const redoStackNames: string[] = observable([]); export const undoStack: UndoBatch[] = observable([]); export const redoStack: UndoBatch[] = observable([]); export const batchCounter = observable.box(0); let _fieldPrinter: (val: any) => string = val => val?.toString(); export function SetFieldPrinter(printer: (val: any) => string) { _fieldPrinter = printer; } export function AddEvent(event: UndoEvent, value?: any): void { if (currentBatch && batchCounter.get() && !undoing) { SnappingManager.PrintToConsole && console.log( ' '.slice(0, batchCounter.get()) + 'UndoEvent : ' + event.prop + ' = ' + // prettier-ignore (value instanceof RichTextField ? value.Text : value instanceof Array ? value.map(_fieldPrinter).join(',') : _fieldPrinter(value)) ); 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) { console.log('WARNING: undo batch already disposed'); return false; } this.disposed = true; openBatches.splice(openBatches.indexOf(this)); return EndBatch(this.batchName, cancel); }; end = () => this.dispose(false); cancel = () => this.dispose(true); } export function StartBatch(batchName: string): Batch { SnappingManager.PrintToConsole && console.log(' '.slice(0, batchCounter.get()) + 'Start ' + batchCounter + ' ' + batchName); runInAction(() => batchCounter.set(batchCounter.get() + 1)); if (currentBatch === undefined) { currentBatch = []; } return new Batch(batchName); } const EndBatch = action((batchName: string, cancel: boolean = false) => { runInAction(() => batchCounter.set(batchCounter.get() - 1)); SnappingManager.PrintToConsole && console.log(' '.slice(0, batchCounter.get()) + 'End ' + batchName + ' (' + (currentBatch?.length ?? 0) + ')'); if (batchCounter.get() === 0 && currentBatch?.length) { if (!cancel) { undoStack.push(currentBatch); undoStackNames.push(batchName ?? '???'); } redoStackNames.length = 0; redoStack.length = 0; currentBatch = undefined; return true; } return false; }); export function StartTempBatch() { tempEvents = []; } export function EndTempBatch(success: boolean) { UndoManager.UndoTempBatch(success); } // 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((success: any) => { if (tempEvents && !success) { undoing = true; for (let i = tempEvents.length - 1; i >= 0; i--) { currentBatch?.includes(tempEvents[i]) && currentBatch.splice(currentBatch.indexOf(tempEvents[i])); tempEvents[i].undo(); } undoing = false; } tempEvents = undefined; }); export const Undo = action(() => { if (undoStack.length === 0) { return; } const names = undoStackNames.pop(); const commands = undoStack.pop(); if (!commands) { return; } undoing = true; // eslint-disable-next-line prettier/prettier commands .slice() .reverse() .forEach(command => command.undo()); undoing = false; redoStackNames.push(names ?? '???'); redoStack.push(commands); }); export const Redo = action(() => { if (redoStack.length === 0) { return; } const names = redoStackNames.pop(); const commands = redoStack.pop(); if (!commands) { return; } undoing = true; commands.forEach(command => command.redo()); undoing = false; undoStackNames.push(names ?? '???'); undoStack.push(commands); }); }