/* Javascript State Machine Library - https://github.com/jakesgordon/javascript-state-machine Copyright (c) 2012, 2013, 2014, 2015, Jake Gordon and contributors Released under the MIT license - https://github.com/jakesgordon/javascript-state-machine/blob/master/LICENSE */ (function () { var StateMachine = { //--------------------------------------------------------------------------- VERSION: "2.4.0", //--------------------------------------------------------------------------- Result: { SUCCEEDED: 1, // the event transitioned successfully from one state to another NOTRANSITION: 2, // the event was successfull but no state transition was necessary CANCELLED: 3, // the event was cancelled by the caller in a beforeEvent callback PENDING: 4 // the event is asynchronous and the caller is in control of when the transition occurs }, Error: { INVALID_TRANSITION: 100, // caller tried to fire an event that was innapropriate in the current state PENDING_TRANSITION: 200, // caller tried to fire an event while an async transition was still pending INVALID_CALLBACK: 300 // caller provided callback function threw an exception }, WILDCARD: '*', ASYNC: 'async', //--------------------------------------------------------------------------- create: function(cfg, target) { var initial = (typeof cfg.initial == 'string') ? { state: cfg.initial } : cfg.initial; // allow for a simple string, or an object with { state: 'foo', event: 'setup', defer: true|false } var terminal = cfg.terminal || cfg['final']; var fsm = target || cfg.target || {}; var events = cfg.events || []; var callbacks = cfg.callbacks || {}; var map = {}; // track state transitions allowed for an event { event: { from: [ to ] } } var transitions = {}; // track events allowed from a state { state: [ event ] } var add = function(e) { var from = Array.isArray(e.from) ? e.from : (e.from ? [e.from] : [StateMachine.WILDCARD]); // allow 'wildcard' transition if 'from' is not specified map[e.name] = map[e.name] || {}; for (var n = 0 ; n < from.length ; n++) { transitions[from[n]] = transitions[from[n]] || []; transitions[from[n]].push(e.name); map[e.name][from[n]] = e.to || from[n]; // allow no-op transition if 'to' is not specified } if (e.to) transitions[e.to] = transitions[e.to] || []; }; if (initial) { initial.event = initial.event || 'startup'; add({ name: initial.event, from: 'none', to: initial.state }); } for(var n = 0 ; n < events.length ; n++) add(events[n]); for(var name in map) { if (map.hasOwnProperty(name)) fsm[name] = StateMachine.buildEvent(name, map[name]); } for(var name in callbacks) { if (callbacks.hasOwnProperty(name)) fsm[name] = callbacks[name] } fsm.current = 'none'; fsm.is = function(state) { return Array.isArray(state) ? (state.indexOf(this.current) >= 0) : (this.current === state); }; fsm.can = function(event) { return !this.transition && (map[event] !== undefined) && (map[event].hasOwnProperty(this.current) || map[event].hasOwnProperty(StateMachine.WILDCARD)); } fsm.cannot = function(event) { return !this.can(event); }; fsm.transitions = function() { return (transitions[this.current] || []).concat(transitions[StateMachine.WILDCARD] || []); }; fsm.isFinished = function() { return this.is(terminal); }; fsm.error = cfg.error || function(name, from, to, args, error, msg, e) { throw e || msg; }; // default behavior when something unexpected happens is to throw an exception, but caller can override this behavior if desired (see github issue #3 and #17) fsm.states = function() { return Object.keys(transitions).sort() }; if (initial && !initial.defer) fsm[initial.event](); return fsm; }, //=========================================================================== doCallback: function(fsm, func, name, from, to, args) { if (func) { try { return func.apply(fsm, [name, from, to].concat(args)); } catch(e) { return fsm.error(name, from, to, args, StateMachine.Error.INVALID_CALLBACK, "an exception occurred in a caller-provided callback function", e); } } }, beforeAnyEvent: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onbeforeevent'], name, from, to, args); }, afterAnyEvent: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onafterevent'] || fsm['onevent'], name, from, to, args); }, leaveAnyState: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onleavestate'], name, from, to, args); }, enterAnyState: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onenterstate'] || fsm['onstate'], name, from, to, args); }, changeState: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onchangestate'], name, from, to, args); }, beforeThisEvent: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onbefore' + name], name, from, to, args); }, afterThisEvent: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onafter' + name] || fsm['on' + name], name, from, to, args); }, leaveThisState: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onleave' + from], name, from, to, args); }, enterThisState: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onenter' + to] || fsm['on' + to], name, from, to, args); }, beforeEvent: function(fsm, name, from, to, args) { if ((false === StateMachine.beforeThisEvent(fsm, name, from, to, args)) || (false === StateMachine.beforeAnyEvent( fsm, name, from, to, args))) return false; }, afterEvent: function(fsm, name, from, to, args) { StateMachine.afterThisEvent(fsm, name, from, to, args); StateMachine.afterAnyEvent( fsm, name, from, to, args); }, leaveState: function(fsm, name, from, to, args) { var specific = StateMachine.leaveThisState(fsm, name, from, to, args), general = StateMachine.leaveAnyState( fsm, name, from, to, args); if ((false === specific) || (false === general)) return false; else if ((StateMachine.ASYNC === specific) || (StateMachine.ASYNC === general)) return StateMachine.ASYNC; }, enterState: function(fsm, name, from, to, args) { StateMachine.enterThisState(fsm, name, from, to, args); StateMachine.enterAnyState( fsm, name, from, to, args); }, //=========================================================================== buildEvent: function(name, map) { return function() { var from = this.current; var to = map[from] || (map[StateMachine.WILDCARD] != StateMachine.WILDCARD ? map[StateMachine.WILDCARD] : from) || from; var args = Array.prototype.slice.call(arguments); // turn arguments into pure array if (this.transition) return this.error(name, from, to, args, StateMachine.Error.PENDING_TRANSITION, "event " + name + " inappropriate because previous transition did not complete"); if (this.cannot(name)) return this.error(name, from, to, args, StateMachine.Error.INVALID_TRANSITION, "event " + name + " inappropriate in current state " + this.current); if (false === StateMachine.beforeEvent(this, name, from, to, args)) return StateMachine.Result.CANCELLED; if (from === to) { StateMachine.afterEvent(this, name, from, to, args); return StateMachine.Result.NOTRANSITION; } // prepare a transition method for use EITHER lower down, or by caller if they want an async transition (indicated by an ASYNC return value from leaveState) var fsm = this; this.transition = function() { fsm.transition = null; // this method should only ever be called once fsm.current = to; StateMachine.enterState( fsm, name, from, to, args); StateMachine.changeState(fsm, name, from, to, args); StateMachine.afterEvent( fsm, name, from, to, args); return StateMachine.Result.SUCCEEDED; }; this.transition.cancel = function() { // provide a way for caller to cancel async transition if desired (issue #22) fsm.transition = null; StateMachine.afterEvent(fsm, name, from, to, args); } var leave = StateMachine.leaveState(this, name, from, to, args); if (false === leave) { this.transition = null; return StateMachine.Result.CANCELLED; } else if (StateMachine.ASYNC === leave) { return StateMachine.Result.PENDING; } else { if (this.transition) // need to check in case user manually called transition() but forgot to return StateMachine.ASYNC return this.transition(); } }; } }; // StateMachine //=========================================================================== //====== // NODE //====== if (typeof exports !== 'undefined') { if (typeof module !== 'undefined' && module.exports) { exports = module.exports = StateMachine; } exports.StateMachine = StateMachine; } //============ // AMD/REQUIRE //============ else if (typeof define === 'function' && define.amd) { define(function(require) { return StateMachine; }); } //======== // BROWSER //======== else if (typeof window !== 'undefined') { window.StateMachine = StateMachine; } //=========== // WEB WORKER //=========== else if (typeof self !== 'undefined') { self.StateMachine = StateMachine; } }());