StateMachine.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669
  1. (function webpackUniversalModuleDefinition(root, factory) {
  2. if(typeof exports === 'object' && typeof module === 'object')
  3. module.exports = factory();
  4. else if(typeof define === 'function' && define.amd)
  5. define("StateMachine", [], factory);
  6. else if(typeof exports === 'object')
  7. exports["StateMachine"] = factory();
  8. else
  9. root["StateMachine"] = factory();
  10. })(this, function() {
  11. return /******/ (function(modules) { // webpackBootstrap
  12. /******/ // The module cache
  13. /******/ var installedModules = {};
  14. /******/
  15. /******/ // The require function
  16. /******/ function __webpack_require__(moduleId) {
  17. /******/
  18. /******/ // Check if module is in cache
  19. /******/ if(installedModules[moduleId]) {
  20. /******/ return installedModules[moduleId].exports;
  21. /******/ }
  22. /******/ // Create a new module (and put it into the cache)
  23. /******/ var module = installedModules[moduleId] = {
  24. /******/ i: moduleId,
  25. /******/ l: false,
  26. /******/ exports: {}
  27. /******/ };
  28. /******/
  29. /******/ // Execute the module function
  30. /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
  31. /******/
  32. /******/ // Flag the module as loaded
  33. /******/ module.l = true;
  34. /******/
  35. /******/ // Return the exports of the module
  36. /******/ return module.exports;
  37. /******/ }
  38. /******/
  39. /******/
  40. /******/ // expose the modules object (__webpack_modules__)
  41. /******/ __webpack_require__.m = modules;
  42. /******/
  43. /******/ // expose the module cache
  44. /******/ __webpack_require__.c = installedModules;
  45. /******/
  46. /******/ // identity function for calling harmony imports with the correct context
  47. /******/ __webpack_require__.i = function(value) { return value; };
  48. /******/
  49. /******/ // define getter function for harmony exports
  50. /******/ __webpack_require__.d = function(exports, name, getter) {
  51. /******/ if(!__webpack_require__.o(exports, name)) {
  52. /******/ Object.defineProperty(exports, name, {
  53. /******/ configurable: false,
  54. /******/ enumerable: true,
  55. /******/ get: getter
  56. /******/ });
  57. /******/ }
  58. /******/ };
  59. /******/
  60. /******/ // getDefaultExport function for compatibility with non-harmony modules
  61. /******/ __webpack_require__.n = function(module) {
  62. /******/ var getter = module && module.__esModule ?
  63. /******/ function getDefault() { return module['default']; } :
  64. /******/ function getModuleExports() { return module; };
  65. /******/ __webpack_require__.d(getter, 'a', getter);
  66. /******/ return getter;
  67. /******/ };
  68. /******/
  69. /******/ // Object.prototype.hasOwnProperty.call
  70. /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
  71. /******/
  72. /******/ // __webpack_public_path__
  73. /******/ __webpack_require__.p = "";
  74. /******/
  75. /******/ // Load entry module and return exports
  76. /******/ return __webpack_require__(__webpack_require__.s = 5);
  77. /******/ })
  78. /************************************************************************/
  79. /******/ ([
  80. /* 0 */
  81. /***/ (function(module, exports, __webpack_require__) {
  82. "use strict";
  83. module.exports = function(target, sources) {
  84. var n, source, key;
  85. for(n = 1 ; n < arguments.length ; n++) {
  86. source = arguments[n];
  87. for(key in source) {
  88. if (source.hasOwnProperty(key))
  89. target[key] = source[key];
  90. }
  91. }
  92. return target;
  93. }
  94. /***/ }),
  95. /* 1 */
  96. /***/ (function(module, exports, __webpack_require__) {
  97. "use strict";
  98. //-------------------------------------------------------------------------------------------------
  99. var mixin = __webpack_require__(0);
  100. //-------------------------------------------------------------------------------------------------
  101. module.exports = {
  102. build: function(target, config) {
  103. var n, max, plugin, plugins = config.plugins;
  104. for(n = 0, max = plugins.length ; n < max ; n++) {
  105. plugin = plugins[n];
  106. if (plugin.methods)
  107. mixin(target, plugin.methods);
  108. if (plugin.properties)
  109. Object.defineProperties(target, plugin.properties);
  110. }
  111. },
  112. hook: function(fsm, name, additional) {
  113. var n, max, method, plugin,
  114. plugins = fsm.config.plugins,
  115. args = [fsm.context];
  116. if (additional)
  117. args = args.concat(additional)
  118. for(n = 0, max = plugins.length ; n < max ; n++) {
  119. plugin = plugins[n]
  120. method = plugins[n][name]
  121. if (method)
  122. method.apply(plugin, args);
  123. }
  124. }
  125. }
  126. //-------------------------------------------------------------------------------------------------
  127. /***/ }),
  128. /* 2 */
  129. /***/ (function(module, exports, __webpack_require__) {
  130. "use strict";
  131. //-------------------------------------------------------------------------------------------------
  132. function camelize(label) {
  133. if (label.length === 0)
  134. return label;
  135. var n, result, word, words = label.split(/[_-]/);
  136. // single word with first character already lowercase, return untouched
  137. if ((words.length === 1) && (words[0][0].toLowerCase() === words[0][0]))
  138. return label;
  139. result = words[0].toLowerCase();
  140. for(n = 1 ; n < words.length ; n++) {
  141. result = result + words[n].charAt(0).toUpperCase() + words[n].substring(1).toLowerCase();
  142. }
  143. return result;
  144. }
  145. //-------------------------------------------------------------------------------------------------
  146. camelize.prepended = function(prepend, label) {
  147. label = camelize(label);
  148. return prepend + label[0].toUpperCase() + label.substring(1);
  149. }
  150. //-------------------------------------------------------------------------------------------------
  151. module.exports = camelize;
  152. /***/ }),
  153. /* 3 */
  154. /***/ (function(module, exports, __webpack_require__) {
  155. "use strict";
  156. //-------------------------------------------------------------------------------------------------
  157. var mixin = __webpack_require__(0),
  158. camelize = __webpack_require__(2);
  159. //-------------------------------------------------------------------------------------------------
  160. function Config(options, StateMachine) {
  161. options = options || {};
  162. this.options = options; // preserving original options can be useful (e.g visualize plugin)
  163. this.defaults = StateMachine.defaults;
  164. this.states = [];
  165. this.transitions = [];
  166. this.map = {};
  167. this.lifecycle = this.configureLifecycle();
  168. this.init = this.configureInitTransition(options.init);
  169. this.data = this.configureData(options.data);
  170. this.methods = this.configureMethods(options.methods);
  171. this.map[this.defaults.wildcard] = {};
  172. this.configureTransitions(options.transitions || []);
  173. this.plugins = this.configurePlugins(options.plugins, StateMachine.plugin);
  174. }
  175. //-------------------------------------------------------------------------------------------------
  176. mixin(Config.prototype, {
  177. addState: function(name) {
  178. if (!this.map[name]) {
  179. this.states.push(name);
  180. this.addStateLifecycleNames(name);
  181. this.map[name] = {};
  182. }
  183. },
  184. addStateLifecycleNames: function(name) {
  185. this.lifecycle.onEnter[name] = camelize.prepended('onEnter', name);
  186. this.lifecycle.onLeave[name] = camelize.prepended('onLeave', name);
  187. this.lifecycle.on[name] = camelize.prepended('on', name);
  188. },
  189. addTransition: function(name) {
  190. if (this.transitions.indexOf(name) < 0) {
  191. this.transitions.push(name);
  192. this.addTransitionLifecycleNames(name);
  193. }
  194. },
  195. addTransitionLifecycleNames: function(name) {
  196. this.lifecycle.onBefore[name] = camelize.prepended('onBefore', name);
  197. this.lifecycle.onAfter[name] = camelize.prepended('onAfter', name);
  198. this.lifecycle.on[name] = camelize.prepended('on', name);
  199. },
  200. mapTransition: function(transition) {
  201. var name = transition.name,
  202. from = transition.from,
  203. to = transition.to;
  204. this.addState(from);
  205. if (typeof to !== 'function')
  206. this.addState(to);
  207. this.addTransition(name);
  208. this.map[from][name] = transition;
  209. return transition;
  210. },
  211. configureLifecycle: function() {
  212. return {
  213. onBefore: { transition: 'onBeforeTransition' },
  214. onAfter: { transition: 'onAfterTransition' },
  215. onEnter: { state: 'onEnterState' },
  216. onLeave: { state: 'onLeaveState' },
  217. on: { transition: 'onTransition' }
  218. };
  219. },
  220. configureInitTransition: function(init) {
  221. if (typeof init === 'string') {
  222. return this.mapTransition(mixin({}, this.defaults.init, { to: init, active: true }));
  223. }
  224. else if (typeof init === 'object') {
  225. return this.mapTransition(mixin({}, this.defaults.init, init, { active: true }));
  226. }
  227. else {
  228. this.addState(this.defaults.init.from);
  229. return this.defaults.init;
  230. }
  231. },
  232. configureData: function(data) {
  233. if (typeof data === 'function')
  234. return data;
  235. else if (typeof data === 'object')
  236. return function() { return data; }
  237. else
  238. return function() { return {}; }
  239. },
  240. configureMethods: function(methods) {
  241. return methods || {};
  242. },
  243. configurePlugins: function(plugins, builtin) {
  244. plugins = plugins || [];
  245. var n, max, plugin;
  246. for(n = 0, max = plugins.length ; n < max ; n++) {
  247. plugin = plugins[n];
  248. if (typeof plugin === 'function')
  249. plugins[n] = plugin = plugin()
  250. if (plugin.configure)
  251. plugin.configure(this);
  252. }
  253. return plugins
  254. },
  255. configureTransitions: function(transitions) {
  256. var i, n, transition, from, to, wildcard = this.defaults.wildcard;
  257. for(n = 0 ; n < transitions.length ; n++) {
  258. transition = transitions[n];
  259. from = Array.isArray(transition.from) ? transition.from : [transition.from || wildcard]
  260. to = transition.to || wildcard;
  261. for(i = 0 ; i < from.length ; i++) {
  262. this.mapTransition({ name: transition.name, from: from[i], to: to });
  263. }
  264. }
  265. },
  266. transitionFor: function(state, transition) {
  267. var wildcard = this.defaults.wildcard;
  268. return this.map[state][transition] ||
  269. this.map[wildcard][transition];
  270. },
  271. transitionsFor: function(state) {
  272. var wildcard = this.defaults.wildcard;
  273. return Object.keys(this.map[state]).concat(Object.keys(this.map[wildcard]));
  274. },
  275. allStates: function() {
  276. return this.states;
  277. },
  278. allTransitions: function() {
  279. return this.transitions;
  280. }
  281. });
  282. //-------------------------------------------------------------------------------------------------
  283. module.exports = Config;
  284. //-------------------------------------------------------------------------------------------------
  285. /***/ }),
  286. /* 4 */
  287. /***/ (function(module, exports, __webpack_require__) {
  288. var mixin = __webpack_require__(0),
  289. Exception = __webpack_require__(6),
  290. plugin = __webpack_require__(1),
  291. UNOBSERVED = [ null, [] ];
  292. //-------------------------------------------------------------------------------------------------
  293. function JSM(context, config) {
  294. this.context = context;
  295. this.config = config;
  296. this.state = config.init.from;
  297. this.observers = [context];
  298. }
  299. //-------------------------------------------------------------------------------------------------
  300. mixin(JSM.prototype, {
  301. init: function(args) {
  302. mixin(this.context, this.config.data.apply(this.context, args));
  303. plugin.hook(this, 'init');
  304. if (this.config.init.active)
  305. return this.fire(this.config.init.name, []);
  306. },
  307. is: function(state) {
  308. return Array.isArray(state) ? (state.indexOf(this.state) >= 0) : (this.state === state);
  309. },
  310. isPending: function() {
  311. return this.pending;
  312. },
  313. can: function(transition) {
  314. return !this.isPending() && !!this.seek(transition);
  315. },
  316. cannot: function(transition) {
  317. return !this.can(transition);
  318. },
  319. allStates: function() {
  320. return this.config.allStates();
  321. },
  322. allTransitions: function() {
  323. return this.config.allTransitions();
  324. },
  325. transitions: function() {
  326. return this.config.transitionsFor(this.state);
  327. },
  328. seek: function(transition, args) {
  329. var wildcard = this.config.defaults.wildcard,
  330. entry = this.config.transitionFor(this.state, transition),
  331. to = entry && entry.to;
  332. if (typeof to === 'function')
  333. return to.apply(this.context, args);
  334. else if (to === wildcard)
  335. return this.state
  336. else
  337. return to
  338. },
  339. fire: function(transition, args) {
  340. return this.transit(transition, this.state, this.seek(transition, args), args);
  341. },
  342. transit: function(transition, from, to, args) {
  343. var lifecycle = this.config.lifecycle,
  344. changed = this.config.options.observeUnchangedState || (from !== to);
  345. if (!to)
  346. return this.context.onInvalidTransition(transition, from, to);
  347. if (this.isPending())
  348. return this.context.onPendingTransition(transition, from, to);
  349. this.config.addState(to); // might need to add this state if it's unknown (e.g. conditional transition or goto)
  350. this.beginTransit();
  351. args.unshift({ // this context will be passed to each lifecycle event observer
  352. transition: transition,
  353. from: from,
  354. to: to,
  355. fsm: this.context
  356. });
  357. return this.observeEvents([
  358. this.observersForEvent(lifecycle.onBefore.transition),
  359. this.observersForEvent(lifecycle.onBefore[transition]),
  360. changed ? this.observersForEvent(lifecycle.onLeave.state) : UNOBSERVED,
  361. changed ? this.observersForEvent(lifecycle.onLeave[from]) : UNOBSERVED,
  362. this.observersForEvent(lifecycle.on.transition),
  363. changed ? [ 'doTransit', [ this ] ] : UNOBSERVED,
  364. changed ? this.observersForEvent(lifecycle.onEnter.state) : UNOBSERVED,
  365. changed ? this.observersForEvent(lifecycle.onEnter[to]) : UNOBSERVED,
  366. changed ? this.observersForEvent(lifecycle.on[to]) : UNOBSERVED,
  367. this.observersForEvent(lifecycle.onAfter.transition),
  368. this.observersForEvent(lifecycle.onAfter[transition]),
  369. this.observersForEvent(lifecycle.on[transition])
  370. ], args);
  371. },
  372. beginTransit: function() { this.pending = true; },
  373. endTransit: function(result) { this.pending = false; return result; },
  374. failTransit: function(result) { this.pending = false; throw result; },
  375. doTransit: function(lifecycle) { this.state = lifecycle.to; },
  376. observe: function(args) {
  377. if (args.length === 2) {
  378. var observer = {};
  379. observer[args[0]] = args[1];
  380. this.observers.push(observer);
  381. }
  382. else {
  383. this.observers.push(args[0]);
  384. }
  385. },
  386. observersForEvent: function(event) { // TODO: this could be cached
  387. var n = 0, max = this.observers.length, observer, result = [];
  388. for( ; n < max ; n++) {
  389. observer = this.observers[n];
  390. if (observer[event])
  391. result.push(observer);
  392. }
  393. return [ event, result, true ]
  394. },
  395. observeEvents: function(events, args, previousEvent, previousResult) {
  396. if (events.length === 0) {
  397. return this.endTransit(previousResult === undefined ? true : previousResult);
  398. }
  399. var event = events[0][0],
  400. observers = events[0][1],
  401. pluggable = events[0][2];
  402. args[0].event = event;
  403. if (event && pluggable && event !== previousEvent)
  404. plugin.hook(this, 'lifecycle', args);
  405. if (observers.length === 0) {
  406. events.shift();
  407. return this.observeEvents(events, args, event, previousResult);
  408. }
  409. else {
  410. var observer = observers.shift(),
  411. result = observer[event].apply(observer, args);
  412. if (result && typeof result.then === 'function') {
  413. return result.then(this.observeEvents.bind(this, events, args, event))
  414. .catch(this.failTransit.bind(this))
  415. }
  416. else if (result === false) {
  417. return this.endTransit(false);
  418. }
  419. else {
  420. return this.observeEvents(events, args, event, result);
  421. }
  422. }
  423. },
  424. onInvalidTransition: function(transition, from, to) {
  425. throw new Exception("transition is invalid in current state", transition, from, to, this.state);
  426. },
  427. onPendingTransition: function(transition, from, to) {
  428. throw new Exception("transition is invalid while previous transition is still in progress", transition, from, to, this.state);
  429. }
  430. });
  431. //-------------------------------------------------------------------------------------------------
  432. module.exports = JSM;
  433. //-------------------------------------------------------------------------------------------------
  434. /***/ }),
  435. /* 5 */
  436. /***/ (function(module, exports, __webpack_require__) {
  437. "use strict";
  438. //-----------------------------------------------------------------------------------------------
  439. var mixin = __webpack_require__(0),
  440. camelize = __webpack_require__(2),
  441. plugin = __webpack_require__(1),
  442. Config = __webpack_require__(3),
  443. JSM = __webpack_require__(4);
  444. //-----------------------------------------------------------------------------------------------
  445. var PublicMethods = {
  446. is: function(state) { return this._fsm.is(state) },
  447. can: function(transition) { return this._fsm.can(transition) },
  448. cannot: function(transition) { return this._fsm.cannot(transition) },
  449. observe: function() { return this._fsm.observe(arguments) },
  450. transitions: function() { return this._fsm.transitions() },
  451. allTransitions: function() { return this._fsm.allTransitions() },
  452. allStates: function() { return this._fsm.allStates() },
  453. onInvalidTransition: function(t, from, to) { return this._fsm.onInvalidTransition(t, from, to) },
  454. onPendingTransition: function(t, from, to) { return this._fsm.onPendingTransition(t, from, to) },
  455. }
  456. var PublicProperties = {
  457. state: {
  458. configurable: false,
  459. enumerable: true,
  460. get: function() {
  461. return this._fsm.state;
  462. },
  463. set: function(state) {
  464. throw Error('use transitions to change state')
  465. }
  466. }
  467. }
  468. //-----------------------------------------------------------------------------------------------
  469. function StateMachine(options) {
  470. return apply(this || {}, options);
  471. }
  472. function factory() {
  473. var cstor, options;
  474. if (typeof arguments[0] === 'function') {
  475. cstor = arguments[0];
  476. options = arguments[1] || {};
  477. }
  478. else {
  479. cstor = function() { this._fsm.apply(this, arguments) };
  480. options = arguments[0] || {};
  481. }
  482. var config = new Config(options, StateMachine);
  483. build(cstor.prototype, config);
  484. cstor.prototype._fsm.config = config; // convenience access to shared config without needing an instance
  485. return cstor;
  486. }
  487. //-------------------------------------------------------------------------------------------------
  488. function apply(instance, options) {
  489. var config = new Config(options, StateMachine);
  490. build(instance, config);
  491. instance._fsm();
  492. return instance;
  493. }
  494. function build(target, config) {
  495. if ((typeof target !== 'object') || Array.isArray(target))
  496. throw Error('StateMachine can only be applied to objects');
  497. plugin.build(target, config);
  498. Object.defineProperties(target, PublicProperties);
  499. mixin(target, PublicMethods);
  500. mixin(target, config.methods);
  501. config.allTransitions().forEach(function(transition) {
  502. target[camelize(transition)] = function() {
  503. return this._fsm.fire(transition, [].slice.call(arguments))
  504. }
  505. });
  506. target._fsm = function() {
  507. this._fsm = new JSM(this, config);
  508. this._fsm.init(arguments);
  509. }
  510. }
  511. //-----------------------------------------------------------------------------------------------
  512. StateMachine.version = '3.0.1';
  513. StateMachine.factory = factory;
  514. StateMachine.apply = apply;
  515. StateMachine.defaults = {
  516. wildcard: '*',
  517. init: {
  518. name: 'init',
  519. from: 'none'
  520. }
  521. }
  522. //===============================================================================================
  523. module.exports = StateMachine;
  524. /***/ }),
  525. /* 6 */
  526. /***/ (function(module, exports, __webpack_require__) {
  527. "use strict";
  528. module.exports = function(message, transition, from, to, current) {
  529. this.message = message;
  530. this.transition = transition;
  531. this.from = from;
  532. this.to = to;
  533. this.current = current;
  534. }
  535. /***/ })
  536. /******/ ]);
  537. });