UNPKG

@mezzy/commands

Version:

A luxurious user experience framework, developed by your friends at Mezzanine.

291 lines (226 loc) 9.71 kB
import is from '@mezzy/is'; import Debug from '@mezzy/debug'; import { IPromise, Result } from '@mezzy/result'; import { Signal } from '@mezzy/signals'; import { Identifier } from '@mezzy/ids'; import { MapList } from '@mezzy/collections'; import { Queue } from '@mezzy/collections'; import Command from './command'; import CommandEventArgs from './commandEventArgs'; Debug.setCategoryMode('commands', false); export class CommandManager { constructor(isUndoEnabled:boolean = true) { this._isUndoEnabled = isUndoEnabled; } /*====================================================================* START: Properties *====================================================================*/ /** * A normalized value representing progress while running queued commands. * The value starts at 0 when run() is initially called, and progresses to 1 * once all the queued commands have been run and the command queue is empty. */ get progress():number { return this._progress; } private _progress:number = 0; /** * The command queue that holds any pending Commands that are waiting to be run. */ get pending():number { return this._pendingQueue.size; } //===== Private private _isUndoEnabled:boolean = true; private _commands:MapList<string, Command> = new MapList<string, Command>(); private _pendingQueue:Queue<Command> = new Queue<Command>(); private _undoQueue:Queue<Command> = new Queue<Command>(); private _redoQueue:Queue<Command> = new Queue<Command>(); private _isRunningAll:boolean = false; get runStartCount():number { return this._runStartCount; } private _runStartCount:number = 0; get lastReturnValue():any { return this._lastReturnValue; } private _lastReturnValue:any; /*====================================================================* START: Signals *====================================================================*/ /** * Dispatched immediately before each command is run. */ commandRunStart:Signal<CommandEventArgs> = new Signal<CommandEventArgs>(); /** * Dispatched immediately after each command has completed running. */ commandRunComplete:Signal<CommandEventArgs> = new Signal<CommandEventArgs>(); /*====================================================================* START: Methods *====================================================================*/ /*** * Register a command with the command manager that can later be retrieved by key. * If null or empty string is passed for key, a guid is generated for the ICommand key and returned as a string. */ register( scope:Object, runFunction:(...args:any[]) => any, undoFunction?:(...args:any[]) => any, name?:string ):string { if (is.empty(runFunction)) return; let command:Command = new Command(runFunction, undoFunction, scope); if (is.empty(name)) name = 'command_' + Identifier.getSessionUniqueInteger(); this._commands.set(name, command); return name; } unregister(name:string):void { this._commands.delete(name); } /** * Retrieve a stored command by key. */ get(name:string):Command { let command:Command = this._commands.get(name); if (is.empty(command)) { command = new Command(); this._commands.set(name, command); return command; } else return command; } /** * Add a command to the command queue. Once all commands have been added to the queue, * call run() to run them all in sequence. */ queue(command:Command, runArgs?:any[], undoArgs?:any[]):CommandManager { if (is.empty(command)) return; if (is.notEmpty(runArgs)) command.runArgs = runArgs; if (is.notEmpty(undoArgs)) command.undoArgs = undoArgs; this._pendingQueue.add(command); return this; } /** * Add the provided command (if any) to the queue with the specified arguments, then run all queued commands. * The returned result completes with the return value of the last command to be run from the queue. */ run(command?:(Command | string), args?:any[], undoArgs?:any[]):IPromise<any> { if (is.empty(command)) return this._runAll(); else { let commandFinal:Command; if (typeof command === 'string') commandFinal = this.get(command); else commandFinal = <Command>command; if (is.empty(commandFinal)) return Result.resolve(null); this.queue(commandFinal, args, undoArgs); return this._runAll(); } } /** * Undo the last command that was run. */ undo(...args:any[]):IPromise<any> { if (!this._isUndoEnabled || this._undoQueue.size < 1) return Result.resolve(null); let command:Command = this._undoQueue.deleteLast(); if (is.empty(command)) return Result.resolve(null); let result:Result<any> = command.undo.apply(command, args); result.then((returnValue:any) => { this._redoQueue.add(command); }); return result; } /** * Re-run the last command that was called from the undo queue. */ redo(...args:any[]):IPromise<any> { if (!this._isUndoEnabled || this._redoQueue.size < 1) return Result.resolve(null); let command:Command = this._redoQueue.deleteLast(); if (is.empty(command)) return Result.resolve(null); let result:Result<any> = command.run.apply(command, args); result.then((returnValue:any) => { let commandEventArgs:CommandEventArgs = new CommandEventArgs(command, 1, returnValue, args); commandEventArgs.isCommandComplete = true; this.commandRunComplete.dispatch(commandEventArgs); this._undoQueue.add(command); }); return result; } /** * Clear the pending, undo and redo queues. */ clearAllQueues():CommandManager { this._pendingQueue.clear(); this._undoQueue.clear(); this._redoQueue.clear(); return this; } /** * Clear the pending queue. */ clearPending():CommandManager { this._pendingQueue.clear(); return this; } //===== Private private _run(command:Command):IPromise<any> { let pending:number = this._pendingQueue.size + 1; let progressStart:number = 1 - (pending / this._runStartCount); let argsStart:CommandEventArgs = new CommandEventArgs(command, progressStart); if (this._isRunningAll) { argsStart.pending = this._pendingQueue.size + 1; argsStart.totalRunning = this._runStartCount; } this.commandRunStart.dispatch(argsStart); if (this._isUndoEnabled) { this._undoQueue.add(command); this._redoQueue.clear(); } let result:IPromise<any> = command.run(); result.then((returnValue:any) => { this._lastReturnValue = returnValue; let progressComplete:number = 1 - ((pending - 1) / this._runStartCount); let argsComplete:CommandEventArgs = new CommandEventArgs(command, progressComplete, returnValue); if (this._isRunningAll) { argsComplete.pending = this._pendingQueue.size; argsComplete.totalRunning = this._runStartCount; } argsComplete.isCommandComplete = true; this.commandRunComplete.dispatch(argsComplete); }); return result; } private _runNext():IPromise<any> { if (this._pendingQueue.size > 0) { let command:Command = this._pendingQueue.deleteFirst(); if (is.empty(command)) { Debug.error('Expected commands on pending stack, but found none.', 'CommandManager.runNext(...)'); return Result.resolve<any>(null); } Debug.log( command.message + (is.empty(command.message) ? '' : ' ') + 'p = ' + this._progress.toString() + ', remaining: ' + this._pendingQueue.size.toString() + ', total: ' + this._runStartCount.toString(), null, 'commands' ); return this._run(command); } else return Result.resolve<any>(null); } /** * The returned result completes with the return value of the last command to be run from the queue. */ private _runAll():IPromise<any> { if (!this._isRunningAll) { this._isRunningAll = true; this._progress = 0; this._resultRunAll = new Result<any>(); this._lastReturnValue = null; this._runStartCount = this._pendingQueue.size; if (this._runStartCount > 0) this._runAllNext(); else this._resultRunAll.resolve(null); } return this._resultRunAll; } private _resultRunAll:Result<any>; private _runAllNext():void { if (this._pendingQueue.size > 0) { this._runNext().then((result:any) => { this._progress = 1 - (this._pendingQueue.size / this._runStartCount); this._runAllNext(); }); } else { this._isRunningAll = false; this._progress = 1; /// All pending commands have been run in the current batch. /// So, complete with the result of the last command run. this._resultRunAll.resolve(this._lastReturnValue); } } } // End class export default CommandManager;