diff --git a/.ackrc b/.ackrc
new file mode 100644
index 0000000..df1e9a8
--- /dev/null
+++ b/.ackrc
@@ -0,0 +1,4 @@
+--ignore-dir=coverage
+--ignore-dir=node_modules
+--ignore-dir=.nyc_output
+
diff --git a/.gitignore b/.gitignore
index 3c3629e..89d2c73 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,4 @@
node_modules
+coverage
+.nyc_output
+*.swp
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..c86adcc
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,5 @@
+language: node_js
+node_js:
+ - '4'
+ - '6'
+sudo: false
diff --git a/LICENSE b/LICENSE
index 01f6314..b01dd40 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2012, 2013, 2014, 2015, 2016, Jake Gordon and contributors
+Copyright (c) 2012, 2013, 2014, 2015, 2016, 2017, 2018, Jake Gordon and contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index 8020ce1..a2c4add 100644
--- a/README.md
+++ b/README.md
@@ -1,367 +1,148 @@
-Javascript Finite State Machine (v2.3.5)
-========================================
+# Javascript State Machine
-This standalone javascript micro-framework provides a finite state machine for your pleasure.
+[](https://badge.fury.io/js/javascript-state-machine)
+[](https://travis-ci.org/jakesgordon/javascript-state-machine)
- * You can find the [code here](https://github.com/jakesgordon/javascript-state-machine)
- * You can find a [description here](http://codeincomplete.com/posts/2013/1/26/javascript_state_machine_v2_2_0/)
- * You can find a [working demo here](http://codeincomplete.com/posts/2011/8/19/javascript_state_machine_v2/example/)
+A library for finite state machines.
-This library has also been ported to:
+
- * [Go](https://github.com/looplab/fsm) by @maxpersson
- * [Python](https://github.com/oxplot/fysom) by @oxplot
+
-Download
-========
+### NOTE for existing users
-You can download [state-machine.js](https://github.com/jakesgordon/javascript-state-machine/raw/master/state-machine.js),
-or the [minified version](https://github.com/jakesgordon/javascript-state-machine/raw/master/state-machine.min.js)
+> **VERSION 3.0** Is a significant rewrite from earlier versions.
+ Existing 2.x users should be sure to read the [Upgrade Guide](docs/upgrading-from-v2.md).
-Alternatively:
+
- git clone git@github.com:jakesgordon/javascript-state-machine
+# Installation
+In a browser:
- * All code is in state-machine.js
- * Minified version provided in state-machine.min.js
- * No 3rd party library is required
- * Demo can be found in /index.html
- * QUnit (browser) tests can be found in /test/index.html
- * QUnit (headless) tests can be run with "node test/runner.js" (after installing node-qunit with "npm install")
+```html
+
+```
-Usage
-=====
+> after downloading the [source](dist/state-machine.js) or the [minified version](dist/state-machine.min.js)
-Include `state-machine.js` in your web application, or, for nodejs `require("javascript-state-machine.js")`.
+Using npm:
-In its simplest form, create a standalone state machine using:
+```shell
+ npm install --save-dev javascript-state-machine
+```
- var fsm = StateMachine.create({
- initial: 'green',
- events: [
- { name: 'warn', from: 'green', to: 'yellow' },
- { name: 'panic', from: 'yellow', to: 'red' },
- { name: 'calm', from: 'red', to: 'yellow' },
- { name: 'clear', from: 'yellow', to: 'green' }
- ]});
+In Node.js:
-... will create an object with a method for each event:
+```javascript
+ var StateMachine = require('javascript-state-machine');
+```
- * fsm.warn() - transition from 'green' to 'yellow'
- * fsm.panic() - transition from 'yellow' to 'red'
- * fsm.calm() - transition from 'red' to 'yellow'
- * fsm.clear() - transition from 'yellow' to 'green'
+# Usage
-along with the following members:
+A state machine can be constructed using:
- * fsm.current - contains the current state
- * fsm.is(s) - return true if state `s` is the current state
- * fsm.can(e) - return true if event `e` can be fired in the current state
- * fsm.cannot(e) - return true if event `e` cannot be fired in the current state
- * fsm.transitions() - return list of events that are allowed from the current state
+```javascript
+ var fsm = new StateMachine({
+ init: 'solid',
+ transitions: [
+ { name: 'melt', from: 'solid', to: 'liquid' },
+ { name: 'freeze', from: 'liquid', to: 'solid' },
+ { name: 'vaporize', from: 'liquid', to: 'gas' },
+ { name: 'condense', from: 'gas', to: 'liquid' }
+ ],
+ methods: {
+ onMelt: function() { console.log('I melted') },
+ onFreeze: function() { console.log('I froze') },
+ onVaporize: function() { console.log('I vaporized') },
+ onCondense: function() { console.log('I condensed') }
+ }
+ });
+```
-Multiple 'from' and 'to' states for a single event
-==================================================
+... which creates an object with a current state property:
-If an event is allowed **from** multiple states, and always transitions to the same
-state, then simply provide an array of states in the `from` attribute of an event. However,
-if an event is allowed from multiple states, but should transition **to** a different
-state depending on the current state, then provide multiple event entries with
-the same name:
+ * `fsm.state`
- var fsm = StateMachine.create({
- initial: 'hungry',
- events: [
- { name: 'eat', from: 'hungry', to: 'satisfied' },
- { name: 'eat', from: 'satisfied', to: 'full' },
- { name: 'eat', from: 'full', to: 'sick' },
- { name: 'rest', from: ['hungry', 'satisfied', 'full', 'sick'], to: 'hungry' },
- ]});
+... methods to transition to a different state:
-This example will create an object with 2 event methods:
+ * `fsm.melt()`
+ * `fsm.freeze()`
+ * `fsm.vaporize()`
+ * `fsm.condense()`
- * fsm.eat()
- * fsm.rest()
+... observer methods called automatically during the lifecycle of a transition:
-The `rest` event will always transition to the `hungry` state, while the `eat` event
-will transition to a state that is dependent on the current state.
+ * `onMelt()`
+ * `onFreeze()`
+ * `onVaporize()`
+ * `onCondense()`
->> NOTE: The `rest` event could use a wildcard '*' for the 'from' state if it should be
-allowed from any current state.
+... along with the following helper methods:
->> NOTE: The `rest` event in the above example can also be specified as multiple events with
-the same name if you prefer the verbose approach.
+ * `fsm.is(s)` - return true if state `s` is the current state
+ * `fsm.can(t)` - return true if transition `t` can occur from the current state
+ * `fsm.cannot(t)` - return true if transition `t` cannot occur from the current state
+ * `fsm.transitions()` - return list of transitions that are allowed from the current state
+ * `fsm.allTransitions()` - return list of all possible transitions
+ * `fsm.allStates()` - return list of all possible states
-Callbacks
-=========
+# Terminology
-4 types of callback are available by attaching methods to your StateMachine using the following naming conventions:
+A state machine consists of a set of [**States**](docs/states-and-transitions.md)
- * `onbeforeEVENT` - fired before the event
- * `onleaveSTATE` - fired when leaving the old state
- * `onenterSTATE` - fired when entering the new state
- * `onafterEVENT` - fired after the event
+ * solid
+ * liquid
+ * gas
->> (using your **specific** EVENT and STATE names)
+A state machine changes state by using [**Transitions**](docs/states-and-transitions.md)
-For convenience, the 2 most useful callbacks can be shortened:
+ * melt
+ * freeze
+ * vaporize
+ * condense
- * `onEVENT` - convenience shorthand for `onafterEVENT`
- * `onSTATE` - convenience shorthand for `onenterSTATE`
+A state machine can perform actions during a transition by observing [**Lifecycle Events**](docs/lifecycle-events.md)
-In addition, 4 general-purpose callbacks can be used to capture **all** event and state changes:
+ * onBeforeMelt
+ * onAfterMelt
+ * onLeaveSolid
+ * onEnterLiquid
+ * ...
- * `onbeforeevent` - fired before *any* event
- * `onleavestate` - fired when leaving *any* state
- * `onenterstate` - fired when entering *any* state
- * `onafterevent` - fired after *any* event
+A state machine can also have arbitrary [**Data and Methods**](docs/data-and-methods.md).
-All callbacks will be passed the same arguments:
+Multiple instances of a state machine can be created using a [**State Machine Factory**](docs/state-machine-factory.md).
- * **event** name
- * **from** state
- * **to** state
- * _(followed by any arguments you passed into the original event method)_
+# Documentation
-Callbacks can be specified when the state machine is first created:
+Read more about
- var fsm = StateMachine.create({
- initial: 'green',
- events: [
- { name: 'warn', from: 'green', to: 'yellow' },
- { name: 'panic', from: 'yellow', to: 'red' },
- { name: 'calm', from: 'red', to: 'yellow' },
- { name: 'clear', from: 'yellow', to: 'green' }
- ],
- callbacks: {
- onpanic: function(event, from, to, msg) { alert('panic! ' + msg); },
- onclear: function(event, from, to, msg) { alert('thanks to ' + msg); },
- ongreen: function(event, from, to) { document.body.className = 'green'; },
- onyellow: function(event, from, to) { document.body.className = 'yellow'; },
- onred: function(event, from, to) { document.body.className = 'red'; },
- }
- });
+ * [States and Transitions](docs/states-and-transitions.md)
+ * [Data and Methods](docs/data-and-methods.md)
+ * [Lifecycle Events](docs/lifecycle-events.md)
+ * [Asynchronous Transitions](docs/async-transitions.md)
+ * [Initialization](docs/initialization.md)
+ * [Error Handling](docs/error-handling.md)
+ * [State History](docs/state-history.md)
+ * [Visualization](docs/visualization.md)
+ * [State Machine Factory](docs/state-machine-factory.md)
+ * [Upgrading from 2.x](docs/upgrading-from-v2.md)
+
+# Contributing
- fsm.panic('killer bees');
- fsm.clear('sedatives in the honey pots');
- ...
+You can [Contribute](docs/contributing.md) to this project with issues or pull requests.
-Additionally, they can be added and removed from the state machine at any time:
+# Release Notes
- fsm.ongreen = null;
- fsm.onyellow = null;
- fsm.onred = null;
- fsm.onenterstate = function(event, from, to) { document.body.className = to; };
+See [RELEASE NOTES](RELEASE_NOTES.md) file.
+# License
-The order in which callbacks occur is as follows:
+See [MIT LICENSE](https://github.com/jakesgordon/javascript-state-machine/blob/master/LICENSE) file.
->> assume event **go** transitions from **red** state to **green**
-
- * `onbeforego` - specific handler for the **go** event only
- * `onbeforeevent` - generic handler for all events
- * `onleavered` - specific handler for the **red** state only
- * `onleavestate` - generic handler for all states
- * `onentergreen` - specific handler for the **green** state only
- * `onenterstate` - generic handler for all states
- * `onaftergo` - specific handler for the **go** event only
- * `onafterevent` - generic handler for all events
-
->> NOTE: the legacy `onchangestate` handler has been deprecated and will be removed in a future version
-
-You can affect the event in 3 ways:
-
- * return `false` from an `onbeforeEVENT` handler to cancel the event.
- * return `false` from an `onleaveSTATE` handler to cancel the event.
- * return `ASYNC` from an `onleaveSTATE` handler to perform an asynchronous state transition (see next section)
-
-Asynchronous State Transitions
-==============================
-
-Sometimes, you need to execute some asynchronous code during a state transition and ensure the
-new state is not entered until your code has completed.
-
-A good example of this is when you transition out of a `menu` state, perhaps you want to gradually
-fade the menu away, or slide it off the screen and don't want to transition to your `game` state
-until after that animation has been performed.
-
-You can now return `StateMachine.ASYNC` from your `onleavestate` handler and the state machine
-will be _'put on hold'_ until you are ready to trigger the transition using the new `transition()`
-method.
-
-For example, using jQuery effects:
-
- var fsm = StateMachine.create({
-
- initial: 'menu',
-
- events: [
- { name: 'play', from: 'menu', to: 'game' },
- { name: 'quit', from: 'game', to: 'menu' }
- ],
-
- callbacks: {
-
- onentermenu: function() { $('#menu').show(); },
- onentergame: function() { $('#game').show(); },
-
- onleavemenu: function() {
- $('#menu').fadeOut('fast', function() {
- fsm.transition();
- });
- return StateMachine.ASYNC; // tell StateMachine to defer next state until we call transition (in fadeOut callback above)
- },
-
- onleavegame: function() {
- $('#game').slideDown('slow', function() {
- fsm.transition();
- };
- return StateMachine.ASYNC; // tell StateMachine to defer next state until we call transition (in slideDown callback above)
- }
-
- }
- });
-
->> _NOTE: If you decide to cancel the ASYNC event, you can call `fsm.transition.cancel();`
-
-State Machine Classes
-=====================
-
-You can also turn all instances of a _class_ into an FSM by applying
-the state machine functionality to the prototype, including your callbacks
-in your prototype, and providing a `startup` event for use when constructing
-instances:
-
- MyFSM = function() { // my constructor function
- this.startup();
- };
-
- MyFSM.prototype = {
-
- onpanic: function(event, from, to) { alert('panic'); },
- onclear: function(event, from, to) { alert('all is clear'); },
-
- // my other prototype methods
-
- };
-
- StateMachine.create({
- target: MyFSM.prototype,
- events: [
- { name: 'startup', from: 'none', to: 'green' },
- { name: 'warn', from: 'green', to: 'yellow' },
- { name: 'panic', from: 'yellow', to: 'red' },
- { name: 'calm', from: 'red', to: 'yellow' },
- { name: 'clear', from: 'yellow', to: 'green' }
- ]});
-
-
-This should be easy to adjust to fit your appropriate mechanism for object construction.
-
->> _NOTE: the `startup` event can be given any name, but it must be present in some form to
- ensure that each instance constructed is initialized with its own unique `current` state._
-
-Initialization Options
-======================
-
-How the state machine should initialize can depend on your application requirements, so
-the library provides a number of simple options.
-
-By default, if you don't specify any initial state, the state machine will be in the `'none'`
-state and you would need to provide an event to take it out of this state:
-
- var fsm = StateMachine.create({
- events: [
- { name: 'startup', from: 'none', to: 'green' },
- { name: 'panic', from: 'green', to: 'red' },
- { name: 'calm', from: 'red', to: 'green' },
- ]});
- alert(fsm.current); // "none"
- fsm.startup();
- alert(fsm.current); // "green"
-
-If you specify the name of your initial state (as in all the earlier examples), then an
-implicit `startup` event will be created for you and fired when the state machine is constructed.
-
- var fsm = StateMachine.create({
- initial: 'green',
- events: [
- { name: 'panic', from: 'green', to: 'red' },
- { name: 'calm', from: 'red', to: 'green' },
- ]});
- alert(fsm.current); // "green"
-
-If your object already has a `startup` method you can use a different name for the initial event
-
- var fsm = StateMachine.create({
- initial: { state: 'green', event: 'init' },
- events: [
- { name: 'panic', from: 'green', to: 'red' },
- { name: 'calm', from: 'red', to: 'green' },
- ]});
- alert(fsm.current); // "green"
-
-Finally, if you want to wait to call the initial state transition event until a later date you
-can `defer` it:
-
- var fsm = StateMachine.create({
- initial: { state: 'green', event: 'init', defer: true },
- events: [
- { name: 'panic', from: 'green', to: 'red' },
- { name: 'calm', from: 'red', to: 'green' },
- ]});
- alert(fsm.current); // "none"
- fsm.init();
- alert(fsm.current); // "green"
-
-Of course, we have now come full circle, this last example is pretty much functionally the
-same as the first example in this section where you simply define your own startup event.
-
-So you have a number of choices available to you when initializing your state machine.
-
->> _IMPORTANT NOTE: if you are using the pattern described in the previous section "State Machine
- Classes", and wish to declare an `initial` state in this manner, you MUST use the `defer: true`
- attribute and manually call the starting event in your constructor function. This will ensure
- that each instance gets its own unique `current` state, rather than an (unwanted) shared
- `current` state on the prototype object itself._
-
-Handling Failures
-======================
-
-By default, if you try to call an event method that is not allowed in the current state, the
-state machine will throw an exception. If you prefer to handle the problem yourself, you can
-define a custom `error` handler:
-
- var fsm = StateMachine.create({
- initial: 'green',
- error: function(eventName, from, to, args, errorCode, errorMessage) {
- return 'event ' + eventName + ' was naughty :- ' + errorMessage;
- },
- events: [
- { name: 'panic', from: 'green', to: 'red' },
- { name: 'calm', from: 'red', to: 'green' },
- ]});
- alert(fsm.calm()); // "event calm was naughty :- event not allowed in current state green"
-
-Release Notes
-=============
-
-See [RELEASE NOTES](https://github.com/jakesgordon/javascript-state-machine/blob/master/RELEASE_NOTES.md) file.
-
-License
-=======
-
-See [LICENSE](https://github.com/jakesgordon/javascript-state-machine/blob/master/LICENSE) file.
-
-Contact
-=======
+# Contact
If you have any ideas, feedback, requests or bug reports, you can reach me at
-[jake@codeincomplete.com](mailto:jake@codeincomplete.com), or via
-my website: [Code inComplete](http://codeincomplete.com/)
-
-
-
-
-
+[jakesgordon@gmail.com](mailto:jakesgordon@gmail.com), or via
+my website: [jakesgordon.com](https://jakesgordon.com/)
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 7e9fd70..67de268 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,3 +1,50 @@
+Version 3.1.0 (July 12th 2018)
+------------------------------
+
+ * Changed back to MIT license
+
+Version 3.0.1 (June 10th 2017)
+------------------------------
+
+ * First 3.x release - see 3.0.0-rc.1 release notes below
+
+ * fix issue #109 - rejection from async lifecycle method does not reject transitions promise
+ * fix issue #106 - async transition: forward resolved value
+ * fix issue #107 - lifecycle event name breaks for all uppercase
+
+Version 3.0.0-rc.1 (January 10 2017)
+------------------------------------
+
+**IMPORTANT NOTE**: this version includes **breaking changes** that will require code updates.
+
+Please read [UPGRADING FROM 2.x](docs/upgrading-from-v2.md) for details. Highlights include:
+
+ * Improved Construction.
+ * Arbitrary Data and Methods.
+ * Observable Transitions
+ * Conditional Transitions
+ * Promise-based Asynchronous Transitions
+ * Improved Transition Lifecycle Events
+ * State History
+ * Visualization
+ * Webpack build system
+ * ...
+
+
+
+
+Version 2.4.0 (November 20 2016)
+--------------------------------
+
+ * added npm install instructions to readme
+ * fix for javascript error when running in jasmine/node (issue #88)
+ * exclude build files from bower install (pull request #75)
+ * ensure WILDCARD events are included in list of available transitions() (issue #93)
+ * fix FSM getting stuck into "*" state when using double wildcard (issue #64)
+ * function (fsm.states) returning list of all available states in the machine would help automated testing (issue #54)
+ * state machine hides callback exceptions (issue #62)
+ * replaced (dev dependency) YUI compressor with uglify-js for building minified version
+
Version 2.3.5 (January 20 2014)
-------------------------------
@@ -34,7 +81,7 @@ Version 2.2.0 (January 26th 2013)
* Added generic state callbacks 'onleavestate' and 'onenterstate' (issue #28)
* Fixed 'undefined' event return codes (issue #34) - pull from gentooboontoo (thanks!)
* Allow async event transition to be cancelled (issue #22)
- * [read more...](http://codeincomplete.com/posts/2013/1/26/javascript_state_machine_v2_2_0/)
+ * [read more...](https://jakesgordon.com/writing/javascript-state-machine-v2-2-0/)
Version 2.1.0 (January 7th 2012)
--------------------------------
@@ -57,14 +104,14 @@ Version 2.0.0 (August 19th 2011)
* added a generic `onchangestate(event,from,to)` callback to detect all state changes with a single function.
* allow callbacks to be declared at creation time (instead of having to attach them afterwards)
* renamed 'hooks' => 'callbacks'
- * [read more...](http://codeincomplete.com/posts/2011/8/19/javascript_state_machine_v2/)
+ * [read more...](https://jakesgordon.com/writing/javascript-state-machine-v2/)
Version 1.2.0 (June 21st 2011)
------------------------------
* allows the same event to transition to different states, depending on the current state (see 'Multiple...' section in README.md)
- * [read more...](http://codeincomplete.com/posts/2011/6/21/javascript_state_machine_v1_2_0/)
+ * [read more...](https://jakesgordon.com/writing/javascript-state-machine-v1-2-0/)
Version 1.0.0 (June 1st 2011)
-----------------------------
* initial version
- * [read more...](http://codeincomplete.com/posts/2011/6/1/javascript_state_machine/)
+ * [read more...](https://jakesgordon.com/writing/javascript-state-machine/)
diff --git a/Rakefile b/Rakefile
deleted file mode 100644
index beb8702..0000000
--- a/Rakefile
+++ /dev/null
@@ -1,8 +0,0 @@
-
-desc "create minified version of state-machine.js"
-task :minify do
- require File.expand_path(File.join(File.dirname(__FILE__), 'minifier/minifier'))
- Minifier.enabled = true
- Minifier.minify('state-machine.js')
-end
-
diff --git a/bin/examples b/bin/examples
new file mode 100755
index 0000000..e726661
--- /dev/null
+++ b/bin/examples
@@ -0,0 +1,60 @@
+#!/usr/bin/env node
+
+//=================================================================================================
+//
+// This script is used to regenerate the example visualizations
+//
+//=================================================================================================
+
+var fs = require('fs'),
+ path = require('path'),
+ child = require('child_process');
+
+//-------------------------------------------------------------------------------------------------
+
+fs.readdirSync('examples')
+ .filter(function(file) { return path.extname(file) === ".js" })
+ .map(visualize);
+
+//-------------------------------------------------------------------------------------------------
+
+function visualize(example) {
+ var name = path.basename(example, '.js'),
+ fsm = require('../examples/' + example),
+ dot = fsm.visualize(),
+ svg = dot2svg(dot),
+ png = dot2png(dot);
+ console.log('visualizing examples/' + example);
+ fs.writeFileSync('examples/' + name + '.dot', dot);
+ fs.writeFileSync('examples/' + name + '.svg', svg);
+ fs.writeFileSync('examples/' + name + '.png', png, 'binary');
+}
+
+//-------------------------------------------------------------------------------------------------
+
+function dot2svg(dot) {
+ var result = child.spawnSync("dot", ["-Tsvg"], { input: dot });
+ if (result.error)
+ dotError(result.error.errno);
+ return result.stdout.toString();
+}
+
+//-------------------------------------------------------------------------------------------------
+
+function dot2png(dot) {
+ var result = child.spawnSync("dot", ["-Tpng"], { input: dot });
+ if (result.error)
+ dotError(result.error.errno);
+ return result.stdout;
+}
+
+//-------------------------------------------------------------------------------------------------
+
+function dotError(errno) {
+ if (errno === 'ENOENT')
+ throw new Error("dot program not found. Install graphviz (http://graphviz.org)")
+ else
+ throw new Error("unexpected error: " + errno)
+}
+
+//-------------------------------------------------------------------------------------------------
diff --git a/bin/minify b/bin/minify
new file mode 100755
index 0000000..88fe76c
--- /dev/null
+++ b/bin/minify
@@ -0,0 +1,40 @@
+#!/usr/bin/env node
+
+//=================================================================================================
+//
+// This script is used (by npm run build) to minify the distributed source code
+//
+//=================================================================================================
+
+var fs = require('fs-sync'),
+ path = require('path'),
+ uglify = require('uglify-js'),
+ target = 'dist';
+
+//-------------------------------------------------------------------------------------------------
+
+fs.expand("lib/**/*.js")
+ .map(minify);
+
+//-------------------------------------------------------------------------------------------------
+
+function minify(file) {
+ var name = output_name(file),
+ expanded = path.join(target, name + '.js'),
+ minified = path.join(target, name + '.min.js')
+
+ console.log('copied ' + file + ' to ' + expanded + ' and minified as ' + minified);
+
+ fs.copy(file, expanded, { force: true });
+ fs.write(minified, uglify.minify(expanded).code);
+}
+
+function output_name(file) {
+ var name = path.basename(file, '.js');
+ if (name === 'state-machine')
+ return 'state-machine'
+ else
+ return 'state-machine-' + name
+}
+
+//-------------------------------------------------------------------------------------------------
diff --git a/bower.json b/bower.json
deleted file mode 100644
index 116db62..0000000
--- a/bower.json
+++ /dev/null
@@ -1,28 +0,0 @@
-{
- "name": "javascript-state-machine",
- "version": "2.3.5",
- "homepage": "https://github.com/jakesgordon/javascript-state-machine",
- "authors": [
- "Jake Gordon "
- ],
- "description": "a simple finite state machine library",
- "main": "state-machine.js",
- "moduleType": [
- "amd",
- "globals",
- "node"
- ],
- "keywords": [
- "state machine",
- "server",
- "client"
- ],
- "license": "MIT",
- "ignore": [
- "**/.*",
- "node_modules",
- "bower_components",
- "test",
- "tests"
- ]
-}
diff --git a/demo/demo.js b/demo/demo.js
deleted file mode 100644
index 4789c85..0000000
--- a/demo/demo.js
+++ /dev/null
@@ -1,78 +0,0 @@
-Demo = function() {
-
- var output = document.getElementById('output'),
- demo = document.getElementById('demo'),
- panic = document.getElementById('panic'),
- warn = document.getElementById('warn'),
- calm = document.getElementById('calm'),
- clear = document.getElementById('clear'),
- count = 0;
-
- var log = function(msg, separate) {
- count = count + (separate ? 1 : 0);
- output.value = count + ": " + msg + "\n" + (separate ? "\n" : "") + output.value;
- demo.className = fsm.current;
- panic.disabled = fsm.cannot('panic');
- warn.disabled = fsm.cannot('warn');
- calm.disabled = fsm.cannot('calm');
- clear.disabled = fsm.cannot('clear');
- };
-
- var fsm = StateMachine.create({
-
- events: [
- { name: 'start', from: 'none', to: 'green' },
- { name: 'warn', from: 'green', to: 'yellow' },
- { name: 'panic', from: 'green', to: 'red' },
- { name: 'panic', from: 'yellow', to: 'red' },
- { name: 'calm', from: 'red', to: 'yellow' },
- { name: 'clear', from: 'red', to: 'green' },
- { name: 'clear', from: 'yellow', to: 'green' },
- ],
-
- callbacks: {
- onbeforestart: function(event, from, to) { log("STARTING UP"); },
- onstart: function(event, from, to) { log("READY"); },
-
- onbeforewarn: function(event, from, to) { log("START EVENT: warn!", true); },
- onbeforepanic: function(event, from, to) { log("START EVENT: panic!", true); },
- onbeforecalm: function(event, from, to) { log("START EVENT: calm!", true); },
- onbeforeclear: function(event, from, to) { log("START EVENT: clear!", true); },
-
- onwarn: function(event, from, to) { log("FINISH EVENT: warn!"); },
- onpanic: function(event, from, to) { log("FINISH EVENT: panic!"); },
- oncalm: function(event, from, to) { log("FINISH EVENT: calm!"); },
- onclear: function(event, from, to) { log("FINISH EVENT: clear!"); },
-
- onleavegreen: function(event, from, to) { log("LEAVE STATE: green"); },
- onleaveyellow: function(event, from, to) { log("LEAVE STATE: yellow"); },
- onleavered: function(event, from, to) { log("LEAVE STATE: red"); async(to); return StateMachine.ASYNC; },
-
- ongreen: function(event, from, to) { log("ENTER STATE: green"); },
- onyellow: function(event, from, to) { log("ENTER STATE: yellow"); },
- onred: function(event, from, to) { log("ENTER STATE: red"); },
-
- onchangestate: function(event, from, to) { log("CHANGED STATE: " + from + " to " + to); }
- }
- });
-
- var async = function(to) {
- pending(to, 3);
- setTimeout(function() {
- pending(to, 2);
- setTimeout(function() {
- pending(to, 1);
- setTimeout(function() {
- fsm.transition(); // trigger deferred state transition
- }, 1000);
- }, 1000);
- }, 1000);
- };
-
- var pending = function(to, n) { log("PENDING STATE: " + to + " in ..." + n); };
-
- fsm.start();
- return fsm;
-
-}();
-
diff --git a/dist/state-machine-history.js b/dist/state-machine-history.js
new file mode 100644
index 0000000..19f7c4f
--- /dev/null
+++ b/dist/state-machine-history.js
@@ -0,0 +1,211 @@
+(function webpackUniversalModuleDefinition(root, factory) {
+ if(typeof exports === 'object' && typeof module === 'object')
+ module.exports = factory();
+ else if(typeof define === 'function' && define.amd)
+ define("StateMachineHistory", [], factory);
+ else if(typeof exports === 'object')
+ exports["StateMachineHistory"] = factory();
+ else
+ root["StateMachineHistory"] = factory();
+})(this, function() {
+return /******/ (function(modules) { // webpackBootstrap
+/******/ // The module cache
+/******/ var installedModules = {};
+/******/
+/******/ // The require function
+/******/ function __webpack_require__(moduleId) {
+/******/
+/******/ // Check if module is in cache
+/******/ if(installedModules[moduleId]) {
+/******/ return installedModules[moduleId].exports;
+/******/ }
+/******/ // Create a new module (and put it into the cache)
+/******/ var module = installedModules[moduleId] = {
+/******/ i: moduleId,
+/******/ l: false,
+/******/ exports: {}
+/******/ };
+/******/
+/******/ // Execute the module function
+/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
+/******/
+/******/ // Flag the module as loaded
+/******/ module.l = true;
+/******/
+/******/ // Return the exports of the module
+/******/ return module.exports;
+/******/ }
+/******/
+/******/
+/******/ // expose the modules object (__webpack_modules__)
+/******/ __webpack_require__.m = modules;
+/******/
+/******/ // expose the module cache
+/******/ __webpack_require__.c = installedModules;
+/******/
+/******/ // identity function for calling harmony imports with the correct context
+/******/ __webpack_require__.i = function(value) { return value; };
+/******/
+/******/ // define getter function for harmony exports
+/******/ __webpack_require__.d = function(exports, name, getter) {
+/******/ if(!__webpack_require__.o(exports, name)) {
+/******/ Object.defineProperty(exports, name, {
+/******/ configurable: false,
+/******/ enumerable: true,
+/******/ get: getter
+/******/ });
+/******/ }
+/******/ };
+/******/
+/******/ // getDefaultExport function for compatibility with non-harmony modules
+/******/ __webpack_require__.n = function(module) {
+/******/ var getter = module && module.__esModule ?
+/******/ function getDefault() { return module['default']; } :
+/******/ function getModuleExports() { return module; };
+/******/ __webpack_require__.d(getter, 'a', getter);
+/******/ return getter;
+/******/ };
+/******/
+/******/ // Object.prototype.hasOwnProperty.call
+/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
+/******/
+/******/ // __webpack_public_path__
+/******/ __webpack_require__.p = "";
+/******/
+/******/ // Load entry module and return exports
+/******/ return __webpack_require__(__webpack_require__.s = 1);
+/******/ })
+/************************************************************************/
+/******/ ([
+/* 0 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+//-------------------------------------------------------------------------------------------------
+
+function camelize(label) {
+
+ if (label.length === 0)
+ return label;
+
+ var n, result, word, words = label.split(/[_-]/);
+
+ // single word with first character already lowercase, return untouched
+ if ((words.length === 1) && (words[0][0].toLowerCase() === words[0][0]))
+ return label;
+
+ result = words[0].toLowerCase();
+ for(n = 1 ; n < words.length ; n++) {
+ result = result + words[n].charAt(0).toUpperCase() + words[n].substring(1).toLowerCase();
+ }
+
+ return result;
+}
+
+//-------------------------------------------------------------------------------------------------
+
+camelize.prepended = function(prepend, label) {
+ label = camelize(label);
+ return prepend + label[0].toUpperCase() + label.substring(1);
+}
+
+//-------------------------------------------------------------------------------------------------
+
+module.exports = camelize;
+
+
+/***/ }),
+/* 1 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+//-------------------------------------------------------------------------------------------------
+
+var camelize = __webpack_require__(0);
+
+//-------------------------------------------------------------------------------------------------
+
+module.exports = function(options) { options = options || {};
+
+ var past = camelize(options.name || options.past || 'history'),
+ future = camelize( options.future || 'future'),
+ clear = camelize.prepended('clear', past),
+ back = camelize.prepended(past, 'back'),
+ forward = camelize.prepended(past, 'forward'),
+ canBack = camelize.prepended('can', back),
+ canForward = camelize.prepended('can', forward),
+ max = options.max;
+
+ var plugin = {
+
+ configure: function(config) {
+ config.addTransitionLifecycleNames(back);
+ config.addTransitionLifecycleNames(forward);
+ },
+
+ init: function(instance) {
+ instance[past] = [];
+ instance[future] = [];
+ },
+
+ lifecycle: function(instance, lifecycle) {
+ if (lifecycle.event === 'onEnterState') {
+ instance[past].push(lifecycle.to);
+ if (max && instance[past].length > max)
+ instance[past].shift();
+ if (lifecycle.transition !== back && lifecycle.transition !== forward)
+ instance[future].length = 0;
+ }
+ },
+
+ methods: {},
+ properties: {}
+
+ }
+
+ plugin.methods[clear] = function() {
+ this[past].length = 0
+ this[future].length = 0
+ }
+
+ plugin.properties[canBack] = {
+ get: function() {
+ return this[past].length > 1
+ }
+ }
+
+ plugin.properties[canForward] = {
+ get: function() {
+ return this[future].length > 0
+ }
+ }
+
+ plugin.methods[back] = function() {
+ if (!this[canBack])
+ throw Error('no history');
+ var from = this[past].pop(),
+ to = this[past].pop();
+ this[future].push(from);
+ this._fsm.transit(back, from, to, []);
+ }
+
+ plugin.methods[forward] = function() {
+ if (!this[canForward])
+ throw Error('no history');
+ var from = this.state,
+ to = this[future].pop();
+ this._fsm.transit(forward, from, to, []);
+ }
+
+ return plugin;
+
+}
+
+
+/***/ })
+/******/ ]);
+});
\ No newline at end of file
diff --git a/dist/state-machine-history.min.js b/dist/state-machine-history.min.js
new file mode 100644
index 0000000..9c7fb3f
--- /dev/null
+++ b/dist/state-machine-history.min.js
@@ -0,0 +1 @@
+!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define("StateMachineHistory",[],e):"object"==typeof exports?exports.StateMachineHistory=e():t.StateMachineHistory=e()}(this,function(){return function(t){function e(r){if(n[r])return n[r].exports;var o=n[r]={i:r,l:!1,exports:{}};return t[r].call(o.exports,o,o.exports,e),o.l=!0,o.exports}var n={};return e.m=t,e.c=n,e.i=function(t){return t},e.d=function(t,n,r){e.o(t,n)||Object.defineProperty(t,n,{configurable:!1,enumerable:!0,get:r})},e.n=function(t){var n=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(n,"a",n),n},e.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},e.p="",e(e.s=1)}([function(t,e,n){"use strict";function r(t){if(0===t.length)return t;var e,n,r=t.split(/[_-]/);if(1===r.length&&r[0][0].toLowerCase()===r[0][0])return t;for(n=r[0].toLowerCase(),e=1;ec&&t[e].shift(),r.transition!==i&&r.transition!==s&&(t[n].length=0))},methods:{},properties:{}};return f.methods[o]=function(){this[e].length=0,this[n].length=0},f.properties[u]={get:function(){return this[e].length>1}},f.properties[p]={get:function(){return this[n].length>0}},f.methods[i]=function(){if(!this[u])throw Error("no history");var t=this[e].pop(),r=this[e].pop();this[n].push(t),this._fsm.transit(i,t,r,[])},f.methods[s]=function(){if(!this[p])throw Error("no history");var t=this.state,e=this[n].pop();this._fsm.transit(s,t,e,[])},f}}])});
\ No newline at end of file
diff --git a/dist/state-machine-visualize.js b/dist/state-machine-visualize.js
new file mode 100644
index 0000000..9c18e13
--- /dev/null
+++ b/dist/state-machine-visualize.js
@@ -0,0 +1,269 @@
+(function webpackUniversalModuleDefinition(root, factory) {
+ if(typeof exports === 'object' && typeof module === 'object')
+ module.exports = factory();
+ else if(typeof define === 'function' && define.amd)
+ define("StateMachineVisualize", [], factory);
+ else if(typeof exports === 'object')
+ exports["StateMachineVisualize"] = factory();
+ else
+ root["StateMachineVisualize"] = factory();
+})(this, function() {
+return /******/ (function(modules) { // webpackBootstrap
+/******/ // The module cache
+/******/ var installedModules = {};
+/******/
+/******/ // The require function
+/******/ function __webpack_require__(moduleId) {
+/******/
+/******/ // Check if module is in cache
+/******/ if(installedModules[moduleId]) {
+/******/ return installedModules[moduleId].exports;
+/******/ }
+/******/ // Create a new module (and put it into the cache)
+/******/ var module = installedModules[moduleId] = {
+/******/ i: moduleId,
+/******/ l: false,
+/******/ exports: {}
+/******/ };
+/******/
+/******/ // Execute the module function
+/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
+/******/
+/******/ // Flag the module as loaded
+/******/ module.l = true;
+/******/
+/******/ // Return the exports of the module
+/******/ return module.exports;
+/******/ }
+/******/
+/******/
+/******/ // expose the modules object (__webpack_modules__)
+/******/ __webpack_require__.m = modules;
+/******/
+/******/ // expose the module cache
+/******/ __webpack_require__.c = installedModules;
+/******/
+/******/ // identity function for calling harmony imports with the correct context
+/******/ __webpack_require__.i = function(value) { return value; };
+/******/
+/******/ // define getter function for harmony exports
+/******/ __webpack_require__.d = function(exports, name, getter) {
+/******/ if(!__webpack_require__.o(exports, name)) {
+/******/ Object.defineProperty(exports, name, {
+/******/ configurable: false,
+/******/ enumerable: true,
+/******/ get: getter
+/******/ });
+/******/ }
+/******/ };
+/******/
+/******/ // getDefaultExport function for compatibility with non-harmony modules
+/******/ __webpack_require__.n = function(module) {
+/******/ var getter = module && module.__esModule ?
+/******/ function getDefault() { return module['default']; } :
+/******/ function getModuleExports() { return module; };
+/******/ __webpack_require__.d(getter, 'a', getter);
+/******/ return getter;
+/******/ };
+/******/
+/******/ // Object.prototype.hasOwnProperty.call
+/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
+/******/
+/******/ // __webpack_public_path__
+/******/ __webpack_require__.p = "";
+/******/
+/******/ // Load entry module and return exports
+/******/ return __webpack_require__(__webpack_require__.s = 1);
+/******/ })
+/************************************************************************/
+/******/ ([
+/* 0 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+module.exports = function(target, sources) {
+ var n, source, key;
+ for(n = 1 ; n < arguments.length ; n++) {
+ source = arguments[n];
+ for(key in source) {
+ if (source.hasOwnProperty(key))
+ target[key] = source[key];
+ }
+ }
+ return target;
+}
+
+
+/***/ }),
+/* 1 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+//-------------------------------------------------------------------------------------------------
+
+var mixin = __webpack_require__(0)
+
+//-------------------------------------------------------------------------------------------------
+
+function visualize(fsm, options) {
+ return dotify(dotcfg(fsm, options));
+}
+
+//-------------------------------------------------------------------------------------------------
+
+function dotcfg(fsm, options) {
+
+ options = options || {}
+
+ var config = dotcfg.fetch(fsm),
+ name = options.name,
+ rankdir = dotcfg.rankdir(options.orientation),
+ states = dotcfg.states(config, options),
+ transitions = dotcfg.transitions(config, options),
+ result = { }
+
+ if (name)
+ result.name = name
+
+ if (rankdir)
+ result.rankdir = rankdir
+
+ if (states && states.length > 0)
+ result.states = states
+
+ if (transitions && transitions.length > 0)
+ result.transitions = transitions
+
+ return result
+}
+
+//-------------------------------------------------------------------------------------------------
+
+dotcfg.fetch = function(fsm) {
+ return (typeof fsm === 'function') ? fsm.prototype._fsm.config
+ : fsm._fsm.config
+}
+
+dotcfg.rankdir = function(orientation) {
+ if (orientation === 'horizontal')
+ return 'LR';
+ else if (orientation === 'vertical')
+ return 'TB';
+}
+
+dotcfg.states = function(config, options) {
+ var index, states = config.states;
+ if (!options.init) { // if not showing init transition, then slice out the implied init :from state
+ index = states.indexOf(config.init.from);
+ states = states.slice(0, index).concat(states.slice(index+1));
+ }
+ return states;
+}
+
+dotcfg.transitions = function(config, options) {
+ var n, max, transition,
+ init = config.init,
+ transitions = config.options.transitions || [], // easier to visualize using the ORIGINAL transition declarations rather than our run-time mapping
+ output = [];
+ if (options.init && init.active)
+ dotcfg.transition(init.name, init.from, init.to, init.dot, config, options, output)
+ for (n = 0, max = transitions.length ; n < max ; n++) {
+ transition = config.options.transitions[n]
+ dotcfg.transition(transition.name, transition.from, transition.to, transition.dot, config, options, output)
+ }
+ return output
+}
+
+dotcfg.transition = function(name, from, to, dot, config, options, output) {
+ var n, max, wildcard = config.defaults.wildcard
+
+ if (Array.isArray(from)) {
+ for(n = 0, max = from.length ; n < max ; n++)
+ dotcfg.transition(name, from[n], to, dot, config, options, output)
+ }
+ else if (from === wildcard || from === undefined) {
+ for(n = 0, max = config.states.length ; n < max ; n++)
+ dotcfg.transition(name, config.states[n], to, dot, config, options, output)
+ }
+ else if (to === wildcard || to === undefined) {
+ dotcfg.transition(name, from, from, dot, config, options, output)
+ }
+ else if (typeof to === 'function') {
+ // do nothing, can't display conditional transition
+ }
+ else {
+ output.push(mixin({}, { from: from, to: to, label: pad(name) }, dot || {}))
+ }
+
+}
+
+//-------------------------------------------------------------------------------------------------
+
+function pad(name) {
+ return " " + name + " "
+}
+
+function quote(name) {
+ return "\"" + name + "\""
+}
+
+function dotify(dotcfg) {
+
+ dotcfg = dotcfg || {};
+
+ var name = dotcfg.name || 'fsm',
+ states = dotcfg.states || [],
+ transitions = dotcfg.transitions || [],
+ rankdir = dotcfg.rankdir,
+ output = [],
+ n, max;
+
+ output.push("digraph " + quote(name) + " {")
+ if (rankdir)
+ output.push(" rankdir=" + rankdir + ";")
+ for(n = 0, max = states.length ; n < max ; n++)
+ output.push(dotify.state(states[n]))
+ for(n = 0, max = transitions.length ; n < max ; n++)
+ output.push(dotify.edge(transitions[n]))
+ output.push("}")
+ return output.join("\n")
+
+}
+
+dotify.state = function(state) {
+ return " " + quote(state) + ";"
+}
+
+dotify.edge = function(edge) {
+ return " " + quote(edge.from) + " -> " + quote(edge.to) + dotify.edge.attr(edge) + ";"
+}
+
+dotify.edge.attr = function(edge) {
+ var n, max, key, keys = Object.keys(edge).sort(), output = [];
+ for(n = 0, max = keys.length ; n < max ; n++) {
+ key = keys[n];
+ if (key !== 'from' && key !== 'to')
+ output.push(key + "=" + quote(edge[key]))
+ }
+ return output.length > 0 ? " [ " + output.join(" ; ") + " ]" : ""
+}
+
+//-------------------------------------------------------------------------------------------------
+
+visualize.dotcfg = dotcfg;
+visualize.dotify = dotify;
+
+//-------------------------------------------------------------------------------------------------
+
+module.exports = visualize;
+
+//-------------------------------------------------------------------------------------------------
+
+
+/***/ })
+/******/ ]);
+});
\ No newline at end of file
diff --git a/dist/state-machine-visualize.min.js b/dist/state-machine-visualize.min.js
new file mode 100644
index 0000000..e517d3b
--- /dev/null
+++ b/dist/state-machine-visualize.min.js
@@ -0,0 +1 @@
+!function(t,n){"object"==typeof exports&&"object"==typeof module?module.exports=n():"function"==typeof define&&define.amd?define("StateMachineVisualize",[],n):"object"==typeof exports?exports.StateMachineVisualize=n():t.StateMachineVisualize=n()}(this,function(){return function(t){function n(r){if(e[r])return e[r].exports;var o=e[r]={i:r,l:!1,exports:{}};return t[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}var e={};return n.m=t,n.c=e,n.i=function(t){return t},n.d=function(t,e,r){n.o(t,e)||Object.defineProperty(t,e,{configurable:!1,enumerable:!0,get:r})},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,n){return Object.prototype.hasOwnProperty.call(t,n)},n.p="",n(n.s=1)}([function(t,n,e){"use strict";t.exports=function(t,n){var e,r,o;for(e=1;e0&&(u.states=s),a&&a.length>0&&(u.transitions=a),u}function i(t){return" "+t+" "}function s(t){return'"'+t+'"'}function a(t){t=t||{};var n,e,r=t.name||"fsm",o=t.states||[],i=t.transitions||[],u=t.rankdir,f=[];for(f.push("digraph "+s(r)+" {"),u&&f.push(" rankdir="+u+";"),n=0,e=o.length;n "+s(t.to)+a.edge.attr(t)+";"},a.edge.attr=function(t){var n,e,r,o=Object.keys(t).sort(),i=[];for(n=0,e=o.length;n0?" [ "+i.join(" ; ")+" ]":""},r.dotcfg=o,r.dotify=a,t.exports=r}])});
\ No newline at end of file
diff --git a/dist/state-machine.js b/dist/state-machine.js
new file mode 100644
index 0000000..b8b9e37
--- /dev/null
+++ b/dist/state-machine.js
@@ -0,0 +1,669 @@
+(function webpackUniversalModuleDefinition(root, factory) {
+ if(typeof exports === 'object' && typeof module === 'object')
+ module.exports = factory();
+ else if(typeof define === 'function' && define.amd)
+ define("StateMachine", [], factory);
+ else if(typeof exports === 'object')
+ exports["StateMachine"] = factory();
+ else
+ root["StateMachine"] = factory();
+})(this, function() {
+return /******/ (function(modules) { // webpackBootstrap
+/******/ // The module cache
+/******/ var installedModules = {};
+/******/
+/******/ // The require function
+/******/ function __webpack_require__(moduleId) {
+/******/
+/******/ // Check if module is in cache
+/******/ if(installedModules[moduleId]) {
+/******/ return installedModules[moduleId].exports;
+/******/ }
+/******/ // Create a new module (and put it into the cache)
+/******/ var module = installedModules[moduleId] = {
+/******/ i: moduleId,
+/******/ l: false,
+/******/ exports: {}
+/******/ };
+/******/
+/******/ // Execute the module function
+/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
+/******/
+/******/ // Flag the module as loaded
+/******/ module.l = true;
+/******/
+/******/ // Return the exports of the module
+/******/ return module.exports;
+/******/ }
+/******/
+/******/
+/******/ // expose the modules object (__webpack_modules__)
+/******/ __webpack_require__.m = modules;
+/******/
+/******/ // expose the module cache
+/******/ __webpack_require__.c = installedModules;
+/******/
+/******/ // identity function for calling harmony imports with the correct context
+/******/ __webpack_require__.i = function(value) { return value; };
+/******/
+/******/ // define getter function for harmony exports
+/******/ __webpack_require__.d = function(exports, name, getter) {
+/******/ if(!__webpack_require__.o(exports, name)) {
+/******/ Object.defineProperty(exports, name, {
+/******/ configurable: false,
+/******/ enumerable: true,
+/******/ get: getter
+/******/ });
+/******/ }
+/******/ };
+/******/
+/******/ // getDefaultExport function for compatibility with non-harmony modules
+/******/ __webpack_require__.n = function(module) {
+/******/ var getter = module && module.__esModule ?
+/******/ function getDefault() { return module['default']; } :
+/******/ function getModuleExports() { return module; };
+/******/ __webpack_require__.d(getter, 'a', getter);
+/******/ return getter;
+/******/ };
+/******/
+/******/ // Object.prototype.hasOwnProperty.call
+/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
+/******/
+/******/ // __webpack_public_path__
+/******/ __webpack_require__.p = "";
+/******/
+/******/ // Load entry module and return exports
+/******/ return __webpack_require__(__webpack_require__.s = 5);
+/******/ })
+/************************************************************************/
+/******/ ([
+/* 0 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+module.exports = function(target, sources) {
+ var n, source, key;
+ for(n = 1 ; n < arguments.length ; n++) {
+ source = arguments[n];
+ for(key in source) {
+ if (source.hasOwnProperty(key))
+ target[key] = source[key];
+ }
+ }
+ return target;
+}
+
+
+/***/ }),
+/* 1 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+//-------------------------------------------------------------------------------------------------
+
+var mixin = __webpack_require__(0);
+
+//-------------------------------------------------------------------------------------------------
+
+module.exports = {
+
+ build: function(target, config) {
+ var n, max, plugin, plugins = config.plugins;
+ for(n = 0, max = plugins.length ; n < max ; n++) {
+ plugin = plugins[n];
+ if (plugin.methods)
+ mixin(target, plugin.methods);
+ if (plugin.properties)
+ Object.defineProperties(target, plugin.properties);
+ }
+ },
+
+ hook: function(fsm, name, additional) {
+ var n, max, method, plugin,
+ plugins = fsm.config.plugins,
+ args = [fsm.context];
+
+ if (additional)
+ args = args.concat(additional)
+
+ for(n = 0, max = plugins.length ; n < max ; n++) {
+ plugin = plugins[n]
+ method = plugins[n][name]
+ if (method)
+ method.apply(plugin, args);
+ }
+ }
+
+}
+
+//-------------------------------------------------------------------------------------------------
+
+
+/***/ }),
+/* 2 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+//-------------------------------------------------------------------------------------------------
+
+function camelize(label) {
+
+ if (label.length === 0)
+ return label;
+
+ var n, result, word, words = label.split(/[_-]/);
+
+ // single word with first character already lowercase, return untouched
+ if ((words.length === 1) && (words[0][0].toLowerCase() === words[0][0]))
+ return label;
+
+ result = words[0].toLowerCase();
+ for(n = 1 ; n < words.length ; n++) {
+ result = result + words[n].charAt(0).toUpperCase() + words[n].substring(1).toLowerCase();
+ }
+
+ return result;
+}
+
+//-------------------------------------------------------------------------------------------------
+
+camelize.prepended = function(prepend, label) {
+ label = camelize(label);
+ return prepend + label[0].toUpperCase() + label.substring(1);
+}
+
+//-------------------------------------------------------------------------------------------------
+
+module.exports = camelize;
+
+
+/***/ }),
+/* 3 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+//-------------------------------------------------------------------------------------------------
+
+var mixin = __webpack_require__(0),
+ camelize = __webpack_require__(2);
+
+//-------------------------------------------------------------------------------------------------
+
+function Config(options, StateMachine) {
+
+ options = options || {};
+
+ this.options = options; // preserving original options can be useful (e.g visualize plugin)
+ this.defaults = StateMachine.defaults;
+ this.states = [];
+ this.transitions = [];
+ this.map = {};
+ this.lifecycle = this.configureLifecycle();
+ this.init = this.configureInitTransition(options.init);
+ this.data = this.configureData(options.data);
+ this.methods = this.configureMethods(options.methods);
+
+ this.map[this.defaults.wildcard] = {};
+
+ this.configureTransitions(options.transitions || []);
+
+ this.plugins = this.configurePlugins(options.plugins, StateMachine.plugin);
+
+}
+
+//-------------------------------------------------------------------------------------------------
+
+mixin(Config.prototype, {
+
+ addState: function(name) {
+ if (!this.map[name]) {
+ this.states.push(name);
+ this.addStateLifecycleNames(name);
+ this.map[name] = {};
+ }
+ },
+
+ addStateLifecycleNames: function(name) {
+ this.lifecycle.onEnter[name] = camelize.prepended('onEnter', name);
+ this.lifecycle.onLeave[name] = camelize.prepended('onLeave', name);
+ this.lifecycle.on[name] = camelize.prepended('on', name);
+ },
+
+ addTransition: function(name) {
+ if (this.transitions.indexOf(name) < 0) {
+ this.transitions.push(name);
+ this.addTransitionLifecycleNames(name);
+ }
+ },
+
+ addTransitionLifecycleNames: function(name) {
+ this.lifecycle.onBefore[name] = camelize.prepended('onBefore', name);
+ this.lifecycle.onAfter[name] = camelize.prepended('onAfter', name);
+ this.lifecycle.on[name] = camelize.prepended('on', name);
+ },
+
+ mapTransition: function(transition) {
+ var name = transition.name,
+ from = transition.from,
+ to = transition.to;
+ this.addState(from);
+ if (typeof to !== 'function')
+ this.addState(to);
+ this.addTransition(name);
+ this.map[from][name] = transition;
+ return transition;
+ },
+
+ configureLifecycle: function() {
+ return {
+ onBefore: { transition: 'onBeforeTransition' },
+ onAfter: { transition: 'onAfterTransition' },
+ onEnter: { state: 'onEnterState' },
+ onLeave: { state: 'onLeaveState' },
+ on: { transition: 'onTransition' }
+ };
+ },
+
+ configureInitTransition: function(init) {
+ if (typeof init === 'string') {
+ return this.mapTransition(mixin({}, this.defaults.init, { to: init, active: true }));
+ }
+ else if (typeof init === 'object') {
+ return this.mapTransition(mixin({}, this.defaults.init, init, { active: true }));
+ }
+ else {
+ this.addState(this.defaults.init.from);
+ return this.defaults.init;
+ }
+ },
+
+ configureData: function(data) {
+ if (typeof data === 'function')
+ return data;
+ else if (typeof data === 'object')
+ return function() { return data; }
+ else
+ return function() { return {}; }
+ },
+
+ configureMethods: function(methods) {
+ return methods || {};
+ },
+
+ configurePlugins: function(plugins, builtin) {
+ plugins = plugins || [];
+ var n, max, plugin;
+ for(n = 0, max = plugins.length ; n < max ; n++) {
+ plugin = plugins[n];
+ if (typeof plugin === 'function')
+ plugins[n] = plugin = plugin()
+ if (plugin.configure)
+ plugin.configure(this);
+ }
+ return plugins
+ },
+
+ configureTransitions: function(transitions) {
+ var i, n, transition, from, to, wildcard = this.defaults.wildcard;
+ for(n = 0 ; n < transitions.length ; n++) {
+ transition = transitions[n];
+ from = Array.isArray(transition.from) ? transition.from : [transition.from || wildcard]
+ to = transition.to || wildcard;
+ for(i = 0 ; i < from.length ; i++) {
+ this.mapTransition({ name: transition.name, from: from[i], to: to });
+ }
+ }
+ },
+
+ transitionFor: function(state, transition) {
+ var wildcard = this.defaults.wildcard;
+ return this.map[state][transition] ||
+ this.map[wildcard][transition];
+ },
+
+ transitionsFor: function(state) {
+ var wildcard = this.defaults.wildcard;
+ return Object.keys(this.map[state]).concat(Object.keys(this.map[wildcard]));
+ },
+
+ allStates: function() {
+ return this.states;
+ },
+
+ allTransitions: function() {
+ return this.transitions;
+ }
+
+});
+
+//-------------------------------------------------------------------------------------------------
+
+module.exports = Config;
+
+//-------------------------------------------------------------------------------------------------
+
+
+/***/ }),
+/* 4 */
+/***/ (function(module, exports, __webpack_require__) {
+
+
+var mixin = __webpack_require__(0),
+ Exception = __webpack_require__(6),
+ plugin = __webpack_require__(1),
+ UNOBSERVED = [ null, [] ];
+
+//-------------------------------------------------------------------------------------------------
+
+function JSM(context, config) {
+ this.context = context;
+ this.config = config;
+ this.state = config.init.from;
+ this.observers = [context];
+}
+
+//-------------------------------------------------------------------------------------------------
+
+mixin(JSM.prototype, {
+
+ init: function(args) {
+ mixin(this.context, this.config.data.apply(this.context, args));
+ plugin.hook(this, 'init');
+ if (this.config.init.active)
+ return this.fire(this.config.init.name, []);
+ },
+
+ is: function(state) {
+ return Array.isArray(state) ? (state.indexOf(this.state) >= 0) : (this.state === state);
+ },
+
+ isPending: function() {
+ return this.pending;
+ },
+
+ can: function(transition) {
+ return !this.isPending() && !!this.seek(transition);
+ },
+
+ cannot: function(transition) {
+ return !this.can(transition);
+ },
+
+ allStates: function() {
+ return this.config.allStates();
+ },
+
+ allTransitions: function() {
+ return this.config.allTransitions();
+ },
+
+ transitions: function() {
+ return this.config.transitionsFor(this.state);
+ },
+
+ seek: function(transition, args) {
+ var wildcard = this.config.defaults.wildcard,
+ entry = this.config.transitionFor(this.state, transition),
+ to = entry && entry.to;
+ if (typeof to === 'function')
+ return to.apply(this.context, args);
+ else if (to === wildcard)
+ return this.state
+ else
+ return to
+ },
+
+ fire: function(transition, args) {
+ return this.transit(transition, this.state, this.seek(transition, args), args);
+ },
+
+ transit: function(transition, from, to, args) {
+
+ var lifecycle = this.config.lifecycle,
+ changed = this.config.options.observeUnchangedState || (from !== to);
+
+ if (!to)
+ return this.context.onInvalidTransition(transition, from, to);
+
+ if (this.isPending())
+ return this.context.onPendingTransition(transition, from, to);
+
+ this.config.addState(to); // might need to add this state if it's unknown (e.g. conditional transition or goto)
+
+ this.beginTransit();
+
+ args.unshift({ // this context will be passed to each lifecycle event observer
+ transition: transition,
+ from: from,
+ to: to,
+ fsm: this.context
+ });
+
+ return this.observeEvents([
+ this.observersForEvent(lifecycle.onBefore.transition),
+ this.observersForEvent(lifecycle.onBefore[transition]),
+ changed ? this.observersForEvent(lifecycle.onLeave.state) : UNOBSERVED,
+ changed ? this.observersForEvent(lifecycle.onLeave[from]) : UNOBSERVED,
+ this.observersForEvent(lifecycle.on.transition),
+ changed ? [ 'doTransit', [ this ] ] : UNOBSERVED,
+ changed ? this.observersForEvent(lifecycle.onEnter.state) : UNOBSERVED,
+ changed ? this.observersForEvent(lifecycle.onEnter[to]) : UNOBSERVED,
+ changed ? this.observersForEvent(lifecycle.on[to]) : UNOBSERVED,
+ this.observersForEvent(lifecycle.onAfter.transition),
+ this.observersForEvent(lifecycle.onAfter[transition]),
+ this.observersForEvent(lifecycle.on[transition])
+ ], args);
+ },
+
+ beginTransit: function() { this.pending = true; },
+ endTransit: function(result) { this.pending = false; return result; },
+ failTransit: function(result) { this.pending = false; throw result; },
+ doTransit: function(lifecycle) { this.state = lifecycle.to; },
+
+ observe: function(args) {
+ if (args.length === 2) {
+ var observer = {};
+ observer[args[0]] = args[1];
+ this.observers.push(observer);
+ }
+ else {
+ this.observers.push(args[0]);
+ }
+ },
+
+ observersForEvent: function(event) { // TODO: this could be cached
+ var n = 0, max = this.observers.length, observer, result = [];
+ for( ; n < max ; n++) {
+ observer = this.observers[n];
+ if (observer[event])
+ result.push(observer);
+ }
+ return [ event, result, true ]
+ },
+
+ observeEvents: function(events, args, previousEvent, previousResult) {
+ if (events.length === 0) {
+ return this.endTransit(previousResult === undefined ? true : previousResult);
+ }
+
+ var event = events[0][0],
+ observers = events[0][1],
+ pluggable = events[0][2];
+
+ args[0].event = event;
+ if (event && pluggable && event !== previousEvent)
+ plugin.hook(this, 'lifecycle', args);
+
+ if (observers.length === 0) {
+ events.shift();
+ return this.observeEvents(events, args, event, previousResult);
+ }
+ else {
+ var observer = observers.shift(),
+ result = observer[event].apply(observer, args);
+ if (result && typeof result.then === 'function') {
+ return result.then(this.observeEvents.bind(this, events, args, event))
+ .catch(this.failTransit.bind(this))
+ }
+ else if (result === false) {
+ return this.endTransit(false);
+ }
+ else {
+ return this.observeEvents(events, args, event, result);
+ }
+ }
+ },
+
+ onInvalidTransition: function(transition, from, to) {
+ throw new Exception("transition is invalid in current state", transition, from, to, this.state);
+ },
+
+ onPendingTransition: function(transition, from, to) {
+ throw new Exception("transition is invalid while previous transition is still in progress", transition, from, to, this.state);
+ }
+
+});
+
+//-------------------------------------------------------------------------------------------------
+
+module.exports = JSM;
+
+//-------------------------------------------------------------------------------------------------
+
+
+/***/ }),
+/* 5 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+//-----------------------------------------------------------------------------------------------
+
+var mixin = __webpack_require__(0),
+ camelize = __webpack_require__(2),
+ plugin = __webpack_require__(1),
+ Config = __webpack_require__(3),
+ JSM = __webpack_require__(4);
+
+//-----------------------------------------------------------------------------------------------
+
+var PublicMethods = {
+ is: function(state) { return this._fsm.is(state) },
+ can: function(transition) { return this._fsm.can(transition) },
+ cannot: function(transition) { return this._fsm.cannot(transition) },
+ observe: function() { return this._fsm.observe(arguments) },
+ transitions: function() { return this._fsm.transitions() },
+ allTransitions: function() { return this._fsm.allTransitions() },
+ allStates: function() { return this._fsm.allStates() },
+ onInvalidTransition: function(t, from, to) { return this._fsm.onInvalidTransition(t, from, to) },
+ onPendingTransition: function(t, from, to) { return this._fsm.onPendingTransition(t, from, to) },
+}
+
+var PublicProperties = {
+ state: {
+ configurable: false,
+ enumerable: true,
+ get: function() {
+ return this._fsm.state;
+ },
+ set: function(state) {
+ throw Error('use transitions to change state')
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------------------------
+
+function StateMachine(options) {
+ return apply(this || {}, options);
+}
+
+function factory() {
+ var cstor, options;
+ if (typeof arguments[0] === 'function') {
+ cstor = arguments[0];
+ options = arguments[1] || {};
+ }
+ else {
+ cstor = function() { this._fsm.apply(this, arguments) };
+ options = arguments[0] || {};
+ }
+ var config = new Config(options, StateMachine);
+ build(cstor.prototype, config);
+ cstor.prototype._fsm.config = config; // convenience access to shared config without needing an instance
+ return cstor;
+}
+
+//-------------------------------------------------------------------------------------------------
+
+function apply(instance, options) {
+ var config = new Config(options, StateMachine);
+ build(instance, config);
+ instance._fsm();
+ return instance;
+}
+
+function build(target, config) {
+ if ((typeof target !== 'object') || Array.isArray(target))
+ throw Error('StateMachine can only be applied to objects');
+ plugin.build(target, config);
+ Object.defineProperties(target, PublicProperties);
+ mixin(target, PublicMethods);
+ mixin(target, config.methods);
+ config.allTransitions().forEach(function(transition) {
+ target[camelize(transition)] = function() {
+ return this._fsm.fire(transition, [].slice.call(arguments))
+ }
+ });
+ target._fsm = function() {
+ this._fsm = new JSM(this, config);
+ this._fsm.init(arguments);
+ }
+}
+
+//-----------------------------------------------------------------------------------------------
+
+StateMachine.version = '3.0.1';
+StateMachine.factory = factory;
+StateMachine.apply = apply;
+StateMachine.defaults = {
+ wildcard: '*',
+ init: {
+ name: 'init',
+ from: 'none'
+ }
+}
+
+//===============================================================================================
+
+module.exports = StateMachine;
+
+
+/***/ }),
+/* 6 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+module.exports = function(message, transition, from, to, current) {
+ this.message = message;
+ this.transition = transition;
+ this.from = from;
+ this.to = to;
+ this.current = current;
+}
+
+
+/***/ })
+/******/ ]);
+});
\ No newline at end of file
diff --git a/dist/state-machine.min.js b/dist/state-machine.min.js
new file mode 100644
index 0000000..b9439bc
--- /dev/null
+++ b/dist/state-machine.min.js
@@ -0,0 +1 @@
+!function(t,n){"object"==typeof exports&&"object"==typeof module?module.exports=n():"function"==typeof define&&define.amd?define("StateMachine",[],n):"object"==typeof exports?exports.StateMachine=n():t.StateMachine=n()}(this,function(){return function(t){function n(e){if(i[e])return i[e].exports;var s=i[e]={i:e,l:!1,exports:{}};return t[e].call(s.exports,s,s.exports,n),s.l=!0,s.exports}var i={};return n.m=t,n.c=i,n.i=function(t){return t},n.d=function(t,i,e){n.o(t,i)||Object.defineProperty(t,i,{configurable:!1,enumerable:!0,get:e})},n.n=function(t){var i=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(i,"a",i),i},n.o=function(t,n){return Object.prototype.hasOwnProperty.call(t,n)},n.p="",n(n.s=5)}([function(t,n,i){"use strict";t.exports=function(t,n){var i,e,s;for(i=1;i=0:this.state===t},isPending:function(){return this.pending},can:function(t){return!this.isPending()&&!!this.seek(t)},cannot:function(t){return!this.can(t)},allStates:function(){return this.config.allStates()},allTransitions:function(){return this.config.allTransitions()},transitions:function(){return this.config.transitionsFor(this.state)},seek:function(t,n){var i=this.config.defaults.wildcard,e=this.config.transitionFor(this.state,t),s=e&&e.to;return"function"==typeof s?s.apply(this.context,n):s===i?this.state:s},fire:function(t,n){return this.transit(t,this.state,this.seek(t,n),n)},transit:function(t,n,i,e){var s=this.config.lifecycle,r=this.config.options.observeUnchangedState||n!==i;return i?this.isPending()?this.context.onPendingTransition(t,n,i):(this.config.addState(i),this.beginTransit(),e.unshift({transition:t,from:n,to:i,fsm:this.context}),this.observeEvents([this.observersForEvent(s.onBefore.transition),this.observersForEvent(s.onBefore[t]),r?this.observersForEvent(s.onLeave.state):a,r?this.observersForEvent(s.onLeave[n]):a,this.observersForEvent(s.on.transition),r?["doTransit",[this]]:a,r?this.observersForEvent(s.onEnter.state):a,r?this.observersForEvent(s.onEnter[i]):a,r?this.observersForEvent(s.on[i]):a,this.observersForEvent(s.onAfter.transition),this.observersForEvent(s.onAfter[t]),this.observersForEvent(s.on[t])],e)):this.context.onInvalidTransition(t,n,i)},beginTransit:function(){this.pending=!0},endTransit:function(t){return this.pending=!1,t},failTransit:function(t){throw this.pending=!1,t},doTransit:function(t){this.state=t.to},observe:function(t){if(2===t.length){var n={};n[t[0]]=t[1],this.observers.push(n)}else this.observers.push(t[0])},observersForEvent:function(t){for(var n,i=0,e=this.observers.length,s=[];i You should be familiar with the state machine [Lifecycle Events](lifecycle-events.md) before reading this article.
+
+Sometimes, you need to execute some asynchronous code during a state transition and ensure the new
+state is not entered until your code has completed. A good example of this is when you transition
+out of a state and want to gradually fade a UI component away, or slide it off the screen, and
+don't want to transition to the next state until after that animation has completed.
+
+You can achieve this by returning a [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)
+object from any of the [Lifecycle Events](lifecycle-events.md).
+
+Returning a Promise from a lifecycle event will cause the lifecycle for that transition to
+pause. It can be continued by resolving the promise, or cancelled by rejecting the promise.
+
+For example (using jQuery effects):
+
+```javascript
+ var fsm = new StateMachine({
+
+ init: 'menu',
+
+ transitions: [
+ { name: 'play', from: 'menu', to: 'game' },
+ { name: 'quit', from: 'game', to: 'menu' }
+ ],
+
+ methods: {
+
+ onEnterMenu: function() {
+ return new Promise(function(resolve, reject) {
+ $('#menu').fadeIn('fast', resolve)
+ })
+ },
+
+ onEnterGame: function() {
+ return new Promise(function(resolve, reject) {
+ $('#game').fadeIn('fast', resolve)
+ })
+ },
+
+ onLeaveMenu: function() {
+ return new Promise(function(resolve, reject) {
+ $('#menu').fadeOut('fast', resolve)
+ })
+ },
+
+ onLeaveGame: function() {
+ return new Promise(function(resolve, reject) {
+ $('#game').fadeOut('fast', resolve)
+ })
+ }
+ }
+
+ })
+```
+
+> Be sure that you always resolve (or reject) your Promise eventually, otherwise the state
+ machine will be stuck forever within that pending transition.
diff --git a/docs/contributing.md b/docs/contributing.md
new file mode 100644
index 0000000..01e5e74
--- /dev/null
+++ b/docs/contributing.md
@@ -0,0 +1,59 @@
+# Contributing
+
+The `javascript-state-machine` library is built using:
+
+ * [Webpack 2](https://webpack.js.org/concepts/) - for bundling javascript modules together
+ * [UglifyJS2](https://github.com/mishoo/UglifyJS2) - for minifying bundled javascript files
+ * [Ava](https://github.com/avajs/ava) - for testing
+
+The directory structure includes:
+
+```shell
+ /bin # - build scripts
+ /dist # - minified bundles for distribution
+ /docs # - documentation
+ /examples # - example visualizations
+ /lib # - bundled source code for npm
+ /src # - source code
+ /test # - unit tests
+
+ package.json # - npm configuration
+ webpack.config.js # - webpack configuration
+
+ LICENSE # - the project licensing terms
+ README.md # - the project readme
+ RELEASE_NOTES.md # - the project release notes
+
+```
+
+Build time dependencies can be installed using npm:
+
+```shell
+ > npm install
+```
+
+A number of npm scripts are available:
+
+```shell
+ > npm run test # run unit tests
+ > npm run build # bundle and minify files for distribution
+ > npm run watch # run tests if source files change
+```
+
+## Source Code
+
+The source code is written in es5 syntax and should be supported by all [es5 compatible browsers](http://caniuse.com/#feat=es5).
+[Babel](https://babeljs.io/) is **NOT** used for this project. Webpack is used to
+bundle modules together for distribution.
+
+## Submitting Pull Requests
+
+Generally speaking, please raise an issue first and lets discuss the problem and the
+proposed solution. The next step would be a pull-request - fantastic and thank you for helping out - but
+please try to...
+
+ * ensure the tests pass (`npm test`).
+ * rebuild distribution files (`npm run build`).
+ * include tests for your changes.
+ * include documentation for your changes.
+ * include a great commit message.
diff --git a/docs/data-and-methods.md b/docs/data-and-methods.md
new file mode 100644
index 0000000..28aa7a6
--- /dev/null
+++ b/docs/data-and-methods.md
@@ -0,0 +1,64 @@
+# Data and Methods
+
+In addition to [States](states-and-transitions.md) and [Transitions](states-and-transitions.md), a state machine can
+also contain arbitrary data and methods:
+
+```javascript
+ var fsm = new StateMachine({
+ init: 'A',
+ transitions: [
+ { name: 'step', from: 'A', to: 'B' }
+ ],
+ data: {
+ color: 'red'
+ },
+ methods: {
+ describe: function() {
+ console.log('I am ' + this.color);
+ }
+ }
+ });
+
+ fsm.state; // 'A'
+ fsm.color; // 'red'
+ fsm.describe(); // 'I am red'
+```
+
+## Data and State Machine Factories
+
+If you are constructing multiple instances from a [State Machine Factory](state-machine-factory.md) then the
+`data` object will be shared amongst them. This is almost certainly **NOT** what you want! To
+ensure that each instance gets unique data you should use a `data` method instead:
+
+```javascript
+ var FSM = StateMachine.factory({
+ init: 'A',
+ transitions: [
+ { name: 'step', from: 'A', to: 'B' }
+ ],
+ data: function(color) { // <-- use a method that can be called for each instance
+ return {
+ color: color
+ }
+ },
+ methods: {
+ describe: function() {
+ console.log('I am ' + this.color);
+ }
+ }
+ });
+
+ var a = new FSM('red'),
+ b = new FSM('blue');
+
+ a.state; // 'A'
+ b.state; // 'A'
+
+ a.color; // 'red'
+ b.color; // 'blue'
+
+ a.describe(); // 'I am red'
+ b.describe(); // 'I am blue'
+```
+
+> NOTE: that arguments used when constructing each instance are passed thru to the `data` method directly.
diff --git a/docs/error-handling.md b/docs/error-handling.md
new file mode 100644
index 0000000..c79f14c
--- /dev/null
+++ b/docs/error-handling.md
@@ -0,0 +1,56 @@
+# Error Handling
+
+## Invalid Transitions
+
+By default, if you try to fire a transition that is not allowed in the current state, the
+state machine will throw an exception. If you prefer to handle the problem yourself, you can
+define a custom `onInvalidTransition` handler:
+
+```javascript
+ var fsm = new StateMachine({
+ init: 'A',
+ transitions: [
+ { name: 'step', from: 'A', to: 'B' },
+ { name: 'reset', from: 'B', to: 'A' }
+ ],
+ methods: {
+ onInvalidTransition: function(transition, from, to) {
+ throw new Exception("transition not allowed from that state");
+ }
+ }
+ });
+
+ fsm.state; // 'A'
+ fsm.can('step'); // true
+ fsm.can('reset'); // false
+
+ fsm.reset(); // <-- throws "transition not allowed from that state"
+```
+
+## Pending Transitions
+
+By default, if you try to fire a transition during a [Lifecycle Event](lifecycle-events.md) for a
+pending transition, the state machine will throw an exception. If you prefer to handle the problem
+yourself, you can define a custom `onPendingTransition` handler:
+
+```javascript
+ var fsm = new StateMachine({
+ init: 'A',
+ transitions: [
+ { name: 'step', from: 'A', to: 'B' },
+ { name: 'step', from: 'B', to: 'C' }
+ ],
+ methods: {
+ onLeaveA: function() {
+ this.step(); // <-- uh oh, trying to transition from within a lifecycle event is not allowed
+ },
+ onPendingTransition: function(transition, from, to) {
+ throw new Exception("transition already in progress");
+ }
+ }
+ });
+
+ fsm.state; // 'A'
+ fsm.can('step'), // true
+ fsm.step(); // <-- throws "transition already in progress"
+```
diff --git a/docs/initialization.md b/docs/initialization.md
new file mode 100644
index 0000000..39151ff
--- /dev/null
+++ b/docs/initialization.md
@@ -0,0 +1,57 @@
+# Initialization Options
+
+## Explicit Init Transition
+
+By default, if you don't specify an initial state, the state machine will be in the `none`
+state, no lifecycle events will fire during construction, and you will need to provide an
+explicit transition to advance out of this state:
+
+```javascript
+ var fsm = new StateMachine({
+ transitions: [
+ { name: 'init', from: 'none', to: 'A' },
+ { name: 'step', from: 'A', to: 'B' },
+ { name: 'step', from: 'B', to: 'C' }
+ ]
+ });
+ fsm.state; // 'none'
+ fsm.init(); // 'init()' transition is fired explicitly
+ fsm.state; // 'A'
+```
+
+## Implicit Init Transition
+
+If you specify the name of your initial state (as in most of the examples in this documentation),
+then an implicit `init` transition will be created for you and fired (along with appropriate
+lifecycle events) when the state machine is constructed.
+
+> This is the most common initialization strategy, and the one you should use 90% of the time
+
+```javascript
+ var fsm = new StateMachine({
+ init: 'A',
+ transitions: [
+ { name: 'step', from: 'A', to: 'B' },
+ { name: 'step', from: 'B', to: 'C' }
+ ]
+ }); // 'init()' transition fires from 'none' to 'A' during construction
+ fsm.state; // 'A'
+```
+
+## Initialization and State Machine Factories
+
+For [State Machine Factories](state-machine-factory.md), the `init` transition
+is triggered for each constructed instance.
+
+```javascript
+ var FSM = StateMachine.factory({
+ init: 'A',
+ transitions: [
+ { name: 'step', from: 'A', to: 'B' },
+ { name: 'step', from: 'B', to: 'C' }
+ ]
+ });
+
+ var fsm1 = new FSM(), // 'init()' transition fires from 'none' to 'A' for fsm1
+ fsm2 = new FSM(); // 'init()' transition fires from 'none' to 'A' for fsm2
+```
diff --git a/docs/lifecycle-events.md b/docs/lifecycle-events.md
new file mode 100644
index 0000000..bb3381b
--- /dev/null
+++ b/docs/lifecycle-events.md
@@ -0,0 +1,148 @@
+# Lifecycle Events
+
+In order to track or perform an action when a transition occurs, five
+general-purpose lifecycle events can be observed:
+
+ * `onBeforeTransition` - fired before any transition
+ * `onLeaveState` - fired when leaving any state
+ * `onTransition` - fired during any transition
+ * `onEnterState` - fired when entering any state
+ * `onAfterTransition` - fired after any transition
+
+In addition to the general-purpose events, transitions can be observed
+using your specific transition and state names:
+
+ * `onBefore` - fired before a specific TRANSITION begins
+ * `onLeave` - fired when leaving a specific STATE
+ * `onEnter` - fired when entering a specific STATE
+ * `onAfter` - fired after a specific TRANSITION completes
+
+For convenience, the 2 most useful events can be shortened:
+
+ * `on` - convenience shorthand for `onAfter`
+ * `on` - convenience shorthand for `onEnter`
+
+## Observing Lifecycle Events
+
+Individual lifecycle events can be observed using an observer method:
+
+```javascript
+ fsm.observe('onStep', function() {
+ console.log('stepped');
+ });
+```
+
+Multiple events can be observed using an observer object:
+
+```javascript
+ fsm.observe({
+ onStep: function() { console.log('stepped'); }
+ onA: function() { console.log('entered state A'); }
+ onB: function() { console.log('entered state B'); }
+ });
+```
+
+A state machine always observes its own lifecycle events:
+
+```javascript
+ var fsm = new StateMachine({
+ init: 'A',
+ transitions: [
+ { name: 'step', from: 'A', to: 'B' }
+ ],
+ methods: {
+ onStep: function() { console.log('stepped'); }
+ onA: function() { console.log('entered state A'); }
+ onB: function() { console.log('entered state B'); }
+ }
+ });
+```
+
+## Lifecycle Event Arguments
+
+Observers will be passed a single argument containing a `lifecycle` object with the following attributes:
+
+ * **transition** - the transition name
+ * **from** - the previous state
+ * **to** - the next state
+
+In addition to the `lifecycle` argument, the observer will receive any arbitrary arguments passed
+into the transition method
+
+```javascript
+ var fsm = new StateMachine({
+ transitions: [
+ { name: 'step', from: 'A', to: 'B' }
+ ],
+ methods: {
+ onTransition: function(lifecycle, arg1, arg2) {
+ console.log(lifecycle.transition); // 'step'
+ console.log(lifecycle.from); // 'A'
+ console.log(lifecycle.to); // 'B'
+ console.log(arg1); // 42
+ console.log(arg2); // 'hello'
+ }
+ }
+ });
+
+ fsm.step(42, 'hello');
+```
+
+## Lifecycle Event Names
+
+Lifecycle event names always use standard javascipt camelCase, even if your transition and
+state names do not:
+
+```javascript
+ var fsm = new StateMachine({
+ transitions: [
+ { name: 'do-with-dash', from: 'has-dash', to: 'has_underscore' },
+ { name: 'do_with_underscore', from: 'has_underscore', to: 'alreadyCamelized' },
+ { name: 'doAlreadyCamelized', from: 'alreadyCamelize', to: 'has-dash' }
+ ],
+ methods: {
+ onBeforeDoWithDash: function() { /* ... */ },
+ onBeforeDoWithUnderscore: function() { /* ... */ },
+ onBeforeDoAlreadyCamelized: function() { /* ... */ },
+ onLeaveHasDash: function() { /* ... */ },
+ onLeaveHasUnderscore: function() { /* ... */ },
+ onLeaveAlreadyCamelized: function() { /* ... */ },
+ onEnterHasDash: function() { /* ... */ },
+ onEnterHasUnderscore: function() { /* ... */ },
+ onEnterAlreadyCamelized: function() { /* ... */ },
+ onAfterDoWithDash: function() { /* ... */ },
+ onAfterDoWithUnderscore: function() { /* ... */ },
+ onAfterDoAlreadyCamelized: function() { /* ... */ }
+ }
+ });
+```
+
+# Lifecycle Events Listed in Order
+
+To recap, the lifecycle of a transition occurs in the following order:
+
+ * `onBeforeTransition` - fired before any transition
+ * `onBefore` - fired before a specific TRANSITION
+ * `onLeaveState` - fired when leaving any state
+ * `onLeave` - fired when leaving a specific STATE
+ * `onTransition` - fired during any transition
+ * `onEnterState` - fired when entering any state
+ * `onEnter` - fired when entering a specific STATE
+ * `on` - convenience shorthand for `onEnter`
+ * `onAfterTransition` - fired after any transition
+ * `onAfter` - fired after a specific TRANSITION
+ * `on` - convenience shorthand for `onAfter`
+
+# Cancelling a Transition
+
+Any observer can cancel a transition by explicitly returning `false` during any of the following
+lifecycle events:
+
+ * `onBeforeTransition`
+ * `onBefore`
+ * `onLeaveState`
+ * `onLeave`
+ * `onTransition`
+
+All subsequent lifecycle events will be cancelled and the state will remain unchanged.
+
diff --git a/docs/state-history.md b/docs/state-history.md
new file mode 100644
index 0000000..7e2ac85
--- /dev/null
+++ b/docs/state-history.md
@@ -0,0 +1,127 @@
+# Remembering State History
+
+By default, a state machine only tracks its current state. If you wish to track the state history
+you can extend the state machine with the `state-machine-history` plugin.
+
+```javascript
+ var StateMachineHistory = require('javascript-state-machine/lib/history')
+```
+
+```javascript
+
+ var fsm = new StateMachine({
+ init: 'A',
+ transitions: [
+ { name: 'step', from: 'A', to: 'B' },
+ { name: 'step', from: 'B', to: 'C' },
+ { name: 'step', from: 'C', to: 'D' }
+ ],
+ plugins: [
+ new StateMachineHistory() // <-- plugin enabled here
+ ]
+ })
+
+ fsm.history; // [ 'A' ]
+ fsm.step();
+ fsm.history; // [ 'A', 'B' ]
+ fsm.step();
+ fsm.history; // [ 'A', 'B', 'C' ]
+
+ fsm.clearHistory();
+
+ fsm.history; // [ ]
+
+```
+## Traversing History
+
+You can traverse back through history using the `historyBack` and `historyForward` methods:
+
+```javascript
+ var fsm = new StateMachine({
+ init: 'A',
+ transitions: [
+ { name: 'step', from: 'A', to: 'B' },
+ { name: 'step', from: 'B', to: 'C' },
+ { name: 'step', from: 'C', to: 'D' }
+ ]
+ })
+
+ fsm.step();
+ fsm.step();
+ fsm.step();
+
+ fsm.state; // 'D'
+ fsm.history; // [ 'A', 'B', 'C', 'D' ]
+
+ fsm.historyBack();
+
+ fsm.state; // 'C'
+ fsm.history; // [ 'A', 'B', 'C' ]
+
+ fsm.historyBack();
+
+ fsm.state; // 'B'
+ fsm.history; // [ 'A', 'B' ]
+
+ fsm.historyForward();
+
+ fsm.state; // 'C'
+ fsm.history; // [ 'A', 'B', 'C' ]
+```
+
+You can test if history traversal is allowed using the following properties:
+
+```javascript
+ fsm.canHistoryBack; // true/false
+ fsm.canHistoryForward; // true/false
+```
+
+A full set of [Lifecycle Events](lifecycle-events.md) will still apply when traversing history with
+`historyBack` and `historyForward`.
+
+## Limiting History
+
+By default, the state machine history is unbounded and will continue to grow until cleared. You
+can limit storage to only the last N states by configuring the plugin:
+
+``` javascript
+ var fsm = new StateMachine({
+ plugins: [
+ new StateMachineHistory({ max: 100 }) // <-- plugin configuration
+ ]
+ })
+```
+
+## Customizing History
+
+If the `history` terminology clashes with your existing state machine attributes or methods, you
+can enable the plugin with a different name:
+
+```javascript
+ var fsm = new StateMachine({
+ init: 'A',
+ transitions: [
+ { name: 'step', from: 'A', to: 'B' },
+ { name: 'step', from: 'B', to: 'C' },
+ { name: 'step', from: 'C', to: 'D' }
+ ],
+ plugins: [
+ new StateMachineHistory({ name: 'memory' })
+ ]
+ })
+
+ fsm.step();
+ fsm.step();
+
+ fsm.memory; // [ 'A', 'B', 'C' ]
+
+ fsm.memoryBack();
+ fsm.memory; // [ 'A', 'B' ]
+
+ fsm.memoryForward();
+ fsm.memory; // [ 'A', 'B', 'C' ]
+
+ fsm.clearMemory();
+ fsm.memory; // [ ]
+```
+
diff --git a/docs/state-machine-factory.md b/docs/state-machine-factory.md
new file mode 100644
index 0000000..9f5e906
--- /dev/null
+++ b/docs/state-machine-factory.md
@@ -0,0 +1,104 @@
+# State Machine Factory
+
+Most examples in this documentation construct a single state machine instance, for example:
+
+```javascript
+ var fsm = new StateMachine({
+ init: 'solid',
+ transitions: [
+ { name: 'melt', from: 'solid', to: 'liquid' },
+ { name: 'freeze', from: 'liquid', to: 'solid' },
+ { name: 'vaporize', from: 'liquid', to: 'gas' },
+ { name: 'condense', from: 'gas', to: 'liquid' }
+ ]
+ });
+```
+
+If you wish to construct multiple instances using the same configuration you should use a State
+Machine Factory. A State Machine Factory provides a javascript constructor function (e.g. a 'class')
+that can be instantiated multiple times:
+
+```javascript
+ var Matter = StateMachine.factory({ // <-- the factory is constructed here
+ init: 'solid',
+ transitions: [
+ { name: 'melt', from: 'solid', to: 'liquid' },
+ { name: 'freeze', from: 'liquid', to: 'solid' },
+ { name: 'vaporize', from: 'liquid', to: 'gas' },
+ { name: 'condense', from: 'gas', to: 'liquid' }
+ ]
+ });
+
+ var a = new Matter(), // <-- instances are constructed here
+ b = new Matter(),
+ c = new Matter();
+
+ b.melt();
+ c.melt();
+ c.vaporize();
+
+ a.state; // solid
+ b.state; // liquid
+ c.state; // gas
+```
+
+Using the factory, each state machine instance is a unique javascript object. Each instance manages
+its own `state` property, but methods are shared via the normal javascript prototype mechanism.
+
+> NOTE: be aware of special case handling required for [Data and State Machine Factories](data-and-methods.md#data-and-state-machine-factories)
+
+## Applying State Machine Behavior to Existing Objects
+
+Occasionally, you may wish to apply state machine behavior to an already existing
+object (e.g. a react component). You can achieve this using the `StateMachine.apply` method:
+
+```javascript
+ var component = { /* ... */ };
+
+ StateMachine.apply(component, {
+ init: 'A',
+ transitions: {
+ { name: 'step', from: 'A', to: 'B' }
+ }
+ });
+```
+
+> Be careful not to use state or transition names that will clash with existing object properties.
+
+## Applying State Machine Factory Behavior to Existing Classes
+
+You can also apply state machine factory behavior to an existing class, however you must now
+take responsibility for initialization by calling `this._fsm()` from within your class
+constructor method:
+
+```javascript
+ function Person(name) {
+ this.name = name;
+ this._fsm(); // <-- IMPORTANT
+ }
+
+ Person.prototype = {
+ speak: function() {
+ console.log('my name is ' + this.name + ' and I am ' + this.state);
+ }
+ }
+
+ StateMachine.factory(Person, {
+ init: 'idle',
+ transitions: {
+ { name: 'sleep', from: 'idle', to: 'sleeping' },
+ { name: 'wake', from: 'sleeping', to: 'idle' }
+ }
+ });
+
+ var amy = new Person('amy'),
+ bob = new Person('bob');
+
+ bob.sleep();
+
+ amy.state; // 'idle'
+ bob.state; // 'sleeping'
+
+ amy.speak(); // 'my name is amy and I am idle'
+ bob.speak(); // 'my name is bob and I am sleeping'
+```
diff --git a/docs/states-and-transitions.md b/docs/states-and-transitions.md
new file mode 100644
index 0000000..0581379
--- /dev/null
+++ b/docs/states-and-transitions.md
@@ -0,0 +1,156 @@
+# States and Transitions
+
+
+
+A state machine consists of a set of **states**, e.g:
+
+ * solid
+ * liquid
+ * gas
+
+.. and a set of **transitions**, e.g:
+
+ * melt
+ * freeze
+ * vaporize
+ * condense
+
+```javascript
+ var fsm = new StateMachine({
+ init: 'solid',
+ transitions: [
+ { name: 'melt', from: 'solid', to: 'liquid' },
+ { name: 'freeze', from: 'liquid', to: 'solid' },
+ { name: 'vaporize', from: 'liquid', to: 'gas' },
+ { name: 'condense', from: 'gas', to: 'liquid' }
+ ]
+ });
+
+ fsm.state; // 'solid'
+ fsm.melt();
+ fsm.state; // 'liquid'
+ fsm.vaporize();
+ fsm.state; // 'gas'
+```
+
+## Multiple states for a transition
+
+
+
+If a transition is allowed `from` multiple states then declare the transitions with the same name:
+
+```javascript
+ { name: 'step', from: 'A', to: 'B' },
+ { name: 'step', from: 'B', to: 'C' },
+ { name: 'step', from: 'C', to: 'D' }
+```
+
+If a transition with multiple `from` states always transitions `to` the same state, e.g:
+
+```javascript
+ { name: 'reset', from: 'B', to: 'A' },
+ { name: 'reset', from: 'C', to: 'A' },
+ { name: 'reset', from: 'D', to: 'A' }
+```
+
+... then it can be abbreviated using an array of `from` states:
+
+```javascript
+ { name: 'reset', from: [ 'B', 'C', 'D' ], to: 'A' }
+```
+
+Combining these into a single example:
+
+```javascript
+ var fsm = new StateMachine({
+ init: 'A',
+ transitions: [
+ { name: 'step', from: 'A', to: 'B' },
+ { name: 'step', from: 'B', to: 'C' },
+ { name: 'step', from: 'C', to: 'D' },
+ { name: 'reset', from: [ 'B', 'C', 'D' ], to: 'A' }
+ ]
+ })
+```
+
+This example will create an object with 2 transition methods:
+
+ * `fsm.step()`
+ * `fsm.reset()`
+
+The `reset` transition will always end up in the `A` state, while the `step` transition
+will end up in a state that is dependent on the current state.
+
+## Wildcard Transitions
+
+If a transition is appropriate from **any** state, then a wildcard '*' `from` state can be used:
+
+```javascript
+ var fsm = new StateMachine({
+ transitions: [
+ // ...
+ { name: 'reset', from: '*', to: 'A' }
+ ]
+ });
+```
+
+## Conditional Transitions
+
+A transition can choose the target state at run-time by providing a function as the `to` attribute:
+
+```javascript
+ var fsm = new StateMachine({
+ init: 'A',
+ transitions: [
+ { name: 'step', from: '*', to: function(n) { return increaseCharacter(this.state, n || 1) } }
+ ]
+ });
+
+ fsm.state; // A
+ fsm.step();
+ fsm.state; // B
+ fsm.step(5);
+ fsm.state; // G
+
+ // helper method to perform (c = c + n) on the 1st character in str
+ function increaseCharacter(str, n) {
+ return String.fromCharCode(str.charCodeAt(0) + n);
+ }
+```
+
+The `allStates` method will only include conditional states once they have been seen at run-time:
+
+```javascript
+ fsm.state; // A
+ fsm.allStates(); // [ 'A' ]
+ fsm.step();
+ fsm.state; // B
+ fsm.allStates(); // [ 'A', 'B' ]
+ fsm.step(5);
+ fsm.state; // G
+ fsm.allStates(); // [ 'A', 'B', 'G' ]
+```
+
+## GOTO - Changing State Without a Transition
+
+You can use a conditional transition, combined with a wildcard `from`, to implement
+arbitrary `goto` behavior:
+
+```javascript
+ var fsm = new StateMachine({
+ init: 'A'
+ transitions: [
+ { name: 'step', from: 'A', to: 'B' },
+ { name: 'step', from: 'B', to: 'C' },
+ { name: 'step', from: 'C', to: 'D' },
+ { name: 'goto', from: '*', to: function(s) { return s } }
+ ]
+ })
+
+ fsm.state; // 'A'
+ fsm.goto('D');
+ fsm.state; // 'D'
+```
+
+A full set of [Lifecycle Events](lifecycle-events.md) still apply when using `goto`.
+
diff --git a/docs/upgrading-from-v2.md b/docs/upgrading-from-v2.md
new file mode 100644
index 0000000..42f8150
--- /dev/null
+++ b/docs/upgrading-from-v2.md
@@ -0,0 +1,378 @@
+# Upgrading from Version 2.x
+
+Version 3.0 is a significant rewrite from earlier versions in order to support more
+advanced use cases and to improve the existing use cases. Unfortunately, many of these
+updates are incompatible with earlier versions, so changes are required in your code when you upgrade
+to version 3.x. We want to tackle those all in one swoop and avoid any more big-bang changes
+in the future.
+
+Please read this article carefully if you are upgrading from version 2.x to 3.x.
+
+> A [summary](#upgrade-summary) of the changes required can be found at the end of the article.
+
+### Table of Contents
+
+ * [**Construction**](#construction) - constructing single instances follows a more idomatic javascript pattern.
+ * [**State Machine Factory**](#state-machine-factory) - constructing multiple instances from a class has been simplified.
+ * [**Data and Methods**](#data-and-methods) - A state machine can now have additional data and methods.
+ * [**Renamed Terminology**](#renamed-terminology) - A more consistent terminology has been applied.
+ * [**Lifecycle Events**](#lifecycle-events) - (previously called 'callbacks') are camelCased and observable.
+ * [**Async Transitions**](#promise-based-asynchronous-transitions) - Asynchronous transitions now use standard Promises.
+ * [**Conditional Transitions**](#conditional-transitions) - A transition can now dynamically choose its target state at run-time.
+ * [**Goto**](#goto) - The state can be changed without a defined transition using `goto`.
+ * [**State History**](#state-history) - The state history can now be retained and traversed with back/forward semantics.
+ * [**Visualization**](#visualization) - A state machine can now be visualized using GraphViz.
+ * [**Build System**](#build-system) - A new webpack-based build system has been implemented.
+
+## Construction
+
+Constructing a single state machine now follows a more idiomatic javascript pattern:
+
+Version 2.x:
+
+```javascript
+ var fsm = StateMachine.create({ /* ... */ })
+```
+
+**Version 3.x**:
+
+```javascript
+ var fsm = new StateMachine({ /* ... */ }) // <-- more idomatic
+```
+
+## State Machine Factory
+
+Constructing multiple instances from a state machine 'class' has been simplified:
+
+Version 2.x:
+
+```javascript
+ function FSM() { }
+
+ StateMachine.create({
+ target: FSM.prototype,
+ // ...
+ })
+
+ var a = new FSM(),
+ b = new FSM();
+```
+
+**Version 3.x**:
+
+```javascript
+ var FSM = StateMachine.factory({ /* ... */ }), // <-- generate a factory (a constructor function)
+ a = new FSM(), // <-- then create instances
+ b = new FSM();
+```
+
+## Data and Methods
+
+A state machine can now have additional (arbitrary) data and methods defined:
+
+Version 2.x: _not supported_.
+
+**Version 3.x**:
+
+```javascript
+ var fsm = new StateMachine({
+ data: {
+ color: 'red'
+ },
+ methods: {
+ speak: function() { console.log('hello') }
+ }
+ });
+
+ fsm.color; // 'red'
+ fsm.speak(); // 'hello'
+```
+
+## Renamed Terminology
+
+A more consistent terminology has been applied:
+
+ * A state machine consists of a set of [**States**](states-and-transitions.md).
+ * A state machine changes state by using [**Transitions**](states-and-transitions.md).
+ * A state machine can perform actions during a transition by observing [**Lifecycle Events**](lifecycle-events.md).
+ * A state machine can also have arbitrary [**Data and Methods**](data-and-methods.md).
+
+Version 2.x:
+
+```javascript
+ var fsm = StateMachine.create({
+ initial: 'ready',
+ events: [ /* ... */ ],
+ callbacks: { /* ... */ }
+ });
+
+ fsm.current; // 'ready'
+```
+
+**Version 3.x**:
+
+```javascript
+ var fsm = new StateMachine({
+ init: 'ready', // <-- renamed s/initial/init/
+ transitions: [ /* ... */ ], // <-- renamed s/events/transitions/
+ data: { /* ... */ }, // <-- new
+ methods: { /* ... */ } // <-- renamed s/callbacks/methods/
+ // ... which can contain arbitrary methods AND lifecycle event callbacks
+ });
+
+ fsm.state; // 'ready' // <-- renamed s/current/state/
+```
+
+## Lifecycle Events
+
+**Callbacks** have been renamed **Lifecycle Events** and are now declared as `methods` on the
+state machine using a more traditional javascript camelCase for the method names:
+
+Version 2.x:
+
+```javascript
+ var fsm = StateMachine.create({
+ initial: 'initial-state',
+ events: [
+ { name: 'do-something', from: 'initial-state', to: 'final-state' }
+ ],
+ callbacks: {
+ onbeforedosomething: function() { /* ... */ },
+ onleaveinitialstate: function() { /* ... */ },
+ onenterfinalstate: function() { /* ... */ },
+ onafterdosomething: function() { /* ... */ }
+ }
+ })
+```
+
+**Version 3.x**:
+
+```javascript
+ var fsm = new StateMachine({
+ init: 'initial-state',
+ transitions: [
+ { name: 'do-something', from: 'initial-state', to: 'final-state' }
+ ],
+ methods: { // <-- renamed s/callbacks/methods/
+ onBeforeDoSomething: function() { /* ... */ }, // <-- camelCase naming convention
+ onLeaveInitialState: function() { /* ... */ }, // <--
+ onEnterFinalState: function() { /* ... */ }, // <--
+ onAfterDoSomething: function() { /* ... */ } // <--
+ }
+ })
+```
+
+
+Lifecycle events are now passed information in a single `lifecycle` argument:
+
+Version 2.x:
+
+```javascript
+ var fsm = StateMachine.create({
+ events: [
+ { name: 'step', from: 'none', to: 'complete' }
+ ],
+ callbacks: {
+ onbeforestep: function(event, from, to) {
+ console.log('event: ' + event); // 'step'
+ console.log('from: ' + from); // 'none'
+ console.log('to: ' + to); // 'complete'
+ },
+ }
+ });
+```
+
+**Version 3.x**:
+
+```javascript
+ var fsm = new StateMachine({
+ transitions: [
+ { name: 'step', from: 'none', to: 'complete' }
+ ],
+ methods: {
+ onBeforeStep: function(lifecycle) { // <-- combined into a single argument
+ console.log('transition: ' + lifecycle.transition); // 'step'
+ console.log('from: ' + lifecycle.from); // 'none'
+ console.log('to: ' + lifecycle.to); // 'complete'
+ }
+ }
+ });
+```
+
+> This change allows us to include additional information in the future without having to have a ridiculous
+number of arguments to lifecycle event observer methods
+
+
+Lifecycle events are also now observable by others:
+
+Version 2.x: _not supported_.
+
+**Version 3.x**:
+
+```javascript
+ var fsm = new StateMachine({ /* ... */ });
+
+ // observe individual lifecycle events with observer methods
+ fsm.observe('onBeforeTransition', function() { /* ... */ });
+ fsm.observe('onLeaveState', function() { /* ... */ });
+
+ // or observe multiple lifecycle events with an observer object
+ fsm.observe({
+ onBeforeTransition: function() { /* ... */ },
+ onLeaveState: function() { /* ... */ }
+ });
+```
+
+
+The general purpose lifecycle events now use the word `transition` instead of `event` and
+occur **before** their specialized versions:
+
+Version 2.x, the lifecycle order was:
+
+ * `onbefore`
+ * `onbeforeevent`
+ * `onleave`
+ * `onleavestate`
+ * `onenter`
+ * `onenterstate`
+ * `on`
+ * `onafter`
+ * `onafterevent`
+ * `on`
+
+**Version 3.x**, the lifecycle order is:
+
+ * `onBeforeTransition` - fired before any transition
+ * `onBefore` - fired before a specific TRANSITION
+ * `onLeaveState` - fired when leaving any state
+ * `onLeave` - fired when leaving a specific STATE
+ * `onTransition` - fired during any transition
+ * `onEnterState` - fired when entering any state
+ * `onEnter` - fired when entering a specific STATE
+ * `on` - convenience shorthand for `onEnter`
+ * `onAfterTransition` - fired after any transition
+ * `onAfter` - fired after a specific TRANSITION
+ * `on` - convenience shorthand for `onAfter`
+
+> For more details, read [Lifecycle Events](lifecycle-events.md)
+
+## Promise-Based Asynchronous Transitions
+
+Asynchronous transitions are now implemented using standard javascript [Promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise).
+
+If you return a Promise from **any** lifecycle event then the entire lifecycle for that transition
+is put on hold until that Promise gets resolved. If the promise is rejected then the transition
+is cancelled.
+
+Version 2.x:
+
+```javascript
+ var fsm = StateMachine.create({
+ events: [
+ { name: 'step', from: 'none', to: 'complete' }
+ ],
+ callbacks: {
+ onbeforestep: function() {
+ $('#ui').fadeOut('fast', function() {
+ fsm.transition();
+ });
+ return StateMachine.ASYNC;
+ }
+ }
+ });
+```
+
+**Version 3.x**:
+
+```javascript
+ var fsm = new StateMachine({
+ transitions: [
+ { name: 'step', from: 'none', to: 'complete' }
+ ],
+ methods: {
+ onBeforeStep: function() {
+ return new Promise(function(resolve, reject) { // <-- return a Promise instead of StateMachine.ASYNC
+ $('#ui').fadeOut('fast', resolve); // <-- resolve the promise instead of calling .transition()
+ });
+ }
+ }
+ });
+```
+
+> For more details, read [Asynchronous Transitions](async-transitions.md)
+
+## Conditional Transitions
+
+A transition can now be conditional and choose the target state at run-time by providing a function
+as the `to` attribute.
+
+Version 2.x: _not supported_.
+
+**Version 3.x**: See [Conditional Transitions](states-and-transitions.md#conditional-transitions)
+
+## Goto
+
+The state can now be changed without the need for a predefined transition using a conditional `goto`
+transition:
+
+Version 2.x: _not_supported_.
+
+**Version 3.x**: See [Goto](states-and-transitions.md#goto---changing-state-without-a-transition)
+
+## State History
+
+A state machine can now track and traverse (back/forward) its state history.
+
+Version 2.x: _not supported_.
+
+**Version 3.x**: See [State History](state-history.md)
+
+## Visualization
+
+A state machine can now be visualized as a directed graph using GraphViz `.dot` syntax.
+
+Version 2.x: _not_supported_.
+
+**Version 3.x**: See [Visualization](visualization.md)
+
+## Build System
+
+A new [Webpack](https://webpack.js.org/concepts/) based build system has been provided along
+with an [Ava](https://github.com/avajs/ava) based unit test suite.
+
+Version 2.x: _not_supported_.
+
+**Version 3.x**: See [Contributing](contributing.md)
+
+## Other Breaking Changes in Version 3.0
+
+`isFinished` is no longer built-in, you can easily add it to your state machine with a custom method:
+
+```javascript
+ var fsm = new StateMachine({
+ methods: {
+ isFinished: function() { return this.state === 'done' }
+ }
+ })
+```
+
+# UPGRADE SUMMARY
+
+The following list summarizes the above changes you might need when upgrading to version 3.0
+
+ * replace `StateMachine.create()` with `new StateMachine()`
+ * rename:
+ * `initial` to `init`
+ * `events` to `transitions`
+ * `callbacks` to `methods`
+ * `fsm.current` to `fsm.state`
+ * update your callback methods:
+ * rename them to use traditional javascript `camelCasing`
+ * refactor them to use the single `lifecycle` argument instead of individual `event,from,to` arguments
+ * update any asynchronous callback methods:
+ * return a `Promise` instead of `StateMachine.ASYNC`
+ * `resolve()` the promise when ready instead of calling `fsm.transition()`
+ * replace `StateMachine.create({ target: FOO })` with:
+ * if FOO is a class - `StateMachine.factory(FOO, {})`
+ * if FOO is an object - `StateMachine.apply(FOO, {})`
+
diff --git a/docs/visualization.md b/docs/visualization.md
new file mode 100644
index 0000000..e944dcf
--- /dev/null
+++ b/docs/visualization.md
@@ -0,0 +1,211 @@
+# Visualization
+
+It can be very helpful to visualize your state machine as a directed graph. This is possible
+with the open source [GraphViz](http://www.graphviz.org/) library if we convert from our
+state machine configuration to the `.dot` language expected by GraphViz using the
+`visualize` method:
+
+```javascript
+ var visualize = require('javascript-state-machine/lib/visualize');
+
+ var fsm = new StateMachine({
+ init: 'open',
+ transitions: [
+ { name: 'close', from: 'open', to: 'closed' },
+ { name: 'open', from: 'closed', to: 'open' }
+ ]
+ });
+
+ visualize(fsm)
+```
+
+Generates the following .dot syntax:
+
+```dot
+ digraph "fsm" {
+ "closed";
+ "open";
+ "closed" -> "open" [ label=" open " ];
+ "open" -> "closed" [ label=" close " ];
+ }
+```
+
+Which GraphViz displays as:
+
+
+
+## Enhanced Visualization
+
+You can customize the generated `.dot` output - and hence the graphviz visualization - by attaching
+`dot` attributes to your transitions and (optionally) declaring an `orientation`:
+
+```javascript
+ var fsm = new StateMachine({
+ init: 'closed',
+ transitions: [
+ { name: 'open', from: 'closed', to: 'open', dot: { color: 'blue', headport: 'n', tailport: 'n' } },
+ { name: 'close', from: 'open', to: 'closed', dot: { color: 'red', headport: 's', tailport: 's' } }
+ ]
+ });
+ visualize(fsm, { name: 'door', orientation: 'horizontal' });
+```
+
+Generates the following (enhanced) `.dot` syntax:
+
+```dot
+ digraph "door" {
+ rankdir=LR;
+ "closed";
+ "open";
+ "closed" -> "open" [ color="blue" ; headport="n" ; label=" open " ; tailport="n" ];
+ "open" -> "closed" [ color="red" ; headport="s" ; label=" close " ; tailport="s" ];
+ }
+```
+
+Which GraphViz displays as:
+
+
+
+## Visualizing State Machine Factories
+
+You can use the same `visualize` method to generate `.dot` output for a state machine factory:
+
+```javascript
+ var Matter = StateMachine.factory({
+ init: 'solid',
+ transitions: [
+ { name: 'melt', from: 'solid', to: 'liquid', dot: { headport: 'nw' } },
+ { name: 'freeze', from: 'liquid', to: 'solid', dot: { headport: 'se' } },
+ { name: 'vaporize', from: 'liquid', to: 'gas', dot: { headport: 'nw' } },
+ { name: 'condense', from: 'gas', to: 'liquid', dot: { headport: 'se' } }
+ ]
+ });
+
+ visualize(Matter, { name: 'matter', orientation: 'horizontal' })
+```
+
+Generates the following .dot syntax:
+
+```dot
+ digraph "matter" {
+ rankdir=LR;
+ "solid";
+ "liquid";
+ "gas";
+ "solid" -> "liquid" [ headport="nw" ; label=" melt " ];
+ "liquid" -> "solid" [ headport="se" ; label=" freeze " ];
+ "liquid" -> "gas" [ headport="nw" ; label=" vaporize " ];
+ "gas" -> "liquid" [ headport="se" ; label=" condense " ];
+ }
+```
+
+Which GraphViz displays as:
+
+
+
+## Other Examples
+
+```javascript
+ var Wizard = StateMachine.factory({
+ init: 'A',
+ transitions: [
+ { name: 'step', from: 'A', to: 'B', dot: { headport: 'w', tailport: 'ne' } },
+ { name: 'step', from: 'B', to: 'C', dot: { headport: 'w', tailport: 'e' } },
+ { name: 'step', from: 'C', to: 'D', dot: { headport: 'w', tailport: 'e' } },
+ { name: 'reset', from: [ 'B', 'C', 'D' ], to: 'A', dot: { headport: 'se', tailport: 's' } }
+ ]
+ });
+
+ visualize(Wizard, { orientation: 'horizontal' })
+```
+
+Generates:
+
+```dot
+ digraph "wizard" {
+ rankdir=LR;
+ "A";
+ "B";
+ "C";
+ "D";
+ "A" -> "B" [ headport="w" ; label=" step " ; tailport="ne" ];
+ "B" -> "C" [ headport="w" ; label=" step " ; tailport="e" ];
+ "C" -> "D" [ headport="w" ; label=" step " ; tailport="e" ];
+ "B" -> "A" [ headport="se" ; label=" reset " ; tailport="s" ];
+ "C" -> "A" [ headport="se" ; label=" reset " ; tailport="s" ];
+ "D" -> "A" [ headport="se" ; label=" reset " ; tailport="s" ];
+ }
+```
+
+Displays:
+
+
+
+```javascript
+ var ATM = StateMachine.factory({
+ init: 'ready',
+ transitions: [
+ { name: 'insert-card', from: 'ready', to: 'pin' },
+ { name: 'confirm', from: 'pin', to: 'action' },
+ { name: 'reject', from: 'pin', to: 'return-card' },
+ { name: 'withdraw', from: 'return-card', to: 'ready' },
+
+ { name: 'deposit', from: 'action', to: 'deposit-account' },
+ { name: 'provide', from: 'deposit-account', to: 'deposit-amount' },
+ { name: 'provide', from: 'deposit-amount', to: 'confirm-deposit' },
+ { name: 'confirm', from: 'confirm-deposit', to: 'collect-envelope' },
+ { name: 'provide', from: 'collect-envelope', to: 'continue' },
+
+ { name: 'withdraw', from: 'action', to: 'withdrawal-account' },
+ { name: 'provide', from: 'withdrawal-account', to: 'withdrawal-amount' },
+ { name: 'provide', from: 'withdrawal-amount', to: 'confirm-withdrawal' },
+ { name: 'confirm', from: 'confirm-withdrawal', to: 'dispense-cash' },
+ { name: 'withdraw', from: 'dispense-cash', to: 'continue' },
+
+ { name: 'continue', from: 'continue', to: 'action' },
+ { name: 'finish', from: 'continue', to: 'return-card' }
+ ]
+ })
+
+ visualize(ATM)
+```
+
+Generates:
+
+```dot
+ digraph "ATM" {
+ "ready";
+ "pin";
+ "action";
+ "return-card";
+ "deposit-account";
+ "deposit-amount";
+ "confirm-deposit";
+ "collect-envelope";
+ "continue";
+ "withdrawal-account";
+ "withdrawal-amount";
+ "confirm-withdrawal";
+ "dispense-cash";
+ "ready" -> "pin" [ label=" insert-card " ];
+ "pin" -> "action" [ label=" confirm " ];
+ "pin" -> "return-card" [ label=" reject " ];
+ "return-card" -> "ready" [ label=" withdraw " ];
+ "action" -> "deposit-account" [ label=" deposit " ];
+ "deposit-account" -> "deposit-amount" [ label=" provide " ];
+ "deposit-amount" -> "confirm-deposit" [ label=" provide " ];
+ "confirm-deposit" -> "collect-envelope" [ label=" confirm " ];
+ "collect-envelope" -> "continue" [ label=" provide " ];
+ "action" -> "withdrawal-account" [ label=" withdraw " ];
+ "withdrawal-account" -> "withdrawal-amount" [ label=" provide " ];
+ "withdrawal-amount" -> "confirm-withdrawal" [ label=" provide " ];
+ "confirm-withdrawal" -> "dispense-cash" [ label=" confirm " ];
+ "dispense-cash" -> "continue" [ label=" withdraw " ];
+ "continue" -> "action" [ label=" continue " ];
+ "continue" -> "return-card" [ label=" finish " ];
+ }
+```
+
+Displays:
+
+
diff --git a/examples/atm.dot b/examples/atm.dot
new file mode 100644
index 0000000..9f76543
--- /dev/null
+++ b/examples/atm.dot
@@ -0,0 +1,31 @@
+digraph "ATM" {
+ "ready";
+ "pin";
+ "action";
+ "return-card";
+ "deposit-account";
+ "deposit-amount";
+ "confirm-deposit";
+ "collect-envelope";
+ "continue";
+ "withdrawal-account";
+ "withdrawal-amount";
+ "confirm-withdrawal";
+ "dispense-cash";
+ "ready" -> "pin" [ label=" insert-card " ];
+ "pin" -> "action" [ label=" confirm " ];
+ "pin" -> "return-card" [ label=" reject " ];
+ "return-card" -> "ready" [ label=" withdraw " ];
+ "action" -> "deposit-account" [ label=" deposit " ];
+ "deposit-account" -> "deposit-amount" [ label=" provide " ];
+ "deposit-amount" -> "confirm-deposit" [ label=" provide " ];
+ "confirm-deposit" -> "collect-envelope" [ label=" confirm " ];
+ "collect-envelope" -> "continue" [ label=" provide " ];
+ "action" -> "withdrawal-account" [ label=" withdraw " ];
+ "withdrawal-account" -> "withdrawal-amount" [ label=" provide " ];
+ "withdrawal-amount" -> "confirm-withdrawal" [ label=" provide " ];
+ "confirm-withdrawal" -> "dispense-cash" [ label=" confirm " ];
+ "dispense-cash" -> "continue" [ label=" withdraw " ];
+ "continue" -> "action" [ label=" continue " ];
+ "continue" -> "return-card" [ label=" finish " ];
+}
\ No newline at end of file
diff --git a/examples/atm.js b/examples/atm.js
new file mode 100644
index 0000000..0113e55
--- /dev/null
+++ b/examples/atm.js
@@ -0,0 +1,33 @@
+var StateMachine = require('../src/app'),
+ visualize = require('../src/plugin/visualize');
+
+var ATM = StateMachine.factory({
+ init: 'ready',
+ transitions: [
+ { name: 'insert-card', from: 'ready', to: 'pin' },
+ { name: 'confirm', from: 'pin', to: 'action' },
+ { name: 'reject', from: 'pin', to: 'return-card' },
+ { name: 'withdraw', from: 'return-card', to: 'ready' },
+
+ { name: 'deposit', from: 'action', to: 'deposit-account' },
+ { name: 'provide', from: 'deposit-account', to: 'deposit-amount' },
+ { name: 'provide', from: 'deposit-amount', to: 'confirm-deposit' },
+ { name: 'confirm', from: 'confirm-deposit', to: 'collect-envelope' },
+ { name: 'provide', from: 'collect-envelope', to: 'continue' },
+
+ { name: 'withdraw', from: 'action', to: 'withdrawal-account' },
+ { name: 'provide', from: 'withdrawal-account', to: 'withdrawal-amount' },
+ { name: 'provide', from: 'withdrawal-amount', to: 'confirm-withdrawal' },
+ { name: 'confirm', from: 'confirm-withdrawal', to: 'dispense-cash' },
+ { name: 'withdraw', from: 'dispense-cash', to: 'continue' },
+
+ { name: 'continue', from: 'continue', to: 'action' },
+ { name: 'finish', from: 'continue', to: 'return-card' }
+ ]
+})
+
+ATM.visualize = function() {
+ return visualize(ATM, { name: 'ATM' })
+}
+
+module.exports = ATM
diff --git a/examples/atm.png b/examples/atm.png
new file mode 100644
index 0000000..a699d39
Binary files /dev/null and b/examples/atm.png differ
diff --git a/examples/atm.svg b/examples/atm.svg
new file mode 100644
index 0000000..e3bf071
--- /dev/null
+++ b/examples/atm.svg
@@ -0,0 +1,174 @@
+
+
+
+
+
diff --git a/demo/demo.css b/examples/demo/demo.css
similarity index 100%
rename from demo/demo.css
rename to examples/demo/demo.css
diff --git a/examples/demo/demo.js b/examples/demo/demo.js
new file mode 100644
index 0000000..f2b2b95
--- /dev/null
+++ b/examples/demo/demo.js
@@ -0,0 +1,84 @@
+Demo = function() {
+
+ var output = document.getElementById('output'),
+ demo = document.getElementById('demo'),
+ panic = document.getElementById('panic'),
+ warn = document.getElementById('warn'),
+ calm = document.getElementById('calm'),
+ clear = document.getElementById('clear'),
+ count = 0;
+
+ var log = function(msg, separate) {
+ count = count + (separate ? 1 : 0);
+ output.value = count + ": " + msg + "\n" + (separate ? "\n" : "") + output.value;
+ refreshUI();
+ };
+
+ var refreshUI = function() {
+ setTimeout(function() {
+ demo.className = fsm.state;
+ panic.disabled = fsm.cannot('panic', true);
+ warn.disabled = fsm.cannot('warn', true);
+ calm.disabled = fsm.cannot('calm', true);
+ clear.disabled = fsm.cannot('clear', true);
+ }, 0); // defer until end of current tick to allow fsm to complete transaction
+ };
+
+ var fsm = new StateMachine({
+
+ transitions: [
+ { name: 'start', from: 'none', to: 'green' },
+ { name: 'warn', from: 'green', to: 'yellow' },
+ { name: 'panic', from: 'green', to: 'red' },
+ { name: 'panic', from: 'yellow', to: 'red' },
+ { name: 'calm', from: 'red', to: 'yellow' },
+ { name: 'clear', from: 'red', to: 'green' },
+ { name: 'clear', from: 'yellow', to: 'green' },
+ ],
+
+ methods: {
+
+ onBeforeTransition: function(lifecycle) {
+ log("BEFORE: " + lifecycle.transition, true);
+ },
+
+ onLeaveState: function(lifecycle) {
+ log("LEAVE: " + lifecycle.from);
+ },
+
+ onEnterState: function(lifecycle) {
+ log("ENTER: " + lifecycle.to);
+ },
+
+ onAfterTransition: function(lifecycle) {
+ log("AFTER: " + lifecycle.transition);
+ },
+
+ onTransition: function(lifecycle) {
+ log("DURING: " + lifecycle.transition + " (from " + lifecycle.from + " to " + lifecycle.to + ")");
+ },
+
+ onLeaveRed: function(lifecycle) {
+ return new Promise(function(resolve, reject) {
+ var msg = lifecycle.transition + ' to ' + lifecycle.to;
+ log("PENDING " + msg + " in ...3");
+ setTimeout(function() {
+ log("PENDING " + msg + " in ...2");
+ setTimeout(function() {
+ log("PENDING " + msg + " in ...1");
+ setTimeout(function() {
+ resolve();
+ }, 1000);
+ }, 1000);
+ }, 1000);
+ });
+ }
+
+ }
+ });
+
+ fsm.start();
+ return fsm;
+
+}();
+
diff --git a/demo/images/alerts.green.png b/examples/demo/images/alerts.green.png
similarity index 100%
rename from demo/images/alerts.green.png
rename to examples/demo/images/alerts.green.png
diff --git a/demo/images/alerts.red.png b/examples/demo/images/alerts.red.png
similarity index 100%
rename from demo/images/alerts.red.png
rename to examples/demo/images/alerts.red.png
diff --git a/demo/images/alerts.yellow.png b/examples/demo/images/alerts.yellow.png
similarity index 100%
rename from demo/images/alerts.yellow.png
rename to examples/demo/images/alerts.yellow.png
diff --git a/examples/horizontal_door.dot b/examples/horizontal_door.dot
new file mode 100644
index 0000000..008113f
--- /dev/null
+++ b/examples/horizontal_door.dot
@@ -0,0 +1,7 @@
+digraph "door" {
+ rankdir=LR;
+ "closed";
+ "open";
+ "closed" -> "open" [ color="blue" ; headport="n" ; label=" open " ; tailport="n" ];
+ "open" -> "closed" [ color="red" ; headport="s" ; label=" close " ; tailport="s" ];
+}
\ No newline at end of file
diff --git a/examples/horizontal_door.js b/examples/horizontal_door.js
new file mode 100644
index 0000000..1a2f676
--- /dev/null
+++ b/examples/horizontal_door.js
@@ -0,0 +1,16 @@
+var StateMachine = require('../src/app'),
+ visualize = require('../src/plugin/visualize');
+
+var Door = StateMachine.factory({
+ init: 'closed',
+ transitions: [
+ { name: 'open', from: 'closed', to: 'open', dot: { color: 'blue', headport: 'n', tailport: 'n' } },
+ { name: 'close', from: 'open', to: 'closed', dot: { color: 'red', headport: 's', tailport: 's' } }
+ ]
+});
+
+Door.visualize = function() {
+ return visualize(Door, { name: 'door', orientation: 'horizontal' })
+}
+
+module.exports = Door
diff --git a/examples/horizontal_door.png b/examples/horizontal_door.png
new file mode 100644
index 0000000..65d8ddb
Binary files /dev/null and b/examples/horizontal_door.png differ
diff --git a/examples/horizontal_door.svg b/examples/horizontal_door.svg
new file mode 100644
index 0000000..217e038
--- /dev/null
+++ b/examples/horizontal_door.svg
@@ -0,0 +1,35 @@
+
+
+
+
+
diff --git a/examples/matter.dot b/examples/matter.dot
new file mode 100644
index 0000000..9a5b12e
--- /dev/null
+++ b/examples/matter.dot
@@ -0,0 +1,10 @@
+digraph "matter" {
+ rankdir=LR;
+ "solid";
+ "liquid";
+ "gas";
+ "solid" -> "liquid" [ headport="nw" ; label=" melt " ];
+ "liquid" -> "solid" [ headport="se" ; label=" freeze " ];
+ "liquid" -> "gas" [ headport="nw" ; label=" vaporize " ];
+ "gas" -> "liquid" [ headport="se" ; label=" condense " ];
+}
\ No newline at end of file
diff --git a/examples/matter.js b/examples/matter.js
new file mode 100644
index 0000000..c3b960f
--- /dev/null
+++ b/examples/matter.js
@@ -0,0 +1,18 @@
+var StateMachine = require('../src/app'),
+ visualize = require('../src/plugin/visualize');
+
+var Matter = StateMachine.factory({
+ init: 'solid',
+ transitions: [
+ { name: 'melt', from: 'solid', to: 'liquid', dot: { headport: 'nw' } },
+ { name: 'freeze', from: 'liquid', to: 'solid', dot: { headport: 'se' } },
+ { name: 'vaporize', from: 'liquid', to: 'gas', dot: { headport: 'nw' } },
+ { name: 'condense', from: 'gas', to: 'liquid', dot: { headport: 'se' } }
+ ]
+});
+
+Matter.visualize = function() {
+ return visualize(Matter, { name: 'matter', orientation: 'horizontal' })
+}
+
+module.exports = Matter
diff --git a/examples/matter.png b/examples/matter.png
new file mode 100644
index 0000000..cde3b89
Binary files /dev/null and b/examples/matter.png differ
diff --git a/examples/matter.svg b/examples/matter.svg
new file mode 100644
index 0000000..9ccd86f
--- /dev/null
+++ b/examples/matter.svg
@@ -0,0 +1,52 @@
+
+
+
+
+
diff --git a/examples/vertical_door.dot b/examples/vertical_door.dot
new file mode 100644
index 0000000..822dad8
--- /dev/null
+++ b/examples/vertical_door.dot
@@ -0,0 +1,6 @@
+digraph "fsm" {
+ "closed";
+ "open";
+ "closed" -> "open" [ label=" open " ];
+ "open" -> "closed" [ label=" close " ];
+}
\ No newline at end of file
diff --git a/examples/vertical_door.js b/examples/vertical_door.js
new file mode 100644
index 0000000..b619195
--- /dev/null
+++ b/examples/vertical_door.js
@@ -0,0 +1,16 @@
+var StateMachine = require('../src/app'),
+ visualize = require('../src/plugin/visualize');
+
+var Door = StateMachine.factory({
+ init: 'closed',
+ transitions: [
+ { name: 'open', from: 'closed', to: 'open' },
+ { name: 'close', from: 'open', to: 'closed' }
+ ]
+});
+
+Door.visualize = function() {
+ return visualize(Door)
+}
+
+module.exports = Door
diff --git a/examples/vertical_door.png b/examples/vertical_door.png
new file mode 100644
index 0000000..c29023d
Binary files /dev/null and b/examples/vertical_door.png differ
diff --git a/examples/vertical_door.svg b/examples/vertical_door.svg
new file mode 100644
index 0000000..12dc09b
--- /dev/null
+++ b/examples/vertical_door.svg
@@ -0,0 +1,35 @@
+
+
+
+
+
diff --git a/examples/wizard.dot b/examples/wizard.dot
new file mode 100644
index 0000000..c7e6f25
--- /dev/null
+++ b/examples/wizard.dot
@@ -0,0 +1,13 @@
+digraph "wizard" {
+ rankdir=LR;
+ "A";
+ "B";
+ "C";
+ "D";
+ "A" -> "B" [ headport="w" ; label=" step " ; tailport="ne" ];
+ "B" -> "C" [ headport="w" ; label=" step " ; tailport="e" ];
+ "C" -> "D" [ headport="w" ; label=" step " ; tailport="e" ];
+ "B" -> "A" [ headport="se" ; label=" reset " ; tailport="s" ];
+ "C" -> "A" [ headport="se" ; label=" reset " ; tailport="s" ];
+ "D" -> "A" [ headport="se" ; label=" reset " ; tailport="s" ];
+}
\ No newline at end of file
diff --git a/examples/wizard.js b/examples/wizard.js
new file mode 100644
index 0000000..8d1aa4c
--- /dev/null
+++ b/examples/wizard.js
@@ -0,0 +1,18 @@
+var StateMachine = require('../src/app'),
+ visualize = require('../src/plugin/visualize');
+
+var Wizard = StateMachine.factory({
+ init: 'A',
+ transitions: [
+ { name: 'step', from: 'A', to: 'B', dot: { headport: 'w', tailport: 'ne' } },
+ { name: 'step', from: 'B', to: 'C', dot: { headport: 'w', tailport: 'e' } },
+ { name: 'step', from: 'C', to: 'D', dot: { headport: 'w', tailport: 'e' } },
+ { name: 'reset', from: [ 'B', 'C', 'D' ], to: 'A', dot: { headport: 'se', tailport: 's' } }
+ ]
+});
+
+Wizard.visualize = function() {
+ return visualize(Wizard, { name: 'wizard', orientation: 'horizontal' })
+}
+
+module.exports = Wizard
diff --git a/examples/wizard.png b/examples/wizard.png
new file mode 100644
index 0000000..74945ca
Binary files /dev/null and b/examples/wizard.png differ
diff --git a/examples/wizard.svg b/examples/wizard.svg
new file mode 100644
index 0000000..46edc96
--- /dev/null
+++ b/examples/wizard.svg
@@ -0,0 +1,69 @@
+
+
+
+
+
diff --git a/index.html b/index.html
index 2d6cb62..7beb6b8 100644
--- a/index.html
+++ b/index.html
@@ -3,7 +3,7 @@
Javascript Finite State Machine
-
+
@@ -32,8 +32,8 @@ Finite State Machine
-
-
+
+