Fork me on GitHub
Show:

File: ../src/commands/commandmanager.js

define([
  'aeris/util',
  'aeris/events',
  'aeris/commands/abstractcommand',
  'aeris/promisequeue',
  'aeris/errors/invalidargumenterror',
  'aeris/errors/commandhistoryerror'
], function(_, Events, AbstractCommand, PromiseQueue, InvalidArgumentError, CommandHistoryError) {
  /**
   * CommandManager
   * Handles execution and history of commands,
   * and queueing of asynchronous commands.
   *
   * Example:
   * var cm = new CommandManager();
   * cm.executeCommand(new CommandOne());
   * cm.executeCommand(new CommandTwo());   // waits for CommandOne to resolve
   * cm.undo();                             // waits for CommandTwo to resolve, the undoes CommandTwo
   * cm.undo();                             // waits for CommandTwo to resolve undo, then undoes CommandOne
   * cm.redo();                             // waits for CommandOne to resolve undo, then executes CommandOne
   * // etc...
   *
   *
   *
   * Note that attempts to execute a command that is not
   * available will throw a CommandHistoryError.
   *
   * To address this, you can catch CommandHistoryErrors:
   *
   * try {
   *  cm.undo();
   * } catch(e) {
   *  if (e instanceof CommandHistoryError) {
   *    $('#undoBtn').attr('disabled', disabled')
   *    alert('Unable to undo command: ' e.message);
   *  }
   *  else {
   *    // something else went wrong :(
   *    throw e;
   *  }
   * }
   *
   * Or, you can check the availability of a command
   * before executing:
   *
   * if (cm.canUndo()) {
   *  cm.undo();
   * }
   * else {
   *   alert('You can\'t undo what hasn\'t been done.');
   * }
   *
   *
   *
   * @constructor
   * @class aeris.commands.CommandManager
   *
   * @param {Object=} opt_options
   * @param {aeris.PromiseQueue=} opt_options.queueManager
   */
  var CommandManager = function(opt_options) {
    opt_options || (opt_options = {});

    Events.call(this);

    /**
     * A list of executed commands
     * @type {Array.<aeris.commands.AbstractCommand>}
     * @private
     * @property executed_
     */
    this.executed_ = [];

    /**
     * A list of undone commands
     * @type {Array.<aeris.commands.AbstractCommand>}
     * @private
     * @property undone_
     */
    this.undone_ = [];


    this.queueManager_ = opt_options.queueManager || new PromiseQueue();


    /**
     * @event execute
     * @param {aeris.commands.AbstractCommand} command
     */
    /**
     * @event undo
     * @param {aeris.commands.AbstractCommand} command
     */
    /**
     * @event redo
     * @param {aeris.commands.AbstractCommand} command
     */
  };

  // Mixin Events
  _.extend(CommandManager.prototype, Events.prototype);


  /**
   * Executes the command
   *
   * @param {aeris.commands.AbstractCommand} command
   * @return {aeris.promise} Promise to resolve command execution.
   * @method executeCommand
   */
  CommandManager.prototype.executeCommand = function(command) {
    var promise;

    if (!(command instanceof AbstractCommand)) {
      throw new InvalidArgumentError('Invalid command.');
    }

    // Add the command to the queue
    promise = this.queueAndRun_(command.execute, command);

    // Add to execution history
    this.executed_.push(command);

    // Clear undone history, as they cannot be redone at this point
    this.undone_ = [];
    this.undone_.length = 0;

    this.trigger('execute', command);

    return promise;
  };


  /**
   * Undoes the last executed command.
   *
   * @return {aeris.Promise}
   * @method undo
   */
  CommandManager.prototype.undo = function() {
    var command, promise;

    // Because I keep passing in a callback here, instead of to .done()
    if (arguments.length) {
      throw new InvalidArgumentError('Undo does not expect any arguments.');
    }

    if (!this.canUndo()) {
      throw new CommandHistoryError('Unable to undo: no command has been executed.');
    }

    // Remove command from executed list
    command = this.executed_.pop();

    // Undo command
    promise = this.queueAndRun_(command.undo, command);

    // Add to undo history
    this.undone_.push(command);

    this.trigger('undo', command);

    return promise;
  };


  /**
   * Redoes the last executed command.
   *
   * @return {aeris.Promise}
   * @method redo
   */
  CommandManager.prototype.redo = function() {
    var command, promise;

    // Because I keep passing in a callback here, instead of to .done()
    if (arguments.length) {
      throw new InvalidArgumentError('Redo does not expect any arguments.');
    }

    if (!this.canRedo()) {
      throw new CommandHistoryError('Unable to redo: no command has been undone.');
    }

    // Remove command from undone list
    command = this.undone_.pop();

    // Redo (execute) command
    promise = this.queueAndRun_(command.execute, command);

    // Add to executed history
    this.executed_.push(command);

    this.trigger('redo', command);

    return promise;
  };


  /**
   * @return {boolean} Whether there is a command to undo.
   * @method canUndo
   */
  CommandManager.prototype.canUndo = function() {
    return this.executed_.length >= 1;
  };


  /**
   * @return {boolean} Whether there is a command to redo.
   * @method canRedo
   */
  CommandManager.prototype.canRedo = function() {
    return this.undone_.length >= 1;
  };


  /**
   * Add a command action to the PromiseQueue,
   * and make sure the queue is running.
   *
   * @param {function(): aeris.Promise} fn The action to add to the queue.
   * @param {aeris.commands.AbstractCommand} command The command to which the action belongs.
   * @private
   * @method queueAndRun_
   */
  CommandManager.prototype.queueAndRun_ = function(fn, command) {
    var promise;

    if (!(command instanceof AbstractCommand)) {
      throw new InvalidArgumentError('Command actions must be queued in the context of a Command.');
    }

    promise = this.queueManager_.queue(fn, command);

    if (!this.queueManager_.isRunning()) {
      this.queueManager_.dequeue();
    }

    return promise;
  };


  return CommandManager;
});