Skip to content

Instantly share code, notes, and snippets.

@josephferris
Created December 11, 2016 22:10
Show Gist options
  • Save josephferris/0ee945d3392e49b453225e0f207461d4 to your computer and use it in GitHub Desktop.
Save josephferris/0ee945d3392e49b453225e0f207461d4 to your computer and use it in GitHub Desktop.
Module to encapsulate method invocation
import { Observable } from './Observable'
import { Promise } from 'es6-promise'
export class Command {
constructor(options){
this.invoker = undefined
this.options = typeof options !== 'undefined' ? options : {}
this.async = false
}
execute(){
}
validate(actionsPerformed, actionsToPerform){
}
}
export class UndoableCommand extends Command {
constructor(options){
super(options)
}
canUndo(){
return true;
}
undo(){
}
}
export class CommandInvoker extends Observable {
constructor(receiver){
super()
this.commandChain = []
this.commandStack = []
this.redoStack = []
this.receiver = receiver
}
// <summary>
// Add a command to the command chain
// </summary>
setCommand(command){
command.receiver = this.receiver
command.invoker = this;
this.commandChain.push(command);
try {
command.validate(this.commandStack, this.commandChain)
}
catch(e){
this.commandChain.pop()
throw Error(e)
}
return command;
}
// <summary>
// Removes a command from the command chain
// </summary>
unsetCommand(command){
let index = this.commandChain.indexOf(command);
if (index >= 0){
this.commandChain.splice(index, 1);
}
}
// <summary>
// Executes a promise for execute a single command
// </summary>
apply(command){
this.setCommand(command)
return this.execute()
}
// <summary>
// Starts execution of the command chain
// </summary>
execute(){
return new Promise((resolveCallback, rejectCallback) => {
this.inProgress = true
this.on('onNextCommand', this.executeNext)
this.on('onCommandComplete', this.onCommandComplete)
this.on('onComplete', this.onComplete.bind(this, resolveCallback))
this.on('onCommandFailure', this.onCommandFailure.bind(this, rejectCallback))
this.trigger('onNextCommand')
})
}
// <summary>
// Executes next command from the command chain
// </summary>
executeNext(){
if (this.commandChain.length === 0){
this.trigger('onComplete');
return
}
let action = this.commandChain.shift()
try {
action.execute()
if (!action.async){
this.trigger('onCommandComplete', action)
this.trigger('onNextCommand')
}
}
catch(e){
this.trigger('onCommandFailure', action, e)
}
}
onCommandFailure(rejectCallback, command, error){
this.clear()
if (typeof rejectCallback !== 'undefined' && typeof rejectCallback === 'function'){
rejectCallback(error)
}
}
// <summary>
// Event triggered when a command is complete.
// </summary>
onCommandComplete(command){
if (typeof command === 'undefined' || command === null)
return
// a new command was executed or history was changed
if (this.redoStack.indexOf(command) < 0 || command !== this.redoStack.pop()){
this.redoStack.splice(0, this.redoStack.length)
}
// Executed successfully
if (this.commandStack.indexOf(command) < 0){
// Push it into the command stack
this.commandStack.push(command)
}
this.trigger('onNextCommand')
}
// <summary>
// Event triggered when there are not more commands to execute in the command chain
// </summary>
onComplete(resolveCallback){
this.clear()
if (typeof resolveCallback !== 'undefined' && typeof resolveCallback === 'function'){
resolveCallback()
}
}
// <summary>
// Clear the command chain
// </summary>
clear(){
this.off('onNextCommand', this.executeNext)
this.off('onCommandComplete', this.onCommandComplete)
this.off('onComplete', this.onComplete)
this.off('onCommandFailure', this.onCommandFailure)
this.commandChain.splice(0, this.commandChain.length)
this.inProgress = false
}
// <summary>
// Returns true if an undoable action is available to undo.
// </summary>
canUndo(){
return !this.inProgress &&
this.commandStack.length > 0 &&
this.commandStack[this.commandStack.length-1] instanceof UndoableCommand &&
this.commandStack[this.commandStack.length-1].canUndo();
}
// <summary>
// Undo the last action, if undoable
// </summary>
undo(){
return new Promise((resolveCallback, rejectCallback) => {
this.inProgress = true
this.on('onUndone', this.onUndone.bind(this, resolveCallback))
this.on('onUndoFailed', this.onUndoFailed.bind(this, rejectCallback))
this.undoNext()
})
}
canRedo(){
return !this.inProgress && this.redoStack.length > 0
}
// <summary>
// Undo the last action, if posible
// </summary>
redo(){
if (this.redoStack.length === 0){
throw Error('Cannot redo')
}
return this.apply(this.redoStack[this.redoStack.length - 1])
}
// <summary>
// Undo the next undoable action, if undoable
// </summary>
undoNext(){
if(!this.canUndo()){
return
}
let action = this.commandStack.pop()
try {
action.undo()
if (!action.async){
this.trigger('onUndone', action)
}
}
catch(e) {
this.commandStack.push(action)
this.trigger('onUndoFailed', action, e)
}
}
// <summary>
// Event triggered when an action was undone
// </summary>
onUndone(resolveCallback, command){
this.off('onUndone', this.onUndone)
this.off('onUndoFailed', this.onUndoFailed)
if (typeof command !== 'undefined' && command !== null){
this.redoStack.push(command)
}
this.inProgress = false
if (typeof resolveCallback !== 'undefined' && typeof resolveCallback === 'function'){
resolveCallback()
}
}
onUndoFailed(rejectCallback, command, error){
this.off('onUndone', this.onUndone)
this.off('onUndoFailed', this.onUndoFailed)
this.inProgress = false
if (typeof rejectCallback !== 'undefined' && typeof rejectCallback === 'function'){
rejectCallback(error)
}
}
// <summary>
// Clear all actions peformed and to be performed. Clear the storage service
// </summary>
reset(){
this.redoStack.splice(0, this.redoStack.length)
this.commandStack.splice(0, this.commandStack.length)
this.clear()
}
// <summary>
// Find the latest action performed that meets the criteria (callback)
// </summary>
findLastAction(callback){
return this.commandStack.slice().reverse().find(callback)
}
// <summary>
// Find all actions performed that meets the criteria (callback)
// </summary>
findActions(callback){
return this.commandStack.filter(callback)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment