├── .gitignore ├── Gruntfile.js ├── LICENSE-MIT ├── config.ru ├── dist ├── LASTBUILD ├── miso.storyboard.0.1.0.js ├── miso.storyboard.deps.0.1.0.js ├── miso.storyboard.deps.min.0.1.0.js ├── miso.storyboard.min.0.1.0.js ├── miso.storyboard.r.0.1.0.js └── node │ ├── miso.storyboard.0.0.1.js │ ├── miso.storyboard.deps.0.0.1.js │ └── miso.storyboard.deps.0.1.0.js ├── libs ├── lodash.js └── underscore.deferred.js ├── package.json ├── readme.md ├── src ├── events.js ├── node │ └── compat.js ├── require.js └── storyboard.js ├── tasks ├── node.js └── server.js └── test ├── index.build.html ├── index.html ├── support └── template.html ├── unit ├── async.js ├── base.js ├── context.js ├── events.js ├── nesting.js ├── scene_events.js └── sync.js └── vendor ├── jquery.js ├── jslitmus.js ├── qunit.css └── qunit.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /* global module */ 2 | 3 | module.exports = function(grunt) { 4 | 5 | var fullBanner = "/**\n" + 6 | "* <%= pkg.title %> - v<%= pkg.version %> - <%= grunt.template.today(\"m/d/yyyy\") %>\n" + 7 | "* <%= pkg.homepage %>\n" + 8 | "* Copyright (c) <%= grunt.template.today(\"yyyy\") %> <%= pkg.authors %>;\n" + 9 | "* Dual Licensed: <%= _.pluck(pkg.licenses, \"type\").join(\", \") %>\n" + 10 | "* https://github.com/misoproject/storyboard/blob/master/LICENSE-MIT \n" + 11 | "* https://github.com/misoproject/storyboard/blob/master/LICENSE-GPL \n" + 12 | "*/"; 13 | 14 | grunt.initConfig({ 15 | pkg : grunt.file.readJSON("package.json"), 16 | 17 | meta : { 18 | banner : fullBanner, 19 | lastbuild : "<%= grunt.template.today(\"yyyy/mm/dd hh:ss\") %>" 20 | }, 21 | 22 | node: { 23 | wrapper: "src/node/compat.js", 24 | misoStoryboard: "dist/miso.storyboard.<%= pkg.version %>.js" 25 | }, 26 | 27 | concat : { 28 | options : { 29 | banner : fullBanner 30 | }, 31 | 32 | fullnodeps: { 33 | dest: "dist/miso.storyboard.<%= pkg.version %>.js", 34 | src: [ 35 | "<%= meta.banner %>", 36 | "src/events.js", 37 | "src/storyboard.js" 38 | ] 39 | }, 40 | 41 | requirenodeps: { 42 | dest: "dist/miso.storyboard.r.<%= pkg.version %>.js", 43 | src: [ 44 | "<%= meta.banner %>", 45 | "dist/miso.storyboard.<%= pkg.version %>.js", 46 | "src/require.js" 47 | ] 48 | }, 49 | 50 | fulldeps: { 51 | dest : "dist/miso.storyboard.deps.<%= pkg.version %>.js", 52 | src : [ 53 | "<%= meta.banner %>", 54 | "libs/lodash.js", 55 | "libs/underscore.deferred.js", 56 | "dist/miso.storyboard.<%= pkg.version %>.js" 57 | ] 58 | }, 59 | 60 | buildstatus : { 61 | options : { 62 | banner : "<%= grunt.template.today(\"yyyy/mm/dd hh:ss\") %>" 63 | }, 64 | dest : "dist/LASTBUILD", 65 | src : [ 66 | "<%= \"lastbuild\" %>" 67 | ] 68 | } 69 | }, 70 | 71 | uglify : { 72 | options : { 73 | mangle : { 74 | except : [ "_", "$", "moment" ] 75 | }, 76 | squeeze : {}, 77 | codegen : {}, 78 | banner : fullBanner 79 | }, 80 | minnodeps : { 81 | dest : "dist/miso.storyboard.min.<%= pkg.version %>.js", 82 | src : [ 83 | "<%= meta.banner >", 84 | "dist/miso.storyboard.<%= pkg.version %>.js" 85 | ] 86 | }, 87 | mindeps : { 88 | dest : "dist/miso.storyboard.deps.min.<%= pkg.version %>.js", 89 | src : [ 90 | "<%= meta.banner %>", 91 | "dist/miso.storyboard.deps.<%= pkg.version %>.js" 92 | ] 93 | } 94 | }, 95 | 96 | qunit : { 97 | all : { 98 | options : { 99 | urls : [ 100 | "http://localhost:8001/test/index.html" 101 | ] 102 | } 103 | } 104 | }, 105 | 106 | connect: { 107 | server: { 108 | options: { 109 | port: 8000, 110 | base: ".", 111 | keepalive:true 112 | } 113 | }, 114 | qunit: { 115 | options: { 116 | port: 8001, 117 | base: "." 118 | } 119 | } 120 | }, 121 | 122 | watch : { 123 | files : ["src/**/*.js", "test/unit/*.js"], 124 | tasks : ["jshint","connect:qunit","qunit"] 125 | }, 126 | 127 | jshint : { 128 | options : { 129 | unused : true, 130 | devel : true, 131 | noempty : true, 132 | forin : false, 133 | evil : true, 134 | maxerr : 100, 135 | boss : true, 136 | curly : true, 137 | eqeqeq : true, 138 | immed : true, 139 | latedef : true, 140 | newcap : true, 141 | noarg : true, 142 | sub : true, 143 | undef : true, 144 | eqnull : true, 145 | browser : true, 146 | bitwise : true, 147 | loopfunc : true, 148 | predef : [ "_", "Miso", "require", "exports", "define" ] 149 | }, 150 | globals : { 151 | QUnit : true, 152 | module : true, 153 | test : true, 154 | asyncTest : true, 155 | expect : true, 156 | ok : true, 157 | equals : true, 158 | equal : true, 159 | JSLitmus : true, 160 | start : true, 161 | stop : true, 162 | $ : true, 163 | strictEqual : true, 164 | raises : true 165 | } 166 | }, 167 | 168 | files : [ 169 | "src/events.js", 170 | "src/require.js", 171 | "src/storyboard.js", 172 | "src/node/compat.js", 173 | "test/unit/**/*.js" 174 | ] 175 | 176 | }); 177 | 178 | grunt.registerTask("node", function() { 179 | 180 | var nodeConfig = grunt.config("node"); 181 | var read = grunt.file.read; 182 | 183 | var output = grunt.template.process(read(nodeConfig.wrapper), { 184 | data : { 185 | misoStoryboard : read(grunt.template.process(nodeConfig.misoStoryboard)) 186 | } 187 | }); 188 | 189 | // Write the contents out 190 | grunt.file.write("dist/node/miso.storyboard.deps." + 191 | grunt.template.process(grunt.config("pkg").version) + ".js", 192 | output); 193 | }); 194 | 195 | // load available tasks. 196 | grunt.loadNpmTasks("grunt-contrib-watch"); 197 | grunt.loadNpmTasks("grunt-contrib-uglify"); 198 | grunt.loadNpmTasks("grunt-contrib-jshint"); 199 | grunt.loadNpmTasks("grunt-contrib-copy"); 200 | grunt.loadNpmTasks("grunt-contrib-connect"); 201 | grunt.loadNpmTasks("grunt-contrib-concat"); 202 | grunt.loadNpmTasks("grunt-contrib-clean"); 203 | grunt.loadNpmTasks("grunt-contrib-qunit"); 204 | 205 | // Default task. 206 | grunt.registerTask("default", ["jshint", "connect:qunit", "qunit", "concat", "uglify", "node"]); 207 | }; 208 | 209 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Alex Graul, Irene Ros, Rich Harris 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rack' 3 | 4 | root=Dir.pwd 5 | puts ">>> Serving: #{root}" 6 | run Rack::Directory.new("#{root}") 7 | -------------------------------------------------------------------------------- /dist/LASTBUILD: -------------------------------------------------------------------------------- 1 | 2013/05/10 04:26 -------------------------------------------------------------------------------- /dist/miso.storyboard.0.1.0.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Miso.Storyboard - v0.1.0 - 5/10/2013 3 | * http://github.com/misoproject/storyboard 4 | * Copyright (c) 2013 Alex Graul, Irene Ros, Rich Harris; 5 | * Dual Licensed: MIT, GPL 6 | * https://github.com/misoproject/storyboard/blob/master/LICENSE-MIT 7 | * https://github.com/misoproject/storyboard/blob/master/LICENSE-GPL 8 | *//* global _ */ 9 | (function(global, _) { 10 | 11 | var Miso = global.Miso = (global.Miso || {}); 12 | 13 | /** 14 | * Miso Events is a small set of methods that can be mixed into any object 15 | * to make it evented. It allows one to then subscribe to specific object events, 16 | * to publish events, unsubscribe and subscribeOnce. 17 | */ 18 | Miso.Events = { 19 | 20 | 21 | /** 22 | * Triggers a specific event and passes any additional arguments 23 | * to the callbacks subscribed to that event. 24 | * Params: 25 | * name - the name of the event to trigger 26 | * .* - any additional arguments to pass to callbacks. 27 | */ 28 | publish : function(name) { 29 | var args = _.toArray(arguments); 30 | args.shift(); 31 | 32 | if (this._events && this._events[name]) { 33 | _.each(this._events[name], function(subscription) { 34 | subscription.callback.apply(subscription.context || this, args); 35 | }, this); 36 | } 37 | return this; 38 | }, 39 | 40 | /** 41 | * Allows subscribing on an evented object to a specific name. 42 | * Provide a callback to trigger. 43 | * Params: 44 | * name - event to subscribe to 45 | * callback - callback to trigger 46 | * options - optional arguments 47 | * priority - allows rearranging of existing callbacks based on priority 48 | * context - allows attaching diff context to callback 49 | * token - allows callback identification by token. 50 | */ 51 | subscribe : function(name, callback, options) { 52 | options = options || {}; 53 | this._events = this._events || {}; 54 | this._events[name] = this._events[name] || []; 55 | 56 | var subscription = { 57 | callback : callback, 58 | priority : options.priority || 0, 59 | token : options.token || _.uniqueId("t"), 60 | context : options.context || this 61 | }; 62 | var position; 63 | _.each(this._events[name], function(event, index) { 64 | if (!_.isUndefined(position)) { return; } 65 | if (event.priority <= subscription.priority) { 66 | position = index; 67 | } 68 | }); 69 | 70 | this._events[name].splice(position, 0, subscription); 71 | return subscription.token; 72 | }, 73 | 74 | /** 75 | * Allows subscribing to an event once. When the event is triggered 76 | * this subscription will be removed. 77 | * Params: 78 | * name - name of event 79 | * callback - The callback to trigger 80 | */ 81 | subscribeOnce : function(name, callback) { 82 | this._events = this._events || {}; 83 | var token = _.uniqueId("t"); 84 | return this.subscribe(name, function() { 85 | this.unsubscribe(name, { token : token }); 86 | callback.apply(this, arguments); 87 | }, this, token); 88 | }, 89 | 90 | /** 91 | * Allows unsubscribing from a specific event 92 | * Params: 93 | * name - event to unsubscribe from 94 | * identifier - callback to remove OR token. 95 | */ 96 | unsubscribe : function(name, identifier) { 97 | 98 | if (_.isUndefined(this._events[name])) { return this; } 99 | 100 | if (_.isFunction(identifier)) { 101 | this._events[name] = _.reject(this._events[name], function(b) { 102 | return b.callback === identifier; 103 | }); 104 | 105 | } else if ( _.isString(identifier)) { 106 | this._events[name] = _.reject(this._events[name], function(b) { 107 | return b.token === identifier; 108 | }); 109 | 110 | } else { 111 | this._events[name] = []; 112 | } 113 | return this; 114 | } 115 | 116 | }; 117 | 118 | }(this, _)); 119 | 120 | /* global _ */ 121 | 122 | (function(global, _) { 123 | 124 | var Miso = global.Miso = (global.Miso || {}); 125 | 126 | /** 127 | * Creates a new storyboard. 128 | * Params: 129 | * options - various arguments 130 | * context - optional. Set a different context for the storyboard. 131 | * by default it's the scene that is being executed. 132 | * 133 | */ 134 | var Storyboard = Miso.Storyboard = function(options) { 135 | 136 | options = options || {}; 137 | 138 | // save all options so we can clone this later... 139 | this._originalOptions = options; 140 | 141 | // Set up the context for this storyboard. This will be 142 | // available as "this" inside the transition functions. 143 | this._context = options.context || this; 144 | 145 | // Assign custom id to the storyboard. 146 | this._id = _.uniqueId("scene"); 147 | 148 | // If there are scenes defined, initialize them. 149 | if (options.scenes) { 150 | 151 | // if the scenes are actually just set to a function, change them 152 | // to an enter property 153 | _.each(options.scenes, function(scene, name) { 154 | if (typeof scene === "function") { 155 | options.scenes[name] = { 156 | enter : scene 157 | }; 158 | } 159 | }); 160 | 161 | // make sure enter/exit are defined as passthroughs if not present. 162 | _.each(Storyboard.HANDLERS, function(action) { 163 | options.scenes[action] = options.scenes[action] || function() { return true; }; 164 | }); 165 | 166 | // Convert the scenes to actually nested storyboards. A "scene" 167 | // is really just a storyboard of one action with no child scenes. 168 | this._buildScenes(options.scenes); 169 | 170 | // Save the initial scene that we will start from. When .start is called 171 | // on the storyboard, this is the scene we transition to. 172 | this._initial = options.initial; 173 | 174 | // Transition function given that there are child scenes. 175 | this.to = children_to; 176 | 177 | } else { 178 | 179 | // This is a terminal storyboad in that it doesn't actually have any child 180 | // scenes, just its own enter and exit functions. 181 | 182 | this.handlers = {}; 183 | 184 | _.each(Storyboard.HANDLERS, function(action) { 185 | 186 | // save the enter and exit functions and if they don't exist, define them. 187 | options[action] = options[action] || function() { return true; }; 188 | 189 | // wrap functions so they can declare themselves as optionally 190 | // asynchronous without having to worry about deferred management. 191 | this.handlers[action] = wrap(options[action], action); 192 | 193 | }, this); 194 | 195 | // Transition function given that this is a terminal storyboard. 196 | this.to = leaf_to; 197 | } 198 | 199 | 200 | // Iterate over all the properties defiend in the options and as long as they 201 | // are not on a black list, save them on the actual scene. This allows us to define 202 | // helper methods that are not going to be wrapped (and thus instrumented with 203 | // any deferred and async behavior.) 204 | _.each(options, function(prop, name) { 205 | 206 | if (_.indexOf(Storyboard.BLACKLIST, name) !== -1) { 207 | return; 208 | } 209 | 210 | if (_.isFunction(prop)) { 211 | this[name] = (function(contextOwner) { 212 | return function() { 213 | prop.apply(contextOwner._context || contextOwner, arguments); 214 | }; 215 | }(this)); 216 | } else { 217 | this[name] = prop; 218 | } 219 | 220 | }, this); 221 | 222 | }; 223 | 224 | Storyboard.HANDLERS = ["enter","exit"]; 225 | Storyboard.BLACKLIST = ["_id", "initial","scenes","enter","exit","context","_current"]; 226 | 227 | _.extend(Storyboard.prototype, Miso.Events, { 228 | 229 | /** 230 | * Allows for cloning of a storyboard 231 | * Returns: 232 | * s - a new Miso.Storyboard 233 | */ 234 | clone : function() { 235 | 236 | // clone nested storyboard 237 | if (this.scenes) { 238 | _.each(this._originalOptions.scenes, function(scene, name) { 239 | if (scene instanceof Miso.Storyboard) { 240 | this._originalOptions.scenes[name] = scene.clone(); 241 | } 242 | }, this); 243 | } 244 | 245 | return new Miso.Storyboard(this._originalOptions); 246 | }, 247 | 248 | /** 249 | * Attach a new scene to an existing storyboard. 250 | * Params: 251 | * name - The name of the scene 252 | * parent - The storyboard to attach this current scene to. 253 | */ 254 | attach : function(name, parent) { 255 | 256 | this.name = name; 257 | this.parent = parent; 258 | 259 | // if the parent has a custom context the child should inherit it 260 | if (parent._context && (parent._context._id !== parent._id)) { 261 | 262 | this._context = parent._context; 263 | if (this.scenes) { 264 | _.each(this.scenes , function(scene) { 265 | scene.attach(scene.name, this); 266 | }, this); 267 | } 268 | } 269 | return this; 270 | }, 271 | 272 | /** 273 | * Instruct a storyboard to kick off its initial scene. 274 | * This returns a deferred object just like all the .to calls. 275 | * If the initial scene is asynchronous, you will need to define a .then 276 | * callback to wait on the start scene to end its enter transition. 277 | */ 278 | start : function() { 279 | // if we've already started just return a happily resoved deferred 280 | if (typeof this._current !== "undefined") { 281 | return _.Deferred().resolve(); 282 | } else { 283 | return this.to(this._initial); 284 | } 285 | }, 286 | 287 | /** 288 | * Cancels a transition in action. This doesn't actually kill the function 289 | * that is currently in play! It does reject the deferred one was awaiting 290 | * from that transition. 291 | */ 292 | cancelTransition : function() { 293 | this._complete.reject(); 294 | this._transitioning = false; 295 | }, 296 | 297 | /** 298 | * Returns the current scene. 299 | * Returns: 300 | * scene - current scene name, or null. 301 | */ 302 | scene : function() { 303 | return this._current ? this._current.name : null; 304 | }, 305 | 306 | /** 307 | * Checks if the current scene is of a specific name. 308 | * Params: 309 | * scene - scene to check as to whether it is the current scene 310 | * Returns: 311 | * true if it is, false otherwise. 312 | */ 313 | is : function( scene ) { 314 | return (scene === this._current.name); 315 | }, 316 | 317 | /** 318 | * Returns true if storyboard is in the middle of a transition. 319 | */ 320 | inTransition : function() { 321 | return (this._transitioning === true); 322 | }, 323 | 324 | /** 325 | * Allows the changing of context. This will alter what "this" 326 | * will be set to inside the transition methods. 327 | */ 328 | setContext : function(context) { 329 | this._context = context; 330 | if (this.scenes) { 331 | _.each(this.scenes, function(scene) { 332 | scene.setContext(context); 333 | }); 334 | } 335 | }, 336 | 337 | _buildScenes : function( scenes ) { 338 | this.scenes = {}; 339 | _.each(scenes, function(scene, name) { 340 | this.scenes[name] = scene instanceof Miso.Storyboard ? scene : new Miso.Storyboard(scene); 341 | this.scenes[name].attach(name, this); 342 | }, this); 343 | } 344 | }); 345 | 346 | // Used as the to function to scenes which do not have children 347 | // These scenes only have their own enter and exit. 348 | function leaf_to( sceneName, argsArr, deferred ) { 349 | 350 | this._transitioning = true; 351 | var complete = this._complete = deferred || _.Deferred(), 352 | args = argsArr ? argsArr : [], 353 | handlerComplete = _.Deferred() 354 | .done(_.bind(function() { 355 | this._transitioning = false; 356 | this._current = sceneName; 357 | complete.resolve(); 358 | }, this)) 359 | .fail(_.bind(function() { 360 | this._transitioning = false; 361 | complete.reject(); 362 | }, this)); 363 | 364 | this.handlers[sceneName].call(this._context, args, handlerComplete); 365 | 366 | return complete.promise(); 367 | } 368 | 369 | // Used as the function to scenes that do have children. 370 | function children_to( sceneName, argsArr, deferred ) { 371 | var toScene = this.scenes[sceneName], 372 | fromScene = this._current, 373 | args = argsArr ? argsArr : [], 374 | complete = this._complete = deferred || _.Deferred(), 375 | exitComplete = _.Deferred(), 376 | enterComplete = _.Deferred(), 377 | publish = _.bind(function(name, isExit) { 378 | var sceneName = isExit ? fromScene : toScene; 379 | sceneName = sceneName ? sceneName.name : ""; 380 | 381 | this.publish(name, fromScene, toScene); 382 | if (name !== "start" || name !== "end") { 383 | this.publish(sceneName + ":" + name); 384 | } 385 | 386 | }, this), 387 | bailout = _.bind(function() { 388 | this._transitioning = false; 389 | this._current = fromScene; 390 | publish("fail"); 391 | complete.reject(); 392 | }, this), 393 | success = _.bind(function() { 394 | publish("enter"); 395 | this._transitioning = false; 396 | this._current = toScene; 397 | publish("end"); 398 | complete.resolve(); 399 | }, this); 400 | 401 | 402 | if (!toScene) { 403 | throw "Scene \"" + sceneName + "\" not found!"; 404 | } 405 | 406 | // we in the middle of a transition? 407 | if (this._transitioning) { 408 | return complete.reject(); 409 | } 410 | 411 | publish("start"); 412 | 413 | this._transitioning = true; 414 | 415 | if (fromScene) { 416 | 417 | // we are coming from a scene, so transition out of it. 418 | fromScene.to("exit", args, exitComplete); 419 | exitComplete.done(function() { 420 | publish("exit", true); 421 | }); 422 | 423 | } else { 424 | exitComplete.resolve(); 425 | } 426 | 427 | // when we're done exiting, enter the next set 428 | _.when(exitComplete).then(function() { 429 | 430 | toScene.to(toScene._initial || "enter", args, enterComplete); 431 | 432 | }).fail(bailout); 433 | 434 | enterComplete 435 | .then(success) 436 | .fail(bailout); 437 | 438 | return complete.promise(); 439 | } 440 | 441 | function wrap(func, name) { 442 | 443 | //don't wrap non-functions 444 | if ( !_.isFunction(func)) { return func; } 445 | //don't wrap private functions 446 | if ( /^_/.test(name) ) { return func; } 447 | //don't wrap wrapped functions 448 | if (func.__wrapped) { return func; } 449 | 450 | var wrappedFunc = function(args, deferred) { 451 | var async = false, 452 | result; 453 | 454 | deferred = deferred || _.Deferred(); 455 | 456 | this.async = function() { 457 | async = true; 458 | return function(pass) { 459 | return (pass !== false) ? deferred.resolve() : deferred.reject(); 460 | }; 461 | }; 462 | 463 | result = func.apply(this, args); 464 | this.async = undefined; 465 | if (!async) { 466 | return (result !== false) ? deferred.resolve() : deferred.reject(); 467 | } 468 | return deferred.promise(); 469 | }; 470 | 471 | wrappedFunc.__wrapped = true; 472 | return wrappedFunc; 473 | } 474 | 475 | }(this, _)); 476 | -------------------------------------------------------------------------------- /dist/miso.storyboard.deps.min.0.1.0.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Miso.Storyboard - v0.1.0 - 5/10/2013 3 | * http://github.com/misoproject/storyboard 4 | * Copyright (c) 2013 Alex Graul, Irene Ros, Rich Harris; 5 | * Dual Licensed: MIT, GPL 6 | * https://github.com/misoproject/storyboard/blob/master/LICENSE-MIT 7 | * https://github.com/misoproject/storyboard/blob/master/LICENSE-GPL 8 | */(function(t){function n(r){function u(t){return t&&"object"==typeof t&&!dr(t)&&Ye.call(t,"__wrapped__")?t:new W(t)}function D(t){var n=t.length,e=n>=s;if(e)for(var r={},i=-1;n>++i;){var u=c+t[i];(r[u]||(r[u]=[])).push(t[i])}return function(n){if(e){var i=c+n;return r[i]&&zn(r[i],n)>-1}return zn(t,n)>-1}}function T(t){return t.charCodeAt(0)}function B(t,n){var r=t.index,i=n.index;if(t=t.criteria,n=n.criteria,t!==n){if(t>n||t===e)return 1;if(n>t||n===e)return-1}return i>r?-1:1}function q(t,n,e,r){function i(){var r=arguments,s=o?this:n;if(u||(t=n[a]),e.length&&(r=r.length?(r=hr.call(r),c?r.concat(e):e.concat(r)):e),this instanceof i){U.prototype=t.prototype,s=new U,U.prototype=null;var f=t.apply(s,r);return an(f)?f:s}return t.apply(s,r)}var u=on(t),o=!e,a=n;if(o){var c=r;e=n}else if(!u){if(!r)throw new Ue;n=t}return i}function M(t){return"\\"+F[t]}function $(t){return mr[t]}function W(t){this.__wrapped__=t}function U(){}function H(t){var n=!1;if(!t||er.call(t)!=E)return n;var e=t.constructor;return(on(e)?e instanceof e:!0)?(wr(t,function(t,e){n=e}),n===!1||Ye.call(t,n)):n}function K(t,n,r){n||(n=0),r===e&&(r=t?t.length:0);for(var i=-1,u=r-n||0,o=Le(0>u?0:u);u>++i;)o[i]=t[n+i];return o}function z(t){return xr[t]}function P(t){return er.call(t)==w}function V(t,n,r,i,o,a){var c=t;if("function"==typeof n&&(i=r,r=n,n=!1),"function"==typeof r){if(r=i===e?r:u.createCallback(r,i,1),c=r(c),c!==e)return c;c=t}var s=an(c);if(s){var f=er.call(c);if(!L[f])return c;var l=dr(c)}if(!s||!n)return s?l?K(c):kr({},c):c;var h=gr[f];switch(f){case S:case O:return new h(+c);case A:case N:return new h(c);case I:return h(c.source,g.exec(c))}o||(o=[]),a||(a=[]);for(var p=o.length;p--;)if(o[p]==t)return a[p];return c=l?h(c.length):{},l&&(Ye.call(t,"index")&&(c.index=t.index),Ye.call(t,"input")&&(c.input=t.input)),o.push(t),a.push(c),(l?Cn:Cr)(t,function(t,i){c[i]=V(t,n,r,e,o,a)}),c}function G(t,n,e){return V(t,!0,n,e)}function J(t,n,r){var i;return n=u.createCallback(n,r),Cr(t,function(t,r,u){return n(t,r,u)?(i=r,!1):e}),i}function Q(t){var n=[];return wr(t,function(t,e){on(t)&&n.push(e)}),n.sort()}function X(t,n){return t?Ye.call(t,n):!1}function Y(t){for(var n=-1,e=br(t),r=e.length,i={};r>++n;){var u=e[n];i[t[u]]=u}return i}function Z(t){return t===!0||t===!1||er.call(t)==S}function tn(t){return t?"object"==typeof t&&er.call(t)==O:!1}function nn(t){return t?1===t.nodeType:!1}function en(t){var n=!0;if(!t)return n;var e=er.call(t),r=t.length;return e==C||e==N||e==w||e==E&&"number"==typeof r&&on(t.splice)?!r:(Cr(t,function(){return n=!1}),n)}function rn(t,n,r,i,o,c){var s=r===a;if("function"==typeof r&&!s){r=u.createCallback(r,i,2);var f=r(t,n);if(f!==e)return!!f}if(t===n)return 0!==t||1/t==1/n;var l=typeof t,h=typeof n;if(t===t&&(!t||"function"!=l&&"object"!=l)&&(!n||"function"!=h&&"object"!=h))return!1;if(null==t||null==n)return t===n;var p=er.call(t),v=er.call(n);if(p==w&&(p=E),v==w&&(v=E),p!=v)return!1;switch(p){case S:case O:return+t==+n;case A:return t!=+t?n!=+n:0==t?1/t==1/n:t==+n;case I:case N:return t==We(n)}var g=p==C;if(!g){if(Ye.call(t,"__wrapped__ ")||Ye.call(n,"__wrapped__"))return rn(t.__wrapped__||t,n.__wrapped__||n,r,i,o,c);if(p!=E)return!1;var _=t.constructor,d=n.constructor;if(_!=d&&!(on(_)&&_ instanceof _&&on(d)&&d instanceof d))return!1}o||(o=[]),c||(c=[]);for(var y=o.length;y--;)if(o[y]==t)return c[y]==n;var b=0;if(f=!0,o.push(t),c.push(n),g){if(y=t.length,b=n.length,f=b==t.length,!f&&!s)return f;for(;b--;){var m=y,x=n[b];if(s)for(;m--&&!(f=rn(t[m],x,r,i,o,c)););else if(!(f=rn(t[b],x,r,i,o,c)))break}return f}return wr(n,function(n,u,a){return Ye.call(a,u)?(b++,f=Ye.call(t,u)&&rn(t[u],n,r,i,o,c)):e}),f&&!s&&wr(t,function(t,n,r){return Ye.call(r,n)?f=--b>-1:e}),f}function un(t){return ur(t)&&!or(parseFloat(t))}function on(t){return"function"==typeof t}function an(t){return t?R[typeof t]:!1}function cn(t){return fn(t)&&t!=+t}function sn(t){return null===t}function fn(t){return"number"==typeof t||er.call(t)==A}function ln(t){return t?"object"==typeof t&&er.call(t)==I:!1}function hn(t){return"string"==typeof t||er.call(t)==N}function pn(t){return t===e}function vn(t,n,r){var i=arguments,o=0,c=2;if(!an(t))return t;if(r===a)var s=i[3],f=i[4],l=i[5];else f=[],l=[],"number"!=typeof r&&(c=i.length),c>3&&"function"==typeof i[c-2]?s=u.createCallback(i[--c-1],i[c--],2):c>2&&"function"==typeof i[c-1]&&(s=i[--c]);for(;c>++o;)(dr(i[o])?Cn:Cr)(i[o],function(n,r){var i,u,o=n,c=t[r];if(n&&((u=dr(n))||Sr(n))){for(var h=f.length;h--;)if(i=f[h]==n){c=l[h];break}if(!i){var p;s&&(o=s(c,n),(p=o!==e)&&(c=o)),p||(c=u?dr(c)?c:[]:Sr(c)?c:{}),f.push(n),l.push(c),p||(c=vn(c,n,a,s,f,l))}}else s&&(o=s(c,n),o===e&&(o=n)),o!==e&&(c=o);t[r]=c});return t}function gn(t,n,e){var r="function"==typeof n,i={};if(r)n=u.createCallback(n,e);else var o=Je.apply(He,hr.call(arguments,1));return wr(t,function(t,e,u){(r?!n(t,e,u):0>zn(o,e))&&(i[e]=t)}),i}function _n(t){for(var n=-1,e=br(t),r=e.length,i=Le(r);r>++n;){var u=e[n];i[n]=[u,t[u]]}return i}function dn(t,n,e){var r={};if("function"!=typeof n)for(var i=-1,o=Je.apply(He,hr.call(arguments,1)),a=an(t)?o.length:0;a>++i;){var c=o[i];c in t&&(r[c]=t[c])}else n=u.createCallback(n,e),wr(t,function(t,e,i){n(t,e,i)&&(r[e]=t)});return r}function yn(t){for(var n=-1,e=br(t),r=e.length,i=Le(r);r>++n;)i[n]=t[e[n]];return i}function bn(t){for(var n=-1,e=Je.apply(He,hr.call(arguments,1)),r=e.length,i=Le(r);r>++n;)i[n]=t[e[n]];return i}function mn(t,n,r){var i=-1,u=t?t.length:0,o=!1;return r=(0>r?cr(0,u+r):r)||0,"number"==typeof u?o=(hn(t)?t.indexOf(n,r):zn(t,n,r))>-1:Cr(t,function(t){return++i>=r?!(o=t===n):e}),o}function xn(t,n,e){var r={};return n=u.createCallback(n,e),Cn(t,function(t,e,i){e=We(n(t,e,i)),Ye.call(r,e)?r[e]++:r[e]=1}),r}function kn(t,n,e){var r=!0;n=u.createCallback(n,e);var i=-1,o=t?t.length:0;if("number"==typeof o)for(;o>++i&&(r=!!n(t[i],i,t)););else Cr(t,function(t,e,i){return r=!!n(t,e,i)});return r}function jn(t,n,e){var r=[];n=u.createCallback(n,e);var i=-1,o=t?t.length:0;if("number"==typeof o)for(;o>++i;){var a=t[i];n(a,i,t)&&r.push(a)}else Cr(t,function(t,e,i){n(t,e,i)&&r.push(t)});return r}function wn(t,n,r){n=u.createCallback(n,r);var i=-1,o=t?t.length:0;if("number"!=typeof o){var a;return Cr(t,function(t,r,i){return n(t,r,i)?(a=t,!1):e}),a}for(;o>++i;){var c=t[i];if(n(c,i,t))return c}}function Cn(t,n,r){var i=-1,o=t?t.length:0;if(n=n&&r===e?n:u.createCallback(n,r),"number"==typeof o)for(;o>++i&&n(t[i],i,t)!==!1;);else Cr(t,n);return t}function Sn(t,n,e){var r={};return n=u.createCallback(n,e),Cn(t,function(t,e,i){e=We(n(t,e,i)),(Ye.call(r,e)?r[e]:r[e]=[]).push(t)}),r}function On(t,n){var e=hr.call(arguments,2),r=-1,i="function"==typeof n,u=t?t.length:0,o=Le("number"==typeof u?u:0);return Cn(t,function(t){o[++r]=(i?n:t[n]).apply(t,e)}),o}function Dn(t,n,e){var r=-1,i=t?t.length:0;if(n=u.createCallback(n,e),"number"==typeof i)for(var o=Le(i);i>++r;)o[r]=n(t[r],r,t);else o=[],Cr(t,function(t,e,i){o[++r]=n(t,e,i)});return o}function An(t,n,e){var r=-1/0,i=r;if(!n&&dr(t))for(var o=-1,a=t.length;a>++o;){var c=t[o];c>i&&(i=c)}else n=!n&&hn(t)?T:u.createCallback(n,e),Cn(t,function(t,e,u){var o=n(t,e,u);o>r&&(r=o,i=t)});return i}function En(t,n,e){var r=1/0,i=r;if(!n&&dr(t))for(var o=-1,a=t.length;a>++o;){var c=t[o];i>c&&(i=c)}else n=!n&&hn(t)?T:u.createCallback(n,e),Cn(t,function(t,e,u){var o=n(t,e,u);r>o&&(r=o,i=t)});return i}function In(t,n){var e=-1,r=t?t.length:0;if("number"==typeof r)for(var i=Le(r);r>++e;)i[e]=t[e][n];return i||Dn(t,n)}function Nn(t,n,e,r){if(!t)return e;var i=3>arguments.length;n=u.createCallback(n,r,4);var o=-1,a=t.length;if("number"==typeof a)for(i&&(e=t[++o]);a>++o;)e=n(e,t[o],o,t);else Cr(t,function(t,r,u){e=i?(i=!1,t):n(e,t,r,u)});return e}function Ln(t,n,e,r){var i=t,o=t?t.length:0,a=3>arguments.length;if("number"!=typeof o){var c=br(t);o=c.length}return n=u.createCallback(n,r,4),Cn(t,function(t,r,u){r=c?c[--o]:--o,e=a?(a=!1,i[r]):n(e,i[r],r,u)}),e}function Rn(t,n,e){return n=u.createCallback(n,e),jn(t,function(t,e,r){return!n(t,e,r)})}function Fn(t){var n=-1,e=t?t.length:0,r=Le("number"==typeof e?e:0);return Cn(t,function(t){var e=Qe(lr()*(++n+1));r[n]=r[e],r[e]=t}),r}function Tn(t){var n=t?t.length:0;return"number"==typeof n?n:br(t).length}function Bn(t,n,e){var r;n=u.createCallback(n,e);var i=-1,o=t?t.length:0;if("number"==typeof o)for(;o>++i&&!(r=n(t[i],i,t)););else Cr(t,function(t,e,i){return!(r=n(t,e,i))});return!!r}function qn(t,n,e){var r=-1,i=t?t.length:0,o=Le("number"==typeof i?i:0);for(n=u.createCallback(n,e),Cn(t,function(t,e,i){o[++r]={criteria:n(t,e,i),index:r,value:t}}),i=o.length,o.sort(B);i--;)o[i]=o[i].value;return o}function Mn(t){return t&&"number"==typeof t.length?K(t):yn(t)}function $n(t){for(var n=-1,e=t?t.length:0,r=[];e>++n;){var i=t[n];i&&r.push(i)}return r}function Wn(t){for(var n=-1,e=t?t.length:0,r=Je.apply(He,hr.call(arguments,1)),i=D(r),u=[];e>++n;){var o=t[n];i(o)||u.push(o)}return u}function Un(t,n,e){var r=-1,i=t?t.length:0;for(n=u.createCallback(n,e);i>++r;)if(n(t[r],r,t))return r;return-1}function Hn(t,n,e){if(t){var r=0,i=t.length;if("number"!=typeof n&&null!=n){var o=-1;for(n=u.createCallback(n,e);i>++o&&n(t[o],o,t);)r++}else if(r=n,null==r||e)return t[0];return K(t,0,sr(cr(0,r),i))}}function Kn(t,n,e,r){var i=-1,o=t?t.length:0,a=[];for("boolean"!=typeof n&&null!=n&&(r=e,e=n,n=!1),null!=e&&(e=u.createCallback(e,r));o>++i;){var c=t[i];e&&(c=e(c,i,t)),dr(c)?Ze.apply(a,n?c:Kn(c)):a.push(c)}return a}function zn(t,n,e){var r=-1,i=t?t.length:0;if("number"==typeof e)r=(0>e?cr(0,i+e):e||0)-1;else if(e)return r=Yn(t,n),t[r]===n?r:-1;for(;i>++r;)if(t[r]===n)return r;return-1}function Pn(t,n,e){if(!t)return[];var r=0,i=t.length;if("number"!=typeof n&&null!=n){var o=i;for(n=u.createCallback(n,e);o--&&n(t[o],o,t);)r++}else r=null==n||e?1:n||r;return K(t,0,sr(cr(0,i-r),i))}function Vn(t){var n=arguments,e=n.length,r={0:{}},i=-1,u=t?t.length:0,o=u>=s,a=[],f=a;t:for(;u>++i;){var l=t[i];if(o)var h=c+l,p=r[0][h]?!(f=r[0][h]):f=r[0][h]=[];if(p||0>zn(f,l)){o&&f.push(l);for(var v=e;--v;)if(!(r[v]||(r[v]=D(n[v])))(l))continue t;a.push(l)}}return a}function Gn(t,n,e){if(t){var r=0,i=t.length;if("number"!=typeof n&&null!=n){var o=i;for(n=u.createCallback(n,e);o--&&n(t[o],o,t);)r++}else if(r=n,null==r||e)return t[i-1];return K(t,cr(0,i-r))}}function Jn(t,n,e){var r=t?t.length:0;for("number"==typeof e&&(r=(0>e?cr(0,r+e):sr(e,r-1))+1);r--;)if(t[r]===n)return r;return-1}function Qn(t,n,e){t=+t||0,e=+e||1,null==n&&(n=t,t=0);for(var r=-1,i=cr(0,Ve((n-t)/e)),u=Le(i);i>++r;)u[r]=t,t+=e;return u}function Xn(t,n,e){if("number"!=typeof n&&null!=n){var r=0,i=-1,o=t?t.length:0;for(n=u.createCallback(n,e);o>++i&&n(t[i],i,t);)r++}else r=null==n||e?1:cr(0,n);return K(t,r)}function Yn(t,n,e,r){var i=0,o=t?t.length:i;for(e=e?u.createCallback(e,r,1):xe,n=e(n);o>i;){var a=i+o>>>1;n>e(t[a])?i=a+1:o=a}return i}function Zn(t){return dr(t)||(arguments[0]=t?hr.call(t):He),te(Je.apply(He,arguments))}function te(t,n,e,r){var i=-1,o=t?t.length:0,a=[],f=a;"boolean"!=typeof n&&null!=n&&(r=e,e=n,n=!1);var l=!n&&o>=s;if(l)var h={};for(null!=e&&(f=[],e=u.createCallback(e,r));o>++i;){var p=t[i],v=e?e(p,i,t):p;if(l)var g=c+v,_=h[g]?!(f=h[g]):f=h[g]=[];(n?!i||f[f.length-1]!==v:_||0>zn(f,v))&&((e||l)&&f.push(v),a.push(p))}return a}function ne(t){for(var n=-1,e=t?t.length:0,r=e?An(In(t,"length")):0,i=Le(r);e>++n;)for(var u=-1,o=t[n];r>++u;)(i[u]||(i[u]=Le(e)))[n]=o[u];return i}function ee(t){return Wn(t,hr.call(arguments,1))}function re(t){for(var n=-1,e=t?An(In(arguments,"length")):0,r=Le(e);e>++n;)r[n]=In(arguments,n);return r}function ie(t,n){for(var e=-1,r=t?t.length:0,i={};r>++e;){var u=t[e];n?i[u]=n[e]:i[u[0]]=u[1]}return i}function ue(t,n){return 1>t?n():function(){return 1>--t?n.apply(this,arguments):e}}function oe(t,n){return _r.fastBind||rr&&arguments.length>2?rr.call.apply(rr,arguments):q(t,n,hr.call(arguments,2))}function ae(t){for(var n=arguments.length>1?Je.apply(He,hr.call(arguments,1)):Q(t),e=-1,r=n.length;r>++e;){var i=n[e];t[i]=oe(t[i],t)}return t}function ce(t,n){return q(t,n,hr.call(arguments,2),a)}function se(){var t=arguments;return function(){for(var n=arguments,e=t.length;e--;)n=[t[e].apply(this,n)];return n[0]}}function fe(t,n,r){if(null==t)return xe;var i=typeof t;if("function"!=i){if("object"!=i)return function(n){return n[t]};var u=br(t);return function(n){for(var e=u.length,r=!1;e--&&(r=rn(n[u[e]],t[u[e]],a)););return r}}return n!==e?1===r?function(e){return t.call(n,e)}:2===r?function(e,r){return t.call(n,e,r)}:4===r?function(e,r,i,u){return t.call(n,e,r,i,u)}:function(e,r,i){return t.call(n,e,r,i)}:t}function le(t,n,e){function r(){u=c=null,s&&(o=t.apply(a,i))}var i,u,o,a,c,s=!0;if(e===!0){var f=!0;s=!1}else e&&R[typeof e]&&(f=e.leading,s="trailing"in e?e.trailing:s);return function(){return i=arguments,a=this,Ge(c),!u&&f?(u=!0,o=t.apply(a,i)):c=nr(r,n),o}}function he(t){var n=hr.call(arguments,1);return nr(function(){t.apply(e,n)},1)}function pe(t,n){var r=hr.call(arguments,2);return nr(function(){t.apply(e,r)},n)}function ve(t,n){var e={};return function(){var r=c+(n?n.apply(this,arguments):arguments[0]);return Ye.call(e,r)?e[r]:e[r]=t.apply(this,arguments)}}function ge(t){var n,e;return function(){return n?e:(n=!0,e=t.apply(this,arguments),t=null,e)}}function _e(t){return q(t,hr.call(arguments,1))}function de(t){return q(t,hr.call(arguments,1),null,a)}function ye(t,n,e){function r(){a=null,f&&(c=new Fe,u=t.apply(o,i))}var i,u,o,a,c=0,s=!0,f=!0;return e===!1?s=!1:e&&R[typeof e]&&(s="leading"in e?e.leading:s,f="trailing"in e?e.trailing:f),function(){var e=new Fe;a||s||(c=e);var f=n-(e-c);return i=arguments,o=this,0>=f?(Ge(a),a=null,c=e,u=t.apply(o,i)):a||(a=nr(r,f)),u}}function be(t,n){return function(){var e=[t];return Ze.apply(e,arguments),n.apply(this,e)}}function me(t){return null==t?"":We(t).replace(m,$)}function xe(t){return t}function ke(t){Cn(Q(t),function(n){var e=u[n]=t[n];u.prototype[n]=function(){var t=this.__wrapped__,n=[t];Ze.apply(n,arguments);var r=e.apply(u,n);return t&&"object"==typeof t&&t==r?this:new W(r)}})}function je(){return r._=ze,this}function we(t,n){return null==t&&null==n&&(n=1),t=+t||0,null==n&&(n=t,t=0),t+Qe(lr()*((+n||0)-t+1))}function Ce(t,n){var r=t?t[n]:e;return on(r)?t[n]():r}function Se(t,n,r){var i=u.templateSettings;t||(t=""),r=jr({},r,i);var o,a=jr({},r.imports,i.imports),c=br(a),s=yn(a),p=0,g=r.interpolate||b,d="__p += '",y=$e((r.escape||b).source+"|"+g.source+"|"+(g===_?v:b).source+"|"+(r.evaluate||b).source+"|$","g");t.replace(y,function(n,e,r,i,u,a){return r||(r=i),d+=t.slice(p,a).replace(x,M),e&&(d+="' +\n__e("+e+") +\n'"),u&&(o=!0,d+="';\n"+u+";\n__p += '"),r&&(d+="' +\n((__t = ("+r+")) == null ? '' : __t) +\n'"),p=a+n.length,n}),d+="';\n";var m=r.variable,k=m;k||(m="obj",d="with ("+m+") {\n"+d+"\n}\n"),d=(o?d.replace(f,""):d).replace(l,"$1").replace(h,"$1;"),d="function("+m+") {\n"+(k?"":m+" || ("+m+" = {});\n")+"var __t, __p = '', __e = _.escape"+(o?", __j = Array.prototype.join;\nfunction print() { __p += __j.call(arguments, '') }\n":";\n")+d+"return __p\n}";var w="\n/*\n//@ sourceURL="+(r.sourceURL||"/lodash/template/source["+j++ +"]")+"\n*/";try{var C=Te(c,"return "+d+w).apply(e,s)}catch(S){throw S.source=d,S}return n?C(n):(C.source=d,C)}function Oe(t,n,e){t=(t=+t)>-1?t:0;var r=-1,i=Le(t);for(n=u.createCallback(n,e,1);t>++r;)i[r]=n(r);return i}function De(t){return null==t?"":We(t).replace(p,z)}function Ae(t){var n=++o;return We(null==t?"":t)+n}function Ee(t,n){return n(t),t}function Ie(){return We(this.__wrapped__)}function Ne(){return this.__wrapped__}r=r?_.defaults(t.Object(),r,_.pick(t,k)):t;var Le=r.Array,Re=r.Boolean,Fe=r.Date,Te=r.Function,Be=r.Math,qe=r.Number,Me=r.Object,$e=r.RegExp,We=r.String,Ue=r.TypeError,He=Le(),Ke=Me(),ze=r._,Pe=$e("^"+We(Ke.valueOf).replace(/[.*+?^${}()|[\]\\]/g,"\\$&").replace(/valueOf|for [^\]]+/g,".+?")+"$"),Ve=Be.ceil,Ge=r.clearTimeout,Je=He.concat,Qe=Be.floor,Xe=Pe.test(Xe=Me.getPrototypeOf)&&Xe,Ye=Ke.hasOwnProperty,Ze=He.push,tr=r.setImmediate,nr=r.setTimeout,er=Ke.toString,rr=Pe.test(rr=er.bind)&&rr,ir=Pe.test(ir=Le.isArray)&&ir,ur=r.isFinite,or=r.isNaN,ar=Pe.test(ar=Me.keys)&&ar,cr=Be.max,sr=Be.min,fr=r.parseInt,lr=Be.random,hr=He.slice,pr=Pe.test(r.attachEvent),vr=rr&&!/\n|true/.test(rr+pr),gr={};gr[C]=Le,gr[S]=Re,gr[O]=Fe,gr[E]=Me,gr[A]=qe,gr[I]=$e,gr[N]=We;var _r=u.support={};_r.fastBind=rr&&!vr,u.templateSettings={escape:/<%-([\s\S]+?)%>/g,evaluate:/<%([\s\S]+?)%>/g,interpolate:_,variable:"",imports:{_:u}},W.prototype=u.prototype;var dr=ir,yr=function(t){var n,e=t,r=[];if(!e)return r;if(!R[typeof t])return r;for(n in e)Ye.call(e,n)&&r.push(n);return r},br=ar?function(t){return an(t)?ar(t):[]}:yr,mr={"&":"&","<":"<",">":">",'"':""","'":"'"},xr=Y(mr),kr=function(t,n,e){var r,i=t,o=i;if(!i)return o;var a=arguments,c=0,s="number"==typeof e?2:a.length;if(s>3&&"function"==typeof a[s-2])var f=u.createCallback(a[--s-1],a[s--],2);else s>2&&"function"==typeof a[s-1]&&(f=a[--s]);for(;s>++c;)if(i=a[c],i&&R[typeof i]){var l=i.length;if(r=-1,dr(i))for(;l>++r;)o[r]=f?f(o[r],i[r]):i[r];else for(var h=-1,p=R[typeof i]?br(i):[],l=p.length;l>++h;)r=p[h],o[r]=f?f(o[r],i[r]):i[r]}return o},jr=function(t,n,r){var i,u=t,o=u;if(!u)return o;for(var a=arguments,c=0,s="number"==typeof r?2:a.length;s>++c;)if(u=a[c],u&&R[typeof u]){var f=u.length;if(i=-1,dr(u))for(;f>++i;)o[i]===e&&(o[i]=u[i]);else for(var l=-1,h=R[typeof u]?br(u):[],f=h.length;f>++l;)i=h[l],o[i]===e&&(o[i]=u[i])}return o},wr=function(t,n,r){var i,o=t,a=o;if(!o)return a;if(!R[typeof o])return a;n=n&&r===e?n:u.createCallback(n,r);for(i in o)if(n(o[i],i,t)===!1)return a;return a},Cr=function(t,n,r){var i,o=t,a=o;if(!o)return a;if(!R[typeof o])return a;n=n&&r===e?n:u.createCallback(n,r);for(var c=-1,s=R[typeof o]?br(o):[],f=s.length;f>++c;)if(i=s[c],n(o[i],i,t)===!1)return a;return a},Sr=function(t){if(!t||er.call(t)!=E)return!1;var n=t.valueOf,e="function"==typeof n&&(e=Xe(n))&&Xe(e);return e?t==e||Xe(t)==e:H(t)},Or=jn;vr&&i&&"function"==typeof tr&&(he=oe(tr,r));var Dr=8==fr(d+"08")?fr:function(t,n){return fr(hn(t)?t.replace(y,""):t,n||0)};return u.after=ue,u.assign=kr,u.at=bn,u.bind=oe,u.bindAll=ae,u.bindKey=ce,u.compact=$n,u.compose=se,u.countBy=xn,u.createCallback=fe,u.debounce=le,u.defaults=jr,u.defer=he,u.delay=pe,u.difference=Wn,u.filter=jn,u.flatten=Kn,u.forEach=Cn,u.forIn=wr,u.forOwn=Cr,u.functions=Q,u.groupBy=Sn,u.initial=Pn,u.intersection=Vn,u.invert=Y,u.invoke=On,u.keys=br,u.map=Dn,u.max=An,u.memoize=ve,u.merge=vn,u.min=En,u.omit=gn,u.once=ge,u.pairs=_n,u.partial=_e,u.partialRight=de,u.pick=dn,u.pluck=In,u.range=Qn,u.reject=Rn,u.rest=Xn,u.shuffle=Fn,u.sortBy=qn,u.tap=Ee,u.throttle=ye,u.times=Oe,u.toArray=Mn,u.union=Zn,u.uniq=te,u.unzip=ne,u.values=yn,u.where=Or,u.without=ee,u.wrap=be,u.zip=re,u.zipObject=ie,u.collect=Dn,u.drop=Xn,u.each=Cn,u.extend=kr,u.methods=Q,u.object=ie,u.select=jn,u.tail=Xn,u.unique=te,ke(u),u.clone=V,u.cloneDeep=G,u.contains=mn,u.escape=me,u.every=kn,u.find=wn,u.findIndex=Un,u.findKey=J,u.has=X,u.identity=xe,u.indexOf=zn,u.isArguments=P,u.isArray=dr,u.isBoolean=Z,u.isDate=tn,u.isElement=nn,u.isEmpty=en,u.isEqual=rn,u.isFinite=un,u.isFunction=on,u.isNaN=cn,u.isNull=sn,u.isNumber=fn,u.isObject=an,u.isPlainObject=Sr,u.isRegExp=ln,u.isString=hn,u.isUndefined=pn,u.lastIndexOf=Jn,u.mixin=ke,u.noConflict=je,u.parseInt=Dr,u.random=we,u.reduce=Nn,u.reduceRight=Ln,u.result=Ce,u.runInContext=n,u.size=Tn,u.some=Bn,u.sortedIndex=Yn,u.template=Se,u.unescape=De,u.uniqueId=Ae,u.all=kn,u.any=Bn,u.detect=wn,u.foldl=Nn,u.foldr=Ln,u.include=mn,u.inject=Nn,Cr(u,function(t,n){u.prototype[n]||(u.prototype[n]=function(){var n=[this.__wrapped__];return Ze.apply(n,arguments),t.apply(u,n)})}),u.first=Hn,u.last=Gn,u.take=Hn,u.head=Hn,Cr(u,function(t,n){u.prototype[n]||(u.prototype[n]=function(n,e){var r=t(this.__wrapped__,n,e);return null==n||e&&"function"!=typeof n?r:new W(r)})}),u.VERSION="1.2.1",u.prototype.toString=Ie,u.prototype.value=Ne,u.prototype.valueOf=Ne,Cn(["join","pop","shift"],function(t){var n=He[t];u.prototype[t]=function(){return n.apply(this.__wrapped__,arguments)}}),Cn(["push","reverse","sort","unshift"],function(t){var n=He[t];u.prototype[t]=function(){return n.apply(this.__wrapped__,arguments),this}}),Cn(["concat","slice","splice"],function(t){var n=He[t];u.prototype[t]=function(){return new W(n.apply(this.__wrapped__,arguments))}}),u}var e,r="object"==typeof exports&&exports,i="object"==typeof module&&module&&module.exports==r&&module,u="object"==typeof global&&global;(u.global===u||u.window===u)&&(t=u);var o=0,a={},c=+new Date+"",s=200,f=/\b__p \+= '';/g,l=/\b(__p \+=) '' \+/g,h=/(__e\(.*?\)|\b__t\)) \+\n'';/g,p=/&(?:amp|lt|gt|quot|#39);/g,v=/\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g,g=/\w*$/,_=/<%=([\s\S]+?)%>/g,d=" \f \n\r\u2028\u2029 ",y=RegExp("^["+d+"]*0+(?=.$)"),b=/($^)/,m=/[&<>"']/g,x=/['\n\r\t\u2028\u2029\\]/g,k=["Array","Boolean","Date","Function","Math","Number","Object","RegExp","String","_","attachEvent","clearTimeout","isFinite","isNaN","parseInt","setImmediate","setTimeout"],j=0,w="[object Arguments]",C="[object Array]",S="[object Boolean]",O="[object Date]",D="[object Function]",A="[object Number]",E="[object Object]",I="[object RegExp]",N="[object String]",L={};L[D]=!1,L[w]=L[C]=L[S]=L[O]=L[A]=L[E]=L[I]=L[N]=!0;var R={"boolean":!1,"function":!0,object:!0,number:!1,string:!1,undefined:!1},F={"\\":"\\","'":"'","\n":"n","\r":"r"," ":"t","\u2028":"u2028","\u2029":"u2029"},_=n();"function"==typeof define&&"object"==typeof define.amd&&define.amd?(t._=_,define(function(){return _})):r&&!r.nodeType?i?(i.exports=_)._=_:r._=_:t._=_})(this),function(t){function n(t){var n=d[t]={};return f(t.split(/\s+/),function(t){n[t]=!0}),n}var e={},r=Array.prototype,i=Object.prototype,u=i.hasOwnProperty,o=i.toString,a=r.forEach,c=r.indexOf,s=r.slice,f=function(t,n,r){var i,o,c;if(t)if(a&&t.forEach===a)t.forEach(n,r);else if(t.length===+t.length){for(o=0,c=t.length;c>o;o++)if(o in t&&n.call(r,t[o],o,t)===e)return}else for(i in t)if(u.call(t,i)&&n.call(r,t[i],i,t)===e)return},l=function(t){return!!(t&&t.constructor&&t.call&&t.apply)},h=function(t){return f(s.call(arguments,1),function(n){var e;for(e in n)void 0!==n[e]&&(t[e]=n[e])}),t},p=function(t,n,e){var r;if(n){if(c)return c.call(n,t,e);for(r=n.length,e=e?0>e?Math.max(0,r+e):e:0;r>e;e++)if(e in n&&n[e]===t)return e}return-1},v={};f("Boolean Number String Function Array Date RegExp Object".split(" "),function(t){v["[object "+t+"]"]=t.toLowerCase()});var g=function(t){return null==t?t+"":v[o.call(t)]||"object"},_={},d={};_.Callbacks=function(t){t="string"==typeof t?d[t]||n(t):h({},t);var e,r,i,u,o,a,c=[],s=!t.once&&[],l=function(n){for(e=t.memory&&n,r=!0,a=u||0,u=0,o=c.length,i=!0;c&&o>a;a++)if(c[a].apply(n[0],n[1])===!1&&t.stopOnFalse){e=!1;break}i=!1,c&&(s?s.length&&l(s.shift()):e?c=[]:v.disable())},v={add:function(){if(c){var n=c.length;(function r(n){f(n,function(n){var e=g(n);"function"!==e||t.unique&&v.has(n)?n&&n.length&&"string"!==e&&r(n):c.push(n)})})(arguments),i?o=c.length:e&&(u=n,l(e))}return this},remove:function(){return c&&f(arguments,function(t){for(var n;(n=p(t,c,n))>-1;)c.splice(n,1),i&&(o>=n&&o--,a>=n&&a--)}),this},has:function(t){return p(t,c)>-1},empty:function(){return c=[],this},disable:function(){return c=s=e=void 0,this},disabled:function(){return!c},lock:function(){return s=void 0,e||v.disable(),this},locked:function(){return!s},fireWith:function(t,n){return n=n||[],n=[t,n.slice?n.slice():n],!c||r&&!s||(i?s.push(n):l(n)),this},fire:function(){return v.fireWith(this,arguments),this},fired:function(){return!!r}};return v},_.Deferred=function(t){var n=[["resolve","done",_.Callbacks("once memory"),"resolved"],["reject","fail",_.Callbacks("once memory"),"rejected"],["notify","progress",_.Callbacks("memory")]],e="pending",r={state:function(){return e},always:function(){return i.done(arguments).fail(arguments),this},then:function(){var t=arguments;return _.Deferred(function(e){f(n,function(n,r){var u=n[0],o=t[r];i[n[1]](l(o)?function(){var t=o.apply(this,arguments);t&&l(t.promise)?t.promise().done(e.resolve).fail(e.reject).progress(e.notify):e[u+"With"](this===i?e:this,[t])}:e[u])}),t=null}).promise()},promise:function(t){return"object"==typeof t?h(t,r):r}},i={};return r.pipe=r.then,f(n,function(t,u){var o=t[2],a=t[3];r[t[1]]=o.add,a&&o.add(function(){e=a},n[1^u][2].disable,n[2][2].lock),i[t[0]]=o.fire,i[t[0]+"With"]=o.fireWith}),r.promise(i),t&&t.call(i,i),i},_.when=function(t){var n,e,r,i=0,u=s.call(arguments),o=u.length,a=1!==o||t&&l(t.promise)?o:0,c=1===a?t:_.Deferred(),f=function(t,e,r){return function(i){e[t]=this,r[t]=arguments.length>1?s.call(arguments):i,r===n?c.notifyWith(e,r):--a||c.resolveWith(e,r)}};if(o>1)for(n=Array(o),e=Array(o),r=Array(o);o>i;i++)u[i]&&l(u[i].promise)?u[i].promise().done(f(i,r,u)).fail(c.reject).progress(f(i,e,n)):--a;return a||c.resolveWith(r,u),c.promise()},"undefined"!=typeof module&&module.exports?module.exports=_:t._!==void 0?t._.mixin(_):t._=_}(this),function(t,_){var n=t.Miso=t.Miso||{};n.Events={publish:function(t){var n=_.toArray(arguments);return n.shift(),this._events&&this._events[t]&&_.each(this._events[t],function(t){t.callback.apply(t.context||this,n)},this),this},subscribe:function(t,n,e){e=e||{},this._events=this._events||{},this._events[t]=this._events[t]||[];var r,i={callback:n,priority:e.priority||0,token:e.token||_.uniqueId("t"),context:e.context||this};return _.each(this._events[t],function(t,n){_.isUndefined(r)&&t.priority<=i.priority&&(r=n)}),this._events[t].splice(r,0,i),i.token},subscribeOnce:function(t,n){this._events=this._events||{};var e=_.uniqueId("t");return this.subscribe(t,function(){this.unsubscribe(t,{token:e}),n.apply(this,arguments)},this,e)},unsubscribe:function(t,n){return _.isUndefined(this._events[t])?this:(this._events[t]=_.isFunction(n)?_.reject(this._events[t],function(t){return t.callback===n}):_.isString(n)?_.reject(this._events[t],function(t){return t.token===n}):[],this)}}}(this,_),function(t,_){function n(t,n,e){this._transitioning=!0;var r=this._complete=e||_.Deferred(),i=n?n:[],u=_.Deferred().done(_.bind(function(){this._transitioning=!1,this._current=t,r.resolve()},this)).fail(_.bind(function(){this._transitioning=!1,r.reject()},this));return this.handlers[t].call(this._context,i,u),r.promise()}function e(t,n,e){var r=this.scenes[t],i=this._current,u=n?n:[],o=this._complete=e||_.Deferred(),a=_.Deferred(),c=_.Deferred(),s=_.bind(function(t,n){var e=n?i:r;e=e?e.name:"",this.publish(t,i,r),("start"!==t||"end"!==t)&&this.publish(e+":"+t)},this),f=_.bind(function(){this._transitioning=!1,this._current=i,s("fail"),o.reject()},this),l=_.bind(function(){s("enter"),this._transitioning=!1,this._current=r,s("end"),o.resolve()},this);if(!r)throw'Scene "'+t+'" not found!';return this._transitioning?o.reject():(s("start"),this._transitioning=!0,i?(i.to("exit",u,a),a.done(function(){s("exit",!0)})):a.resolve(),_.when(a).then(function(){r.to(r._initial||"enter",u,c)}).fail(f),c.then(l).fail(f),o.promise())}function r(t,n){if(!_.isFunction(t))return t;if(/^_/.test(n))return t;if(t.__wrapped)return t;var e=function(n,e){var r,i=!1;return e=e||_.Deferred(),this.async=function(){return i=!0,function(t){return t!==!1?e.resolve():e.reject()}},r=t.apply(this,n),this.async=void 0,i?e.promise():r!==!1?e.resolve():e.reject()};return e.__wrapped=!0,e}var i=t.Miso=t.Miso||{},u=i.Storyboard=function(t){t=t||{},this._originalOptions=t,this._context=t.context||this,this._id=_.uniqueId("scene"),t.scenes?(_.each(t.scenes,function(n,e){"function"==typeof n&&(t.scenes[e]={enter:n})}),_.each(u.HANDLERS,function(n){t.scenes[n]=t.scenes[n]||function(){return!0}}),this._buildScenes(t.scenes),this._initial=t.initial,this.to=e):(this.handlers={},_.each(u.HANDLERS,function(n){t[n]=t[n]||function(){return!0},this.handlers[n]=r(t[n],n)},this),this.to=n),_.each(t,function(t,n){-1===_.indexOf(u.BLACKLIST,n)&&(this[n]=_.isFunction(t)?function(n){return function(){t.apply(n._context||n,arguments)}}(this):t)},this)};u.HANDLERS=["enter","exit"],u.BLACKLIST=["_id","initial","scenes","enter","exit","context","_current"],_.extend(u.prototype,i.Events,{clone:function(){return this.scenes&&_.each(this._originalOptions.scenes,function(t,n){t instanceof i.Storyboard&&(this._originalOptions.scenes[n]=t.clone())},this),new i.Storyboard(this._originalOptions)},attach:function(t,n){return this.name=t,this.parent=n,n._context&&n._context._id!==n._id&&(this._context=n._context,this.scenes&&_.each(this.scenes,function(t){t.attach(t.name,this)},this)),this},start:function(){return this._current!==void 0?_.Deferred().resolve():this.to(this._initial)},cancelTransition:function(){this._complete.reject(),this._transitioning=!1},scene:function(){return this._current?this._current.name:null},is:function(t){return t===this._current.name},inTransition:function(){return this._transitioning===!0},setContext:function(t){this._context=t,this.scenes&&_.each(this.scenes,function(n){n.setContext(t)})},_buildScenes:function(t){this.scenes={},_.each(t,function(t,n){this.scenes[n]=t instanceof i.Storyboard?t:new i.Storyboard(t),this.scenes[n].attach(n,this)},this)}})}(this,_); -------------------------------------------------------------------------------- /dist/miso.storyboard.min.0.1.0.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Miso.Storyboard - v0.1.0 - 5/10/2013 3 | * http://github.com/misoproject/storyboard 4 | * Copyright (c) 2013 Alex Graul, Irene Ros, Rich Harris; 5 | * Dual Licensed: MIT, GPL 6 | * https://github.com/misoproject/storyboard/blob/master/LICENSE-MIT 7 | * https://github.com/misoproject/storyboard/blob/master/LICENSE-GPL 8 | */(function(t,_){var n=t.Miso=t.Miso||{};n.Events={publish:function(t){var n=_.toArray(arguments);return n.shift(),this._events&&this._events[t]&&_.each(this._events[t],function(t){t.callback.apply(t.context||this,n)},this),this},subscribe:function(t,n,i){i=i||{},this._events=this._events||{},this._events[t]=this._events[t]||[];var e,s={callback:n,priority:i.priority||0,token:i.token||_.uniqueId("t"),context:i.context||this};return _.each(this._events[t],function(t,n){_.isUndefined(e)&&t.priority<=s.priority&&(e=n)}),this._events[t].splice(e,0,s),s.token},subscribeOnce:function(t,n){this._events=this._events||{};var i=_.uniqueId("t");return this.subscribe(t,function(){this.unsubscribe(t,{token:i}),n.apply(this,arguments)},this,i)},unsubscribe:function(t,n){return _.isUndefined(this._events[t])?this:(this._events[t]=_.isFunction(n)?_.reject(this._events[t],function(t){return t.callback===n}):_.isString(n)?_.reject(this._events[t],function(t){return t.token===n}):[],this)}}})(this,_),function(t,_){function n(t,n,i){this._transitioning=!0;var e=this._complete=i||_.Deferred(),s=n?n:[],r=_.Deferred().done(_.bind(function(){this._transitioning=!1,this._current=t,e.resolve()},this)).fail(_.bind(function(){this._transitioning=!1,e.reject()},this));return this.handlers[t].call(this._context,s,r),e.promise()}function i(t,n,i){var e=this.scenes[t],s=this._current,r=n?n:[],c=this._complete=i||_.Deferred(),o=_.Deferred(),h=_.Deferred(),u=_.bind(function(t,n){var i=n?s:e;i=i?i.name:"",this.publish(t,s,e),("start"!==t||"end"!==t)&&this.publish(i+":"+t)},this),a=_.bind(function(){this._transitioning=!1,this._current=s,u("fail"),c.reject()},this),f=_.bind(function(){u("enter"),this._transitioning=!1,this._current=e,u("end"),c.resolve()},this);if(!e)throw'Scene "'+t+'" not found!';return this._transitioning?c.reject():(u("start"),this._transitioning=!0,s?(s.to("exit",r,o),o.done(function(){u("exit",!0)})):o.resolve(),_.when(o).then(function(){e.to(e._initial||"enter",r,h)}).fail(a),h.then(f).fail(a),c.promise())}function e(t,n){if(!_.isFunction(t))return t;if(/^_/.test(n))return t;if(t.__wrapped)return t;var i=function(n,i){var e,s=!1;return i=i||_.Deferred(),this.async=function(){return s=!0,function(t){return t!==!1?i.resolve():i.reject()}},e=t.apply(this,n),this.async=void 0,s?i.promise():e!==!1?i.resolve():i.reject()};return i.__wrapped=!0,i}var s=t.Miso=t.Miso||{},r=s.Storyboard=function(t){t=t||{},this._originalOptions=t,this._context=t.context||this,this._id=_.uniqueId("scene"),t.scenes?(_.each(t.scenes,function(n,i){"function"==typeof n&&(t.scenes[i]={enter:n})}),_.each(r.HANDLERS,function(n){t.scenes[n]=t.scenes[n]||function(){return!0}}),this._buildScenes(t.scenes),this._initial=t.initial,this.to=i):(this.handlers={},_.each(r.HANDLERS,function(n){t[n]=t[n]||function(){return!0},this.handlers[n]=e(t[n],n)},this),this.to=n),_.each(t,function(t,n){-1===_.indexOf(r.BLACKLIST,n)&&(this[n]=_.isFunction(t)?function(n){return function(){t.apply(n._context||n,arguments)}}(this):t)},this)};r.HANDLERS=["enter","exit"],r.BLACKLIST=["_id","initial","scenes","enter","exit","context","_current"],_.extend(r.prototype,s.Events,{clone:function(){return this.scenes&&_.each(this._originalOptions.scenes,function(t,n){t instanceof s.Storyboard&&(this._originalOptions.scenes[n]=t.clone())},this),new s.Storyboard(this._originalOptions)},attach:function(t,n){return this.name=t,this.parent=n,n._context&&n._context._id!==n._id&&(this._context=n._context,this.scenes&&_.each(this.scenes,function(t){t.attach(t.name,this)},this)),this},start:function(){return this._current!==void 0?_.Deferred().resolve():this.to(this._initial)},cancelTransition:function(){this._complete.reject(),this._transitioning=!1},scene:function(){return this._current?this._current.name:null},is:function(t){return t===this._current.name},inTransition:function(){return this._transitioning===!0},setContext:function(t){this._context=t,this.scenes&&_.each(this.scenes,function(n){n.setContext(t)})},_buildScenes:function(t){this.scenes={},_.each(t,function(t,n){this.scenes[n]=t instanceof s.Storyboard?t:new s.Storyboard(t),this.scenes[n].attach(n,this)},this)}})}(this,_); -------------------------------------------------------------------------------- /dist/miso.storyboard.r.0.1.0.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Miso.Storyboard - v0.1.0 - 5/10/2013 3 | * http://github.com/misoproject/storyboard 4 | * Copyright (c) 2013 Alex Graul, Irene Ros, Rich Harris; 5 | * Dual Licensed: MIT, GPL 6 | * https://github.com/misoproject/storyboard/blob/master/LICENSE-MIT 7 | * https://github.com/misoproject/storyboard/blob/master/LICENSE-GPL 8 | *//** 9 | * Miso.Storyboard - v0.1.0 - 5/10/2013 10 | * http://github.com/misoproject/storyboard 11 | * Copyright (c) 2013 Alex Graul, Irene Ros, Rich Harris; 12 | * Dual Licensed: MIT, GPL 13 | * https://github.com/misoproject/storyboard/blob/master/LICENSE-MIT 14 | * https://github.com/misoproject/storyboard/blob/master/LICENSE-GPL 15 | *//* global _ */ 16 | (function(global, _) { 17 | 18 | var Miso = global.Miso = (global.Miso || {}); 19 | 20 | /** 21 | * Miso Events is a small set of methods that can be mixed into any object 22 | * to make it evented. It allows one to then subscribe to specific object events, 23 | * to publish events, unsubscribe and subscribeOnce. 24 | */ 25 | Miso.Events = { 26 | 27 | 28 | /** 29 | * Triggers a specific event and passes any additional arguments 30 | * to the callbacks subscribed to that event. 31 | * Params: 32 | * name - the name of the event to trigger 33 | * .* - any additional arguments to pass to callbacks. 34 | */ 35 | publish : function(name) { 36 | var args = _.toArray(arguments); 37 | args.shift(); 38 | 39 | if (this._events && this._events[name]) { 40 | _.each(this._events[name], function(subscription) { 41 | subscription.callback.apply(subscription.context || this, args); 42 | }, this); 43 | } 44 | return this; 45 | }, 46 | 47 | /** 48 | * Allows subscribing on an evented object to a specific name. 49 | * Provide a callback to trigger. 50 | * Params: 51 | * name - event to subscribe to 52 | * callback - callback to trigger 53 | * options - optional arguments 54 | * priority - allows rearranging of existing callbacks based on priority 55 | * context - allows attaching diff context to callback 56 | * token - allows callback identification by token. 57 | */ 58 | subscribe : function(name, callback, options) { 59 | options = options || {}; 60 | this._events = this._events || {}; 61 | this._events[name] = this._events[name] || []; 62 | 63 | var subscription = { 64 | callback : callback, 65 | priority : options.priority || 0, 66 | token : options.token || _.uniqueId("t"), 67 | context : options.context || this 68 | }; 69 | var position; 70 | _.each(this._events[name], function(event, index) { 71 | if (!_.isUndefined(position)) { return; } 72 | if (event.priority <= subscription.priority) { 73 | position = index; 74 | } 75 | }); 76 | 77 | this._events[name].splice(position, 0, subscription); 78 | return subscription.token; 79 | }, 80 | 81 | /** 82 | * Allows subscribing to an event once. When the event is triggered 83 | * this subscription will be removed. 84 | * Params: 85 | * name - name of event 86 | * callback - The callback to trigger 87 | */ 88 | subscribeOnce : function(name, callback) { 89 | this._events = this._events || {}; 90 | var token = _.uniqueId("t"); 91 | return this.subscribe(name, function() { 92 | this.unsubscribe(name, { token : token }); 93 | callback.apply(this, arguments); 94 | }, this, token); 95 | }, 96 | 97 | /** 98 | * Allows unsubscribing from a specific event 99 | * Params: 100 | * name - event to unsubscribe from 101 | * identifier - callback to remove OR token. 102 | */ 103 | unsubscribe : function(name, identifier) { 104 | 105 | if (_.isUndefined(this._events[name])) { return this; } 106 | 107 | if (_.isFunction(identifier)) { 108 | this._events[name] = _.reject(this._events[name], function(b) { 109 | return b.callback === identifier; 110 | }); 111 | 112 | } else if ( _.isString(identifier)) { 113 | this._events[name] = _.reject(this._events[name], function(b) { 114 | return b.token === identifier; 115 | }); 116 | 117 | } else { 118 | this._events[name] = []; 119 | } 120 | return this; 121 | } 122 | 123 | }; 124 | 125 | }(this, _)); 126 | 127 | /* global _ */ 128 | 129 | (function(global, _) { 130 | 131 | var Miso = global.Miso = (global.Miso || {}); 132 | 133 | /** 134 | * Creates a new storyboard. 135 | * Params: 136 | * options - various arguments 137 | * context - optional. Set a different context for the storyboard. 138 | * by default it's the scene that is being executed. 139 | * 140 | */ 141 | var Storyboard = Miso.Storyboard = function(options) { 142 | 143 | options = options || {}; 144 | 145 | // save all options so we can clone this later... 146 | this._originalOptions = options; 147 | 148 | // Set up the context for this storyboard. This will be 149 | // available as "this" inside the transition functions. 150 | this._context = options.context || this; 151 | 152 | // Assign custom id to the storyboard. 153 | this._id = _.uniqueId("scene"); 154 | 155 | // If there are scenes defined, initialize them. 156 | if (options.scenes) { 157 | 158 | // if the scenes are actually just set to a function, change them 159 | // to an enter property 160 | _.each(options.scenes, function(scene, name) { 161 | if (typeof scene === "function") { 162 | options.scenes[name] = { 163 | enter : scene 164 | }; 165 | } 166 | }); 167 | 168 | // make sure enter/exit are defined as passthroughs if not present. 169 | _.each(Storyboard.HANDLERS, function(action) { 170 | options.scenes[action] = options.scenes[action] || function() { return true; }; 171 | }); 172 | 173 | // Convert the scenes to actually nested storyboards. A "scene" 174 | // is really just a storyboard of one action with no child scenes. 175 | this._buildScenes(options.scenes); 176 | 177 | // Save the initial scene that we will start from. When .start is called 178 | // on the storyboard, this is the scene we transition to. 179 | this._initial = options.initial; 180 | 181 | // Transition function given that there are child scenes. 182 | this.to = children_to; 183 | 184 | } else { 185 | 186 | // This is a terminal storyboad in that it doesn't actually have any child 187 | // scenes, just its own enter and exit functions. 188 | 189 | this.handlers = {}; 190 | 191 | _.each(Storyboard.HANDLERS, function(action) { 192 | 193 | // save the enter and exit functions and if they don't exist, define them. 194 | options[action] = options[action] || function() { return true; }; 195 | 196 | // wrap functions so they can declare themselves as optionally 197 | // asynchronous without having to worry about deferred management. 198 | this.handlers[action] = wrap(options[action], action); 199 | 200 | }, this); 201 | 202 | // Transition function given that this is a terminal storyboard. 203 | this.to = leaf_to; 204 | } 205 | 206 | 207 | // Iterate over all the properties defiend in the options and as long as they 208 | // are not on a black list, save them on the actual scene. This allows us to define 209 | // helper methods that are not going to be wrapped (and thus instrumented with 210 | // any deferred and async behavior.) 211 | _.each(options, function(prop, name) { 212 | 213 | if (_.indexOf(Storyboard.BLACKLIST, name) !== -1) { 214 | return; 215 | } 216 | 217 | if (_.isFunction(prop)) { 218 | this[name] = (function(contextOwner) { 219 | return function() { 220 | prop.apply(contextOwner._context || contextOwner, arguments); 221 | }; 222 | }(this)); 223 | } else { 224 | this[name] = prop; 225 | } 226 | 227 | }, this); 228 | 229 | }; 230 | 231 | Storyboard.HANDLERS = ["enter","exit"]; 232 | Storyboard.BLACKLIST = ["_id", "initial","scenes","enter","exit","context","_current"]; 233 | 234 | _.extend(Storyboard.prototype, Miso.Events, { 235 | 236 | /** 237 | * Allows for cloning of a storyboard 238 | * Returns: 239 | * s - a new Miso.Storyboard 240 | */ 241 | clone : function() { 242 | 243 | // clone nested storyboard 244 | if (this.scenes) { 245 | _.each(this._originalOptions.scenes, function(scene, name) { 246 | if (scene instanceof Miso.Storyboard) { 247 | this._originalOptions.scenes[name] = scene.clone(); 248 | } 249 | }, this); 250 | } 251 | 252 | return new Miso.Storyboard(this._originalOptions); 253 | }, 254 | 255 | /** 256 | * Attach a new scene to an existing storyboard. 257 | * Params: 258 | * name - The name of the scene 259 | * parent - The storyboard to attach this current scene to. 260 | */ 261 | attach : function(name, parent) { 262 | 263 | this.name = name; 264 | this.parent = parent; 265 | 266 | // if the parent has a custom context the child should inherit it 267 | if (parent._context && (parent._context._id !== parent._id)) { 268 | 269 | this._context = parent._context; 270 | if (this.scenes) { 271 | _.each(this.scenes , function(scene) { 272 | scene.attach(scene.name, this); 273 | }, this); 274 | } 275 | } 276 | return this; 277 | }, 278 | 279 | /** 280 | * Instruct a storyboard to kick off its initial scene. 281 | * This returns a deferred object just like all the .to calls. 282 | * If the initial scene is asynchronous, you will need to define a .then 283 | * callback to wait on the start scene to end its enter transition. 284 | */ 285 | start : function() { 286 | // if we've already started just return a happily resoved deferred 287 | if (typeof this._current !== "undefined") { 288 | return _.Deferred().resolve(); 289 | } else { 290 | return this.to(this._initial); 291 | } 292 | }, 293 | 294 | /** 295 | * Cancels a transition in action. This doesn't actually kill the function 296 | * that is currently in play! It does reject the deferred one was awaiting 297 | * from that transition. 298 | */ 299 | cancelTransition : function() { 300 | this._complete.reject(); 301 | this._transitioning = false; 302 | }, 303 | 304 | /** 305 | * Returns the current scene. 306 | * Returns: 307 | * scene - current scene name, or null. 308 | */ 309 | scene : function() { 310 | return this._current ? this._current.name : null; 311 | }, 312 | 313 | /** 314 | * Checks if the current scene is of a specific name. 315 | * Params: 316 | * scene - scene to check as to whether it is the current scene 317 | * Returns: 318 | * true if it is, false otherwise. 319 | */ 320 | is : function( scene ) { 321 | return (scene === this._current.name); 322 | }, 323 | 324 | /** 325 | * Returns true if storyboard is in the middle of a transition. 326 | */ 327 | inTransition : function() { 328 | return (this._transitioning === true); 329 | }, 330 | 331 | /** 332 | * Allows the changing of context. This will alter what "this" 333 | * will be set to inside the transition methods. 334 | */ 335 | setContext : function(context) { 336 | this._context = context; 337 | if (this.scenes) { 338 | _.each(this.scenes, function(scene) { 339 | scene.setContext(context); 340 | }); 341 | } 342 | }, 343 | 344 | _buildScenes : function( scenes ) { 345 | this.scenes = {}; 346 | _.each(scenes, function(scene, name) { 347 | this.scenes[name] = scene instanceof Miso.Storyboard ? scene : new Miso.Storyboard(scene); 348 | this.scenes[name].attach(name, this); 349 | }, this); 350 | } 351 | }); 352 | 353 | // Used as the to function to scenes which do not have children 354 | // These scenes only have their own enter and exit. 355 | function leaf_to( sceneName, argsArr, deferred ) { 356 | 357 | this._transitioning = true; 358 | var complete = this._complete = deferred || _.Deferred(), 359 | args = argsArr ? argsArr : [], 360 | handlerComplete = _.Deferred() 361 | .done(_.bind(function() { 362 | this._transitioning = false; 363 | this._current = sceneName; 364 | complete.resolve(); 365 | }, this)) 366 | .fail(_.bind(function() { 367 | this._transitioning = false; 368 | complete.reject(); 369 | }, this)); 370 | 371 | this.handlers[sceneName].call(this._context, args, handlerComplete); 372 | 373 | return complete.promise(); 374 | } 375 | 376 | // Used as the function to scenes that do have children. 377 | function children_to( sceneName, argsArr, deferred ) { 378 | var toScene = this.scenes[sceneName], 379 | fromScene = this._current, 380 | args = argsArr ? argsArr : [], 381 | complete = this._complete = deferred || _.Deferred(), 382 | exitComplete = _.Deferred(), 383 | enterComplete = _.Deferred(), 384 | publish = _.bind(function(name, isExit) { 385 | var sceneName = isExit ? fromScene : toScene; 386 | sceneName = sceneName ? sceneName.name : ""; 387 | 388 | this.publish(name, fromScene, toScene); 389 | if (name !== "start" || name !== "end") { 390 | this.publish(sceneName + ":" + name); 391 | } 392 | 393 | }, this), 394 | bailout = _.bind(function() { 395 | this._transitioning = false; 396 | this._current = fromScene; 397 | publish("fail"); 398 | complete.reject(); 399 | }, this), 400 | success = _.bind(function() { 401 | publish("enter"); 402 | this._transitioning = false; 403 | this._current = toScene; 404 | publish("end"); 405 | complete.resolve(); 406 | }, this); 407 | 408 | 409 | if (!toScene) { 410 | throw "Scene \"" + sceneName + "\" not found!"; 411 | } 412 | 413 | // we in the middle of a transition? 414 | if (this._transitioning) { 415 | return complete.reject(); 416 | } 417 | 418 | publish("start"); 419 | 420 | this._transitioning = true; 421 | 422 | if (fromScene) { 423 | 424 | // we are coming from a scene, so transition out of it. 425 | fromScene.to("exit", args, exitComplete); 426 | exitComplete.done(function() { 427 | publish("exit", true); 428 | }); 429 | 430 | } else { 431 | exitComplete.resolve(); 432 | } 433 | 434 | // when we're done exiting, enter the next set 435 | _.when(exitComplete).then(function() { 436 | 437 | toScene.to(toScene._initial || "enter", args, enterComplete); 438 | 439 | }).fail(bailout); 440 | 441 | enterComplete 442 | .then(success) 443 | .fail(bailout); 444 | 445 | return complete.promise(); 446 | } 447 | 448 | function wrap(func, name) { 449 | 450 | //don't wrap non-functions 451 | if ( !_.isFunction(func)) { return func; } 452 | //don't wrap private functions 453 | if ( /^_/.test(name) ) { return func; } 454 | //don't wrap wrapped functions 455 | if (func.__wrapped) { return func; } 456 | 457 | var wrappedFunc = function(args, deferred) { 458 | var async = false, 459 | result; 460 | 461 | deferred = deferred || _.Deferred(); 462 | 463 | this.async = function() { 464 | async = true; 465 | return function(pass) { 466 | return (pass !== false) ? deferred.resolve() : deferred.reject(); 467 | }; 468 | }; 469 | 470 | result = func.apply(this, args); 471 | this.async = undefined; 472 | if (!async) { 473 | return (result !== false) ? deferred.resolve() : deferred.reject(); 474 | } 475 | return deferred.promise(); 476 | }; 477 | 478 | wrappedFunc.__wrapped = true; 479 | return wrappedFunc; 480 | } 481 | 482 | }(this, _)); 483 | 484 | /* global exports,define,module */ 485 | (function(global) { 486 | 487 | var Miso = global.Miso || {}; 488 | delete window.Miso; 489 | 490 | // CommonJS module is defined 491 | if (typeof exports !== "undefined") { 492 | if (typeof module !== "undefined" && module.exports) { 493 | // Export module 494 | module.exports = Miso; 495 | } 496 | exports.miso = Miso; 497 | 498 | } else if (typeof define === "function" && define.amd) { 499 | // Register as a named module with AMD. 500 | define("miso", [], function() { 501 | return Miso; 502 | }); 503 | } 504 | }(this)); -------------------------------------------------------------------------------- /dist/node/miso.storyboard.0.0.1.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | _.mixin(require("underscore.deferred")); 3 | 4 | /** 5 | * Miso.Storyboard - v0.0.1 - 11/8/2012 6 | * http://github.com/misoproject/storyboard 7 | * Copyright (c) 2012 Alex Graul, Irene Ros, Rich Harris; 8 | * Dual Licensed: MIT, GPL 9 | * https://github.com/misoproject/storyboard/blob/master/LICENSE-MIT 10 | */ 11 | 12 | (function(global, _) { 13 | 14 | var Miso = global.Miso = (global.Miso || {}); 15 | 16 | /** 17 | * Miso Events is a small set of methods that can be mixed into any object 18 | * to make it evented. It allows one to then subscribe to specific object events, 19 | * to publish events, unsubscribe and subscribeOnce. 20 | */ 21 | Miso.Events = { 22 | 23 | 24 | /** 25 | * Triggers a specific event and passes any additional arguments 26 | * to the callbacks subscribed to that event. 27 | * Params: 28 | * name - the name of the event to trigger 29 | * .* - any additional arguments to pass to callbacks. 30 | */ 31 | publish : function(name) { 32 | var args = _.toArray(arguments); 33 | args.shift(); 34 | 35 | if (this._events && this._events[name]) { 36 | _.each(this._events[name], function(subscription) { 37 | subscription.callback.apply(subscription.context || this, args); 38 | }, this); 39 | } 40 | return this; 41 | }, 42 | 43 | /** 44 | * Allows subscribing on an evented object to a specific name. 45 | * Provide a callback to trigger. 46 | * Params: 47 | * name - event to subscribe to 48 | * callback - callback to trigger 49 | * options - optional arguments 50 | * priority - allows rearranging of existing callbacks based on priority 51 | * context - allows attaching diff context to callback 52 | * token - allows callback identification by token. 53 | */ 54 | subscribe : function(name, callback, options) { 55 | options = options || {}; 56 | this._events = this._events || {}; 57 | this._events[name] = this._events[name] || []; 58 | 59 | var subscription = { 60 | callback : callback, 61 | priority : options.priority || 0, 62 | token : options.token || _.uniqueId('t'), 63 | context : options.context || this 64 | }; 65 | var position; 66 | _.each(this._events[name], function(event, index) { 67 | if (!_.isUndefined(position)) { return; } 68 | if (event.priority <= subscription.priority) { 69 | position = index; 70 | } 71 | }); 72 | 73 | this._events[name].splice(position, 0, subscription); 74 | return subscription.token; 75 | }, 76 | 77 | /** 78 | * Allows subscribing to an event once. When the event is triggered 79 | * this subscription will be removed. 80 | * Params: 81 | * name - name of event 82 | * callback - The callback to trigger 83 | */ 84 | subscribeOnce : function(name, callback) { 85 | this._events = this._events || {}; 86 | var token = _.uniqueId('t'); 87 | return this.subscribe(name, function() { 88 | this.unsubscribe(name, { token : token }); 89 | callback.apply(this, arguments); 90 | }, this, token); 91 | }, 92 | 93 | /** 94 | * Allows unsubscribing from a specific event 95 | * Params: 96 | * name - event to unsubscribe from 97 | * identifier - callback to remove OR token. 98 | */ 99 | unsubscribe : function(name, identifier) { 100 | 101 | if (_.isUndefined(this._events[name])) { return this; } 102 | 103 | if (_.isFunction(identifier)) { 104 | this._events[name] = _.reject(this._events[name], function(b) { 105 | return b.callback === identifier; 106 | }); 107 | 108 | } else if ( _.isString(identifier)) { 109 | this._events[name] = _.reject(this._events[name], function(b) { 110 | return b.token === identifier; 111 | }); 112 | 113 | } else { 114 | this._events[name] = []; 115 | } 116 | return this; 117 | } 118 | 119 | }; 120 | 121 | }(this, _)); 122 | 123 | (function(global, _) { 124 | 125 | var Miso = global.Miso = (global.Miso || {}); 126 | 127 | /** 128 | * Creates a new storyboard. 129 | * Params: 130 | * options - various arguments 131 | * context - optional. Set a different context for the storyboard. 132 | * by default it's the scene that is being executed. 133 | * 134 | */ 135 | var Storyboard = Miso.Storyboard = function(options) { 136 | 137 | options = options || {}; 138 | 139 | // save all options so we can clone this later... 140 | this._originalOptions = options; 141 | 142 | // Set up the context for this storyboard. This will be 143 | // available as 'this' inside the transition functions. 144 | this._context = options.context || this; 145 | 146 | // Assign custom id to the storyboard. 147 | this._id = _.uniqueId('scene'); 148 | 149 | // If there are scenes defined, initialize them. 150 | if (options.scenes) { 151 | 152 | // Convert the scenes to actually nested storyboards. A 'scene' 153 | // is really just a storyboard of one action with no child scenes. 154 | this._buildScenes(options.scenes); 155 | 156 | // Save the initial scene that we will start from. When .start is called 157 | // on the storyboard, this is the scene we transition to. 158 | this._initial = options.initial; 159 | 160 | // Transition function given that there are child scenes. 161 | this.to = children_to; 162 | 163 | } else { 164 | 165 | // This is a terminal storyboad in that it doesn't actually have any child 166 | // scenes, just its own enter and exit functions. 167 | 168 | this.handlers = {}; 169 | 170 | _.each(Storyboard.HANDLERS, function(action) { 171 | 172 | // save the enter and exit functions and if they don't exist, define them. 173 | options[action] = options[action] || function() { return true; }; 174 | 175 | // wrap functions so they can declare themselves as optionally 176 | // asynchronous without having to worry about deferred management. 177 | this.handlers[action] = wrap(options[action], action); 178 | 179 | }, this); 180 | 181 | // Transition function given that this is a terminal storyboard. 182 | this.to = leaf_to; 183 | } 184 | 185 | 186 | // Iterate over all the properties defiend in the options and as long as they 187 | // are not on a black list, save them on the actual scene. This allows us to define 188 | // helper methods that are not going to be wrapped (and thus instrumented with 189 | // any deferred and async behavior.) 190 | _.each(options, function(prop, name) { 191 | 192 | if (_.indexOf(Storyboard.BLACKLIST, name) !== -1) { 193 | return; 194 | } 195 | this[name] = prop; 196 | }, this); 197 | 198 | }; 199 | 200 | Storyboard.HANDLERS = ['enter','exit']; 201 | Storyboard.BLACKLIST = ['_id', 'initial','scenes','enter','exit','context','_current']; 202 | 203 | _.extend(Storyboard.prototype, Miso.Events, { 204 | 205 | /** 206 | * Allows for cloning of a storyboard 207 | * Returns: 208 | * s - a new Miso.Storyboard 209 | */ 210 | clone : function() { 211 | 212 | // clone nested storyboard 213 | if (this.scenes) { 214 | _.each(this._originalOptions.scenes, function(scene, name) { 215 | if (scene instanceof Miso.Storyboard) { 216 | this._originalOptions.scenes[name] = scene.clone(); 217 | } 218 | }, this); 219 | } 220 | 221 | return new Miso.Storyboard(this._originalOptions); 222 | }, 223 | 224 | /** 225 | * Attach a new scene to an existing storyboard. 226 | * Params: 227 | * name - The name of the scene 228 | * parent - The storyboard to attach this current scene to. 229 | */ 230 | attach : function(name, parent) { 231 | 232 | this.name = name; 233 | this.parent = parent; 234 | 235 | // if the parent has a custom context the child should inherit it 236 | if (parent._context && (parent._context._id !== parent._id)) { 237 | 238 | this._context = parent._context; 239 | if (this.scenes) { 240 | _.each(this.scenes , function(scene) { 241 | scene.attach(scene.name, this); 242 | }, this); 243 | } 244 | } 245 | return this; 246 | }, 247 | 248 | /** 249 | * Instruct a storyboard to kick off its initial scene. 250 | * This returns a deferred object just like all the .to calls. 251 | * If the initial scene is asynchronous, you will need to define a .then 252 | * callback to wait on the start scene to end its enter transition. 253 | */ 254 | start : function() { 255 | // if we've already started just return a happily resoved deferred 256 | if (typeof this._current !== "undefined") { 257 | return _.Deferred().resolve(); 258 | } else { 259 | return this.to(this._initial); 260 | } 261 | }, 262 | 263 | /** 264 | * Cancels a transition in action. This doesn't actually kill the function 265 | * that is currently in play! It does reject the deferred one was awaiting 266 | * from that transition. 267 | */ 268 | cancelTransition : function() { 269 | this._complete.reject(); 270 | this._transitioning = false; 271 | }, 272 | 273 | /** 274 | * Returns the current scene. 275 | * Returns: 276 | * scene - current scene name, or null. 277 | */ 278 | scene : function() { 279 | return this._current ? this._current.name : null; 280 | }, 281 | 282 | /** 283 | * Checks if the current scene is of a specific name. 284 | * Params: 285 | * scene - scene to check as to whether it is the current scene 286 | * Returns: 287 | * true if it is, false otherwise. 288 | */ 289 | is : function( scene ) { 290 | return (scene === this._current.name); 291 | }, 292 | 293 | /** 294 | * Returns true if storyboard is in the middle of a transition. 295 | */ 296 | inTransition : function() { 297 | return (this._transitioning === true); 298 | }, 299 | 300 | /** 301 | * Allows the changing of context. This will alter what 'this' 302 | * will be set to inside the transition methods. 303 | */ 304 | setContext : function(context) { 305 | this._context = context; 306 | if (this.scenes) { 307 | _.each(this.scenes, function(scene) { 308 | scene.setContext(context); 309 | }); 310 | } 311 | }, 312 | 313 | _buildScenes : function( scenes ) { 314 | this.scenes = {}; 315 | _.each(scenes, function(scene, name) { 316 | this.scenes[name] = scene instanceof Miso.Storyboard ? scene : new Miso.Storyboard(scene); 317 | this.scenes[name].attach(name, this); 318 | }, this); 319 | } 320 | }); 321 | 322 | // Used as the to function to scenes which do not have children 323 | // These scenes only have their own enter and exit. 324 | function leaf_to( sceneName, argsArr, deferred ) { 325 | 326 | this._transitioning = true; 327 | var complete = this._complete = deferred || _.Deferred(), 328 | args = argsArr ? argsArr : [], 329 | handlerComplete = _.Deferred() 330 | .done(_.bind(function() { 331 | this._transitioning = false; 332 | this._current = sceneName; 333 | complete.resolve(); 334 | }, this)) 335 | .fail(_.bind(function() { 336 | this._transitioning = false; 337 | complete.reject(); 338 | }, this)); 339 | 340 | this.handlers[sceneName].call(this._context, args, handlerComplete); 341 | 342 | return complete.promise(); 343 | } 344 | 345 | // Used as the function to scenes that do have children. 346 | function children_to( sceneName, argsArr, deferred ) { 347 | var toScene = this.scenes[sceneName], 348 | fromScene = this._current, 349 | args = argsArr ? argsArr : [], 350 | complete = this._complete = deferred || _.Deferred(), 351 | exitComplete = _.Deferred(), 352 | enterComplete = _.Deferred(), 353 | publish = _.bind(function(name, isExit) { 354 | var sceneName = isExit ? fromScene : toScene; 355 | sceneName = sceneName ? sceneName.name : ''; 356 | 357 | this.publish(name, fromScene, toScene); 358 | if (name !== 'start' || name !== 'end') { 359 | this.publish(sceneName + ":" + name); 360 | } 361 | 362 | }, this), 363 | bailout = _.bind(function() { 364 | this._transitioning = false; 365 | this._current = fromScene; 366 | publish('fail'); 367 | complete.reject(); 368 | }, this), 369 | success = _.bind(function() { 370 | publish('enter'); 371 | this._transitioning = false; 372 | this._current = toScene; 373 | publish('end'); 374 | complete.resolve(); 375 | }, this); 376 | 377 | 378 | if (!toScene) { 379 | throw "Scene '" + sceneName + "' not found!"; 380 | } 381 | 382 | // we in the middle of a transition? 383 | if (this._transitioning) { 384 | return complete.reject(); 385 | } 386 | 387 | publish('start'); 388 | 389 | this._transitioning = true; 390 | 391 | if (fromScene) { 392 | 393 | // we are coming from a scene, so transition out of it. 394 | fromScene.to('exit', args, exitComplete); 395 | exitComplete.done(function() { 396 | publish('exit', true); 397 | }); 398 | 399 | } else { 400 | exitComplete.resolve(); 401 | } 402 | 403 | // when we're done exiting, enter the next set 404 | _.when(exitComplete).then(function() { 405 | 406 | toScene.to('enter', args, enterComplete); 407 | 408 | }).fail(bailout); 409 | 410 | enterComplete 411 | .then(success) 412 | .fail(bailout); 413 | 414 | return complete.promise(); 415 | } 416 | 417 | function wrap(func, name) { 418 | 419 | //don't wrap non-functions 420 | if ( !_.isFunction(func)) { return func; } 421 | //don't wrap private functions 422 | if ( /^_/.test(name) ) { return func; } 423 | //don't wrap wrapped functions 424 | if (func.__wrapped) { return func; } 425 | 426 | var wrappedFunc = function(args, deferred) { 427 | var async = false, 428 | result; 429 | 430 | deferred = deferred || _.Deferred(); 431 | 432 | this.async = function() { 433 | async = true; 434 | return function(pass) { 435 | return (pass !== false) ? deferred.resolve() : deferred.reject(); 436 | }; 437 | }; 438 | 439 | result = func.apply(this, args); 440 | this.async = undefined; 441 | if (!async) { 442 | return (result !== false) ? deferred.resolve() : deferred.reject(); 443 | } 444 | return deferred.promise(); 445 | }; 446 | 447 | wrappedFunc.__wrapped = true; 448 | return wrappedFunc; 449 | } 450 | 451 | 452 | 453 | }(this, _)); 454 | 455 | 456 | // Expose the module 457 | module.exports = this.Miso; -------------------------------------------------------------------------------- /dist/node/miso.storyboard.deps.0.0.1.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | _.mixin(require("underscore.deferred")); 3 | 4 | /** 5 | * Miso.Storyboard - v0.0.1 - 3/23/2013 6 | * http://github.com/misoproject/storyboard 7 | * Copyright (c) 2013 Alex Graul, Irene Ros, Rich Harris; 8 | * Dual Licensed: MIT, GPL 9 | * https://github.com/misoproject/storyboard/blob/master/LICENSE-MIT 10 | * https://github.com/misoproject/storyboard/blob/master/LICENSE-GPL 11 | */(function(global, _) { 12 | 13 | var Miso = global.Miso = (global.Miso || {}); 14 | 15 | /** 16 | * Miso Events is a small set of methods that can be mixed into any object 17 | * to make it evented. It allows one to then subscribe to specific object events, 18 | * to publish events, unsubscribe and subscribeOnce. 19 | */ 20 | Miso.Events = { 21 | 22 | 23 | /** 24 | * Triggers a specific event and passes any additional arguments 25 | * to the callbacks subscribed to that event. 26 | * Params: 27 | * name - the name of the event to trigger 28 | * .* - any additional arguments to pass to callbacks. 29 | */ 30 | publish : function(name) { 31 | var args = _.toArray(arguments); 32 | args.shift(); 33 | 34 | if (this._events && this._events[name]) { 35 | _.each(this._events[name], function(subscription) { 36 | subscription.callback.apply(subscription.context || this, args); 37 | }, this); 38 | } 39 | return this; 40 | }, 41 | 42 | /** 43 | * Allows subscribing on an evented object to a specific name. 44 | * Provide a callback to trigger. 45 | * Params: 46 | * name - event to subscribe to 47 | * callback - callback to trigger 48 | * options - optional arguments 49 | * priority - allows rearranging of existing callbacks based on priority 50 | * context - allows attaching diff context to callback 51 | * token - allows callback identification by token. 52 | */ 53 | subscribe : function(name, callback, options) { 54 | options = options || {}; 55 | this._events = this._events || {}; 56 | this._events[name] = this._events[name] || []; 57 | 58 | var subscription = { 59 | callback : callback, 60 | priority : options.priority || 0, 61 | token : options.token || _.uniqueId('t'), 62 | context : options.context || this 63 | }; 64 | var position; 65 | _.each(this._events[name], function(event, index) { 66 | if (!_.isUndefined(position)) { return; } 67 | if (event.priority <= subscription.priority) { 68 | position = index; 69 | } 70 | }); 71 | 72 | this._events[name].splice(position, 0, subscription); 73 | return subscription.token; 74 | }, 75 | 76 | /** 77 | * Allows subscribing to an event once. When the event is triggered 78 | * this subscription will be removed. 79 | * Params: 80 | * name - name of event 81 | * callback - The callback to trigger 82 | */ 83 | subscribeOnce : function(name, callback) { 84 | this._events = this._events || {}; 85 | var token = _.uniqueId('t'); 86 | return this.subscribe(name, function() { 87 | this.unsubscribe(name, { token : token }); 88 | callback.apply(this, arguments); 89 | }, this, token); 90 | }, 91 | 92 | /** 93 | * Allows unsubscribing from a specific event 94 | * Params: 95 | * name - event to unsubscribe from 96 | * identifier - callback to remove OR token. 97 | */ 98 | unsubscribe : function(name, identifier) { 99 | 100 | if (_.isUndefined(this._events[name])) { return this; } 101 | 102 | if (_.isFunction(identifier)) { 103 | this._events[name] = _.reject(this._events[name], function(b) { 104 | return b.callback === identifier; 105 | }); 106 | 107 | } else if ( _.isString(identifier)) { 108 | this._events[name] = _.reject(this._events[name], function(b) { 109 | return b.token === identifier; 110 | }); 111 | 112 | } else { 113 | this._events[name] = []; 114 | } 115 | return this; 116 | } 117 | 118 | }; 119 | 120 | }(this, _)); 121 | 122 | (function(global, _) { 123 | 124 | var Miso = global.Miso = (global.Miso || {}); 125 | 126 | /** 127 | * Creates a new storyboard. 128 | * Params: 129 | * options - various arguments 130 | * context - optional. Set a different context for the storyboard. 131 | * by default it's the scene that is being executed. 132 | * 133 | */ 134 | var Storyboard = Miso.Storyboard = function(options) { 135 | 136 | options = options || {}; 137 | 138 | // save all options so we can clone this later... 139 | this._originalOptions = options; 140 | 141 | // Set up the context for this storyboard. This will be 142 | // available as 'this' inside the transition functions. 143 | this._context = options.context || this; 144 | 145 | // Assign custom id to the storyboard. 146 | this._id = _.uniqueId('scene'); 147 | 148 | // If there are scenes defined, initialize them. 149 | if (options.scenes) { 150 | 151 | // Convert the scenes to actually nested storyboards. A 'scene' 152 | // is really just a storyboard of one action with no child scenes. 153 | this._buildScenes(options.scenes); 154 | 155 | // Save the initial scene that we will start from. When .start is called 156 | // on the storyboard, this is the scene we transition to. 157 | this._initial = options.initial; 158 | 159 | // Transition function given that there are child scenes. 160 | this.to = children_to; 161 | 162 | } else { 163 | 164 | // This is a terminal storyboad in that it doesn't actually have any child 165 | // scenes, just its own enter and exit functions. 166 | 167 | this.handlers = {}; 168 | 169 | _.each(Storyboard.HANDLERS, function(action) { 170 | 171 | // save the enter and exit functions and if they don't exist, define them. 172 | options[action] = options[action] || function() { return true; }; 173 | 174 | // wrap functions so they can declare themselves as optionally 175 | // asynchronous without having to worry about deferred management. 176 | this.handlers[action] = wrap(options[action], action); 177 | 178 | }, this); 179 | 180 | // Transition function given that this is a terminal storyboard. 181 | this.to = leaf_to; 182 | } 183 | 184 | 185 | // Iterate over all the properties defiend in the options and as long as they 186 | // are not on a black list, save them on the actual scene. This allows us to define 187 | // helper methods that are not going to be wrapped (and thus instrumented with 188 | // any deferred and async behavior.) 189 | _.each(options, function(prop, name) { 190 | 191 | if (_.indexOf(Storyboard.BLACKLIST, name) !== -1) { 192 | return; 193 | } 194 | 195 | if (_.isFunction(prop)) { 196 | this[name] = (function(contextOwner) { 197 | return function() { 198 | prop.apply(contextOwner._context || contextOwner, arguments); 199 | }; 200 | }(this)); 201 | } else { 202 | this[name] = prop; 203 | } 204 | 205 | 206 | }, this); 207 | 208 | }; 209 | 210 | Storyboard.HANDLERS = ['enter','exit']; 211 | Storyboard.BLACKLIST = ['_id', 'initial','scenes','enter','exit','context','_current']; 212 | 213 | _.extend(Storyboard.prototype, Miso.Events, { 214 | 215 | /** 216 | * Allows for cloning of a storyboard 217 | * Returns: 218 | * s - a new Miso.Storyboard 219 | */ 220 | clone : function() { 221 | 222 | // clone nested storyboard 223 | if (this.scenes) { 224 | _.each(this._originalOptions.scenes, function(scene, name) { 225 | if (scene instanceof Miso.Storyboard) { 226 | this._originalOptions.scenes[name] = scene.clone(); 227 | } 228 | }, this); 229 | } 230 | 231 | return new Miso.Storyboard(this._originalOptions); 232 | }, 233 | 234 | /** 235 | * Attach a new scene to an existing storyboard. 236 | * Params: 237 | * name - The name of the scene 238 | * parent - The storyboard to attach this current scene to. 239 | */ 240 | attach : function(name, parent) { 241 | 242 | this.name = name; 243 | this.parent = parent; 244 | 245 | // if the parent has a custom context the child should inherit it 246 | if (parent._context && (parent._context._id !== parent._id)) { 247 | 248 | this._context = parent._context; 249 | if (this.scenes) { 250 | _.each(this.scenes , function(scene) { 251 | scene.attach(scene.name, this); 252 | }, this); 253 | } 254 | } 255 | return this; 256 | }, 257 | 258 | /** 259 | * Instruct a storyboard to kick off its initial scene. 260 | * This returns a deferred object just like all the .to calls. 261 | * If the initial scene is asynchronous, you will need to define a .then 262 | * callback to wait on the start scene to end its enter transition. 263 | */ 264 | start : function() { 265 | // if we've already started just return a happily resoved deferred 266 | if (typeof this._current !== "undefined") { 267 | return _.Deferred().resolve(); 268 | } else { 269 | return this.to(this._initial); 270 | } 271 | }, 272 | 273 | /** 274 | * Cancels a transition in action. This doesn't actually kill the function 275 | * that is currently in play! It does reject the deferred one was awaiting 276 | * from that transition. 277 | */ 278 | cancelTransition : function() { 279 | this._complete.reject(); 280 | this._transitioning = false; 281 | }, 282 | 283 | /** 284 | * Returns the current scene. 285 | * Returns: 286 | * scene - current scene name, or null. 287 | */ 288 | scene : function() { 289 | return this._current ? this._current.name : null; 290 | }, 291 | 292 | /** 293 | * Checks if the current scene is of a specific name. 294 | * Params: 295 | * scene - scene to check as to whether it is the current scene 296 | * Returns: 297 | * true if it is, false otherwise. 298 | */ 299 | is : function( scene ) { 300 | return (scene === this._current.name); 301 | }, 302 | 303 | /** 304 | * Returns true if storyboard is in the middle of a transition. 305 | */ 306 | inTransition : function() { 307 | return (this._transitioning === true); 308 | }, 309 | 310 | /** 311 | * Allows the changing of context. This will alter what 'this' 312 | * will be set to inside the transition methods. 313 | */ 314 | setContext : function(context) { 315 | this._context = context; 316 | if (this.scenes) { 317 | _.each(this.scenes, function(scene) { 318 | scene.setContext(context); 319 | }); 320 | } 321 | }, 322 | 323 | _buildScenes : function( scenes ) { 324 | this.scenes = {}; 325 | _.each(scenes, function(scene, name) { 326 | this.scenes[name] = scene instanceof Miso.Storyboard ? scene : new Miso.Storyboard(scene); 327 | this.scenes[name].attach(name, this); 328 | }, this); 329 | } 330 | }); 331 | 332 | // Used as the to function to scenes which do not have children 333 | // These scenes only have their own enter and exit. 334 | function leaf_to( sceneName, argsArr, deferred ) { 335 | 336 | this._transitioning = true; 337 | var complete = this._complete = deferred || _.Deferred(), 338 | args = argsArr ? argsArr : [], 339 | handlerComplete = _.Deferred() 340 | .done(_.bind(function() { 341 | this._transitioning = false; 342 | this._current = sceneName; 343 | complete.resolve(); 344 | }, this)) 345 | .fail(_.bind(function() { 346 | this._transitioning = false; 347 | complete.reject(); 348 | }, this)); 349 | 350 | this.handlers[sceneName].call(this._context, args, handlerComplete); 351 | 352 | return complete.promise(); 353 | } 354 | 355 | // Used as the function to scenes that do have children. 356 | function children_to( sceneName, argsArr, deferred ) { 357 | var toScene = this.scenes[sceneName], 358 | fromScene = this._current, 359 | args = argsArr ? argsArr : [], 360 | complete = this._complete = deferred || _.Deferred(), 361 | exitComplete = _.Deferred(), 362 | enterComplete = _.Deferred(), 363 | publish = _.bind(function(name, isExit) { 364 | var sceneName = isExit ? fromScene : toScene; 365 | sceneName = sceneName ? sceneName.name : ''; 366 | 367 | this.publish(name, fromScene, toScene); 368 | if (name !== 'start' || name !== 'end') { 369 | this.publish(sceneName + ":" + name); 370 | } 371 | 372 | }, this), 373 | bailout = _.bind(function() { 374 | this._transitioning = false; 375 | this._current = fromScene; 376 | publish('fail'); 377 | complete.reject(); 378 | }, this), 379 | success = _.bind(function() { 380 | publish('enter'); 381 | this._transitioning = false; 382 | this._current = toScene; 383 | publish('end'); 384 | complete.resolve(); 385 | }, this); 386 | 387 | 388 | if (!toScene) { 389 | throw "Scene '" + sceneName + "' not found!"; 390 | } 391 | 392 | // we in the middle of a transition? 393 | if (this._transitioning) { 394 | return complete.reject(); 395 | } 396 | 397 | publish('start'); 398 | 399 | this._transitioning = true; 400 | 401 | if (fromScene) { 402 | 403 | // we are coming from a scene, so transition out of it. 404 | fromScene.to('exit', args, exitComplete); 405 | exitComplete.done(function() { 406 | publish('exit', true); 407 | }); 408 | 409 | } else { 410 | exitComplete.resolve(); 411 | } 412 | 413 | // when we're done exiting, enter the next set 414 | _.when(exitComplete).then(function() { 415 | 416 | toScene.to('enter', args, enterComplete); 417 | 418 | }).fail(bailout); 419 | 420 | enterComplete 421 | .then(success) 422 | .fail(bailout); 423 | 424 | return complete.promise(); 425 | } 426 | 427 | function wrap(func, name) { 428 | 429 | //don't wrap non-functions 430 | if ( !_.isFunction(func)) { return func; } 431 | //don't wrap private functions 432 | if ( /^_/.test(name) ) { return func; } 433 | //don't wrap wrapped functions 434 | if (func.__wrapped) { return func; } 435 | 436 | var wrappedFunc = function(args, deferred) { 437 | var async = false, 438 | result; 439 | 440 | deferred = deferred || _.Deferred(); 441 | 442 | this.async = function() { 443 | async = true; 444 | return function(pass) { 445 | return (pass !== false) ? deferred.resolve() : deferred.reject(); 446 | }; 447 | }; 448 | 449 | result = func.apply(this, args); 450 | this.async = undefined; 451 | if (!async) { 452 | return (result !== false) ? deferred.resolve() : deferred.reject(); 453 | } 454 | return deferred.promise(); 455 | }; 456 | 457 | wrappedFunc.__wrapped = true; 458 | return wrappedFunc; 459 | } 460 | 461 | 462 | 463 | }(this, _)); 464 | 465 | 466 | // Expose the module 467 | module.exports = this.Miso; 468 | -------------------------------------------------------------------------------- /dist/node/miso.storyboard.deps.0.1.0.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | _.mixin(require("underscore.deferred")); 3 | 4 | /** 5 | * Miso.Storyboard - v0.1.0 - 5/10/2013 6 | * http://github.com/misoproject/storyboard 7 | * Copyright (c) 2013 Alex Graul, Irene Ros, Rich Harris; 8 | * Dual Licensed: MIT, GPL 9 | * https://github.com/misoproject/storyboard/blob/master/LICENSE-MIT 10 | * https://github.com/misoproject/storyboard/blob/master/LICENSE-GPL 11 | *//* global _ */ 12 | (function(global, _) { 13 | 14 | var Miso = global.Miso = (global.Miso || {}); 15 | 16 | /** 17 | * Miso Events is a small set of methods that can be mixed into any object 18 | * to make it evented. It allows one to then subscribe to specific object events, 19 | * to publish events, unsubscribe and subscribeOnce. 20 | */ 21 | Miso.Events = { 22 | 23 | 24 | /** 25 | * Triggers a specific event and passes any additional arguments 26 | * to the callbacks subscribed to that event. 27 | * Params: 28 | * name - the name of the event to trigger 29 | * .* - any additional arguments to pass to callbacks. 30 | */ 31 | publish : function(name) { 32 | var args = _.toArray(arguments); 33 | args.shift(); 34 | 35 | if (this._events && this._events[name]) { 36 | _.each(this._events[name], function(subscription) { 37 | subscription.callback.apply(subscription.context || this, args); 38 | }, this); 39 | } 40 | return this; 41 | }, 42 | 43 | /** 44 | * Allows subscribing on an evented object to a specific name. 45 | * Provide a callback to trigger. 46 | * Params: 47 | * name - event to subscribe to 48 | * callback - callback to trigger 49 | * options - optional arguments 50 | * priority - allows rearranging of existing callbacks based on priority 51 | * context - allows attaching diff context to callback 52 | * token - allows callback identification by token. 53 | */ 54 | subscribe : function(name, callback, options) { 55 | options = options || {}; 56 | this._events = this._events || {}; 57 | this._events[name] = this._events[name] || []; 58 | 59 | var subscription = { 60 | callback : callback, 61 | priority : options.priority || 0, 62 | token : options.token || _.uniqueId("t"), 63 | context : options.context || this 64 | }; 65 | var position; 66 | _.each(this._events[name], function(event, index) { 67 | if (!_.isUndefined(position)) { return; } 68 | if (event.priority <= subscription.priority) { 69 | position = index; 70 | } 71 | }); 72 | 73 | this._events[name].splice(position, 0, subscription); 74 | return subscription.token; 75 | }, 76 | 77 | /** 78 | * Allows subscribing to an event once. When the event is triggered 79 | * this subscription will be removed. 80 | * Params: 81 | * name - name of event 82 | * callback - The callback to trigger 83 | */ 84 | subscribeOnce : function(name, callback) { 85 | this._events = this._events || {}; 86 | var token = _.uniqueId("t"); 87 | return this.subscribe(name, function() { 88 | this.unsubscribe(name, { token : token }); 89 | callback.apply(this, arguments); 90 | }, this, token); 91 | }, 92 | 93 | /** 94 | * Allows unsubscribing from a specific event 95 | * Params: 96 | * name - event to unsubscribe from 97 | * identifier - callback to remove OR token. 98 | */ 99 | unsubscribe : function(name, identifier) { 100 | 101 | if (_.isUndefined(this._events[name])) { return this; } 102 | 103 | if (_.isFunction(identifier)) { 104 | this._events[name] = _.reject(this._events[name], function(b) { 105 | return b.callback === identifier; 106 | }); 107 | 108 | } else if ( _.isString(identifier)) { 109 | this._events[name] = _.reject(this._events[name], function(b) { 110 | return b.token === identifier; 111 | }); 112 | 113 | } else { 114 | this._events[name] = []; 115 | } 116 | return this; 117 | } 118 | 119 | }; 120 | 121 | }(this, _)); 122 | 123 | /* global _ */ 124 | 125 | (function(global, _) { 126 | 127 | var Miso = global.Miso = (global.Miso || {}); 128 | 129 | /** 130 | * Creates a new storyboard. 131 | * Params: 132 | * options - various arguments 133 | * context - optional. Set a different context for the storyboard. 134 | * by default it's the scene that is being executed. 135 | * 136 | */ 137 | var Storyboard = Miso.Storyboard = function(options) { 138 | 139 | options = options || {}; 140 | 141 | // save all options so we can clone this later... 142 | this._originalOptions = options; 143 | 144 | // Set up the context for this storyboard. This will be 145 | // available as "this" inside the transition functions. 146 | this._context = options.context || this; 147 | 148 | // Assign custom id to the storyboard. 149 | this._id = _.uniqueId("scene"); 150 | 151 | // If there are scenes defined, initialize them. 152 | if (options.scenes) { 153 | 154 | // if the scenes are actually just set to a function, change them 155 | // to an enter property 156 | _.each(options.scenes, function(scene, name) { 157 | if (typeof scene === "function") { 158 | options.scenes[name] = { 159 | enter : scene 160 | }; 161 | } 162 | }); 163 | 164 | // make sure enter/exit are defined as passthroughs if not present. 165 | _.each(Storyboard.HANDLERS, function(action) { 166 | options.scenes[action] = options.scenes[action] || function() { return true; }; 167 | }); 168 | 169 | // Convert the scenes to actually nested storyboards. A "scene" 170 | // is really just a storyboard of one action with no child scenes. 171 | this._buildScenes(options.scenes); 172 | 173 | // Save the initial scene that we will start from. When .start is called 174 | // on the storyboard, this is the scene we transition to. 175 | this._initial = options.initial; 176 | 177 | // Transition function given that there are child scenes. 178 | this.to = children_to; 179 | 180 | } else { 181 | 182 | // This is a terminal storyboad in that it doesn't actually have any child 183 | // scenes, just its own enter and exit functions. 184 | 185 | this.handlers = {}; 186 | 187 | _.each(Storyboard.HANDLERS, function(action) { 188 | 189 | // save the enter and exit functions and if they don't exist, define them. 190 | options[action] = options[action] || function() { return true; }; 191 | 192 | // wrap functions so they can declare themselves as optionally 193 | // asynchronous without having to worry about deferred management. 194 | this.handlers[action] = wrap(options[action], action); 195 | 196 | }, this); 197 | 198 | // Transition function given that this is a terminal storyboard. 199 | this.to = leaf_to; 200 | } 201 | 202 | 203 | // Iterate over all the properties defiend in the options and as long as they 204 | // are not on a black list, save them on the actual scene. This allows us to define 205 | // helper methods that are not going to be wrapped (and thus instrumented with 206 | // any deferred and async behavior.) 207 | _.each(options, function(prop, name) { 208 | 209 | if (_.indexOf(Storyboard.BLACKLIST, name) !== -1) { 210 | return; 211 | } 212 | 213 | if (_.isFunction(prop)) { 214 | this[name] = (function(contextOwner) { 215 | return function() { 216 | prop.apply(contextOwner._context || contextOwner, arguments); 217 | }; 218 | }(this)); 219 | } else { 220 | this[name] = prop; 221 | } 222 | 223 | }, this); 224 | 225 | }; 226 | 227 | Storyboard.HANDLERS = ["enter","exit"]; 228 | Storyboard.BLACKLIST = ["_id", "initial","scenes","enter","exit","context","_current"]; 229 | 230 | _.extend(Storyboard.prototype, Miso.Events, { 231 | 232 | /** 233 | * Allows for cloning of a storyboard 234 | * Returns: 235 | * s - a new Miso.Storyboard 236 | */ 237 | clone : function() { 238 | 239 | // clone nested storyboard 240 | if (this.scenes) { 241 | _.each(this._originalOptions.scenes, function(scene, name) { 242 | if (scene instanceof Miso.Storyboard) { 243 | this._originalOptions.scenes[name] = scene.clone(); 244 | } 245 | }, this); 246 | } 247 | 248 | return new Miso.Storyboard(this._originalOptions); 249 | }, 250 | 251 | /** 252 | * Attach a new scene to an existing storyboard. 253 | * Params: 254 | * name - The name of the scene 255 | * parent - The storyboard to attach this current scene to. 256 | */ 257 | attach : function(name, parent) { 258 | 259 | this.name = name; 260 | this.parent = parent; 261 | 262 | // if the parent has a custom context the child should inherit it 263 | if (parent._context && (parent._context._id !== parent._id)) { 264 | 265 | this._context = parent._context; 266 | if (this.scenes) { 267 | _.each(this.scenes , function(scene) { 268 | scene.attach(scene.name, this); 269 | }, this); 270 | } 271 | } 272 | return this; 273 | }, 274 | 275 | /** 276 | * Instruct a storyboard to kick off its initial scene. 277 | * This returns a deferred object just like all the .to calls. 278 | * If the initial scene is asynchronous, you will need to define a .then 279 | * callback to wait on the start scene to end its enter transition. 280 | */ 281 | start : function() { 282 | // if we've already started just return a happily resoved deferred 283 | if (typeof this._current !== "undefined") { 284 | return _.Deferred().resolve(); 285 | } else { 286 | return this.to(this._initial); 287 | } 288 | }, 289 | 290 | /** 291 | * Cancels a transition in action. This doesn't actually kill the function 292 | * that is currently in play! It does reject the deferred one was awaiting 293 | * from that transition. 294 | */ 295 | cancelTransition : function() { 296 | this._complete.reject(); 297 | this._transitioning = false; 298 | }, 299 | 300 | /** 301 | * Returns the current scene. 302 | * Returns: 303 | * scene - current scene name, or null. 304 | */ 305 | scene : function() { 306 | return this._current ? this._current.name : null; 307 | }, 308 | 309 | /** 310 | * Checks if the current scene is of a specific name. 311 | * Params: 312 | * scene - scene to check as to whether it is the current scene 313 | * Returns: 314 | * true if it is, false otherwise. 315 | */ 316 | is : function( scene ) { 317 | return (scene === this._current.name); 318 | }, 319 | 320 | /** 321 | * Returns true if storyboard is in the middle of a transition. 322 | */ 323 | inTransition : function() { 324 | return (this._transitioning === true); 325 | }, 326 | 327 | /** 328 | * Allows the changing of context. This will alter what "this" 329 | * will be set to inside the transition methods. 330 | */ 331 | setContext : function(context) { 332 | this._context = context; 333 | if (this.scenes) { 334 | _.each(this.scenes, function(scene) { 335 | scene.setContext(context); 336 | }); 337 | } 338 | }, 339 | 340 | _buildScenes : function( scenes ) { 341 | this.scenes = {}; 342 | _.each(scenes, function(scene, name) { 343 | this.scenes[name] = scene instanceof Miso.Storyboard ? scene : new Miso.Storyboard(scene); 344 | this.scenes[name].attach(name, this); 345 | }, this); 346 | } 347 | }); 348 | 349 | // Used as the to function to scenes which do not have children 350 | // These scenes only have their own enter and exit. 351 | function leaf_to( sceneName, argsArr, deferred ) { 352 | 353 | this._transitioning = true; 354 | var complete = this._complete = deferred || _.Deferred(), 355 | args = argsArr ? argsArr : [], 356 | handlerComplete = _.Deferred() 357 | .done(_.bind(function() { 358 | this._transitioning = false; 359 | this._current = sceneName; 360 | complete.resolve(); 361 | }, this)) 362 | .fail(_.bind(function() { 363 | this._transitioning = false; 364 | complete.reject(); 365 | }, this)); 366 | 367 | this.handlers[sceneName].call(this._context, args, handlerComplete); 368 | 369 | return complete.promise(); 370 | } 371 | 372 | // Used as the function to scenes that do have children. 373 | function children_to( sceneName, argsArr, deferred ) { 374 | var toScene = this.scenes[sceneName], 375 | fromScene = this._current, 376 | args = argsArr ? argsArr : [], 377 | complete = this._complete = deferred || _.Deferred(), 378 | exitComplete = _.Deferred(), 379 | enterComplete = _.Deferred(), 380 | publish = _.bind(function(name, isExit) { 381 | var sceneName = isExit ? fromScene : toScene; 382 | sceneName = sceneName ? sceneName.name : ""; 383 | 384 | this.publish(name, fromScene, toScene); 385 | if (name !== "start" || name !== "end") { 386 | this.publish(sceneName + ":" + name); 387 | } 388 | 389 | }, this), 390 | bailout = _.bind(function() { 391 | this._transitioning = false; 392 | this._current = fromScene; 393 | publish("fail"); 394 | complete.reject(); 395 | }, this), 396 | success = _.bind(function() { 397 | publish("enter"); 398 | this._transitioning = false; 399 | this._current = toScene; 400 | publish("end"); 401 | complete.resolve(); 402 | }, this); 403 | 404 | 405 | if (!toScene) { 406 | throw "Scene \"" + sceneName + "\" not found!"; 407 | } 408 | 409 | // we in the middle of a transition? 410 | if (this._transitioning) { 411 | return complete.reject(); 412 | } 413 | 414 | publish("start"); 415 | 416 | this._transitioning = true; 417 | 418 | if (fromScene) { 419 | 420 | // we are coming from a scene, so transition out of it. 421 | fromScene.to("exit", args, exitComplete); 422 | exitComplete.done(function() { 423 | publish("exit", true); 424 | }); 425 | 426 | } else { 427 | exitComplete.resolve(); 428 | } 429 | 430 | // when we're done exiting, enter the next set 431 | _.when(exitComplete).then(function() { 432 | 433 | toScene.to(toScene._initial || "enter", args, enterComplete); 434 | 435 | }).fail(bailout); 436 | 437 | enterComplete 438 | .then(success) 439 | .fail(bailout); 440 | 441 | return complete.promise(); 442 | } 443 | 444 | function wrap(func, name) { 445 | 446 | //don't wrap non-functions 447 | if ( !_.isFunction(func)) { return func; } 448 | //don't wrap private functions 449 | if ( /^_/.test(name) ) { return func; } 450 | //don't wrap wrapped functions 451 | if (func.__wrapped) { return func; } 452 | 453 | var wrappedFunc = function(args, deferred) { 454 | var async = false, 455 | result; 456 | 457 | deferred = deferred || _.Deferred(); 458 | 459 | this.async = function() { 460 | async = true; 461 | return function(pass) { 462 | return (pass !== false) ? deferred.resolve() : deferred.reject(); 463 | }; 464 | }; 465 | 466 | result = func.apply(this, args); 467 | this.async = undefined; 468 | if (!async) { 469 | return (result !== false) ? deferred.resolve() : deferred.reject(); 470 | } 471 | return deferred.promise(); 472 | }; 473 | 474 | wrappedFunc.__wrapped = true; 475 | return wrappedFunc; 476 | } 477 | 478 | }(this, _)); 479 | 480 | 481 | // Expose the module 482 | module.exports = this.Miso; 483 | -------------------------------------------------------------------------------- /libs/underscore.deferred.js: -------------------------------------------------------------------------------- 1 | (function(root){ 2 | 3 | // Let's borrow a couple of things from Underscore that we'll need 4 | 5 | // _.each 6 | var breaker = {}, 7 | AP = Array.prototype, 8 | OP = Object.prototype, 9 | 10 | hasOwn = OP.hasOwnProperty, 11 | toString = OP.toString, 12 | forEach = AP.forEach, 13 | indexOf = AP.indexOf, 14 | slice = AP.slice; 15 | 16 | var _each = function( obj, iterator, context ) { 17 | var key, i, l; 18 | 19 | if ( !obj ) { 20 | return; 21 | } 22 | if ( forEach && obj.forEach === forEach ) { 23 | obj.forEach( iterator, context ); 24 | } else if ( obj.length === +obj.length ) { 25 | for ( i = 0, l = obj.length; i < l; i++ ) { 26 | if ( i in obj && iterator.call( context, obj[i], i, obj ) === breaker ) { 27 | return; 28 | } 29 | } 30 | } else { 31 | for ( key in obj ) { 32 | if ( hasOwn.call( obj, key ) ) { 33 | if ( iterator.call( context, obj[key], key, obj) === breaker ) { 34 | return; 35 | } 36 | } 37 | } 38 | } 39 | }; 40 | 41 | // _.isFunction 42 | var _isFunction = function( obj ) { 43 | return !!(obj && obj.constructor && obj.call && obj.apply); 44 | }; 45 | 46 | // _.extend 47 | var _extend = function( obj ) { 48 | 49 | _each( slice.call( arguments, 1), function( source ) { 50 | var prop; 51 | 52 | for ( prop in source ) { 53 | if ( source[prop] !== void 0 ) { 54 | obj[ prop ] = source[ prop ]; 55 | } 56 | } 57 | }); 58 | return obj; 59 | }; 60 | 61 | // $.inArray 62 | var _inArray = function( elem, arr, i ) { 63 | var len; 64 | 65 | if ( arr ) { 66 | if ( indexOf ) { 67 | return indexOf.call( arr, elem, i ); 68 | } 69 | 70 | len = arr.length; 71 | i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0; 72 | 73 | for ( ; i < len; i++ ) { 74 | // Skip accessing in sparse arrays 75 | if ( i in arr && arr[ i ] === elem ) { 76 | return i; 77 | } 78 | } 79 | } 80 | 81 | return -1; 82 | }; 83 | 84 | // And some jQuery specific helpers 85 | 86 | var class2type = {}; 87 | 88 | // Populate the class2type map 89 | _each("Boolean Number String Function Array Date RegExp Object".split(" "), function(name, i) { 90 | class2type[ "[object " + name + "]" ] = name.toLowerCase(); 91 | }); 92 | 93 | var _type = function( obj ) { 94 | return obj == null ? 95 | String( obj ) : 96 | class2type[ toString.call(obj) ] || "object"; 97 | }; 98 | 99 | // Now start the jQuery-cum-Underscore implementation. Some very 100 | // minor changes to the jQuery source to get this working. 101 | 102 | // Internal Deferred namespace 103 | var _d = {}; 104 | // String to Object options format cache 105 | var optionsCache = {}; 106 | 107 | // Convert String-formatted options into Object-formatted ones and store in cache 108 | function createOptions( options ) { 109 | var object = optionsCache[ options ] = {}; 110 | _each( options.split( /\s+/ ), function( flag ) { 111 | object[ flag ] = true; 112 | }); 113 | return object; 114 | } 115 | 116 | _d.Callbacks = function( options ) { 117 | 118 | // Convert options from String-formatted to Object-formatted if needed 119 | // (we check in cache first) 120 | options = typeof options === "string" ? 121 | ( optionsCache[ options ] || createOptions( options ) ) : 122 | _extend( {}, options ); 123 | 124 | var // Last fire value (for non-forgettable lists) 125 | memory, 126 | // Flag to know if list was already fired 127 | fired, 128 | // Flag to know if list is currently firing 129 | firing, 130 | // First callback to fire (used internally by add and fireWith) 131 | firingStart, 132 | // End of the loop when firing 133 | firingLength, 134 | // Index of currently firing callback (modified by remove if needed) 135 | firingIndex, 136 | // Actual callback list 137 | list = [], 138 | // Stack of fire calls for repeatable lists 139 | stack = !options.once && [], 140 | // Fire callbacks 141 | fire = function( data ) { 142 | memory = options.memory && data; 143 | fired = true; 144 | firingIndex = firingStart || 0; 145 | firingStart = 0; 146 | firingLength = list.length; 147 | firing = true; 148 | for ( ; list && firingIndex < firingLength; firingIndex++ ) { 149 | if ( list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ) === false && options.stopOnFalse ) { 150 | memory = false; // To prevent further calls using add 151 | break; 152 | } 153 | } 154 | firing = false; 155 | if ( list ) { 156 | if ( stack ) { 157 | if ( stack.length ) { 158 | fire( stack.shift() ); 159 | } 160 | } else if ( memory ) { 161 | list = []; 162 | } else { 163 | self.disable(); 164 | } 165 | } 166 | }, 167 | // Actual Callbacks object 168 | self = { 169 | // Add a callback or a collection of callbacks to the list 170 | add: function() { 171 | if ( list ) { 172 | // First, we save the current length 173 | var start = list.length; 174 | (function add( args ) { 175 | _each( args, function( arg ) { 176 | var type = _type( arg ); 177 | if ( type === "function" && ( !options.unique || !self.has( arg ) ) ) { 178 | list.push( arg ); 179 | } else if ( arg && arg.length && type !== "string" ) { 180 | // Inspect recursively 181 | add( arg ); 182 | } 183 | }); 184 | })( arguments ); 185 | // Do we need to add the callbacks to the 186 | // current firing batch? 187 | if ( firing ) { 188 | firingLength = list.length; 189 | // With memory, if we're not firing then 190 | // we should call right away 191 | } else if ( memory ) { 192 | firingStart = start; 193 | fire( memory ); 194 | } 195 | } 196 | return this; 197 | }, 198 | // Remove a callback from the list 199 | remove: function() { 200 | if ( list ) { 201 | _each( arguments, function( arg ) { 202 | var index; 203 | while( ( index = _inArray( arg, list, index ) ) > -1 ) { 204 | list.splice( index, 1 ); 205 | // Handle firing indexes 206 | if ( firing ) { 207 | if ( index <= firingLength ) { 208 | firingLength--; 209 | } 210 | if ( index <= firingIndex ) { 211 | firingIndex--; 212 | } 213 | } 214 | } 215 | }); 216 | } 217 | return this; 218 | }, 219 | // Control if a given callback is in the list 220 | has: function( fn ) { 221 | return _inArray( fn, list ) > -1; 222 | }, 223 | // Remove all callbacks from the list 224 | empty: function() { 225 | list = []; 226 | return this; 227 | }, 228 | // Have the list do nothing anymore 229 | disable: function() { 230 | list = stack = memory = undefined; 231 | return this; 232 | }, 233 | // Is it disabled? 234 | disabled: function() { 235 | return !list; 236 | }, 237 | // Lock the list in its current state 238 | lock: function() { 239 | stack = undefined; 240 | if ( !memory ) { 241 | self.disable(); 242 | } 243 | return this; 244 | }, 245 | // Is it locked? 246 | locked: function() { 247 | return !stack; 248 | }, 249 | // Call all callbacks with the given context and arguments 250 | fireWith: function( context, args ) { 251 | args = args || []; 252 | args = [ context, args.slice ? args.slice() : args ]; 253 | if ( list && ( !fired || stack ) ) { 254 | if ( firing ) { 255 | stack.push( args ); 256 | } else { 257 | fire( args ); 258 | } 259 | } 260 | return this; 261 | }, 262 | // Call all the callbacks with the given arguments 263 | fire: function() { 264 | self.fireWith( this, arguments ); 265 | return this; 266 | }, 267 | // To know if the callbacks have already been called at least once 268 | fired: function() { 269 | return !!fired; 270 | } 271 | }; 272 | 273 | return self; 274 | }; 275 | 276 | _d.Deferred = function( func ) { 277 | 278 | var tuples = [ 279 | // action, add listener, listener list, final state 280 | [ "resolve", "done", _d.Callbacks("once memory"), "resolved" ], 281 | [ "reject", "fail", _d.Callbacks("once memory"), "rejected" ], 282 | [ "notify", "progress", _d.Callbacks("memory") ] 283 | ], 284 | state = "pending", 285 | promise = { 286 | state: function() { 287 | return state; 288 | }, 289 | always: function() { 290 | deferred.done( arguments ).fail( arguments ); 291 | return this; 292 | }, 293 | then: function( /* fnDone, fnFail, fnProgress */ ) { 294 | var fns = arguments; 295 | return _d.Deferred(function( newDefer ) { 296 | _each( tuples, function( tuple, i ) { 297 | var action = tuple[ 0 ], 298 | fn = fns[ i ]; 299 | // deferred[ done | fail | progress ] for forwarding actions to newDefer 300 | deferred[ tuple[1] ]( _isFunction( fn ) ? 301 | function() { 302 | var returned = fn.apply( this, arguments ); 303 | if ( returned && _isFunction( returned.promise ) ) { 304 | returned.promise() 305 | .done( newDefer.resolve ) 306 | .fail( newDefer.reject ) 307 | .progress( newDefer.notify ); 308 | } else { 309 | newDefer[ action + "With" ]( this === deferred ? newDefer : this, [ returned ] ); 310 | } 311 | } : 312 | newDefer[ action ] 313 | ); 314 | }); 315 | fns = null; 316 | }).promise(); 317 | }, 318 | // Get a promise for this deferred 319 | // If obj is provided, the promise aspect is added to the object 320 | promise: function( obj ) { 321 | return typeof obj === "object" ? _extend( obj, promise ) : promise; 322 | } 323 | }, 324 | deferred = {}; 325 | 326 | // Keep pipe for back-compat 327 | promise.pipe = promise.then; 328 | 329 | // Add list-specific methods 330 | _each( tuples, function( tuple, i ) { 331 | var list = tuple[ 2 ], 332 | stateString = tuple[ 3 ]; 333 | 334 | // promise[ done | fail | progress ] = list.add 335 | promise[ tuple[1] ] = list.add; 336 | 337 | // Handle state 338 | if ( stateString ) { 339 | list.add(function() { 340 | // state = [ resolved | rejected ] 341 | state = stateString; 342 | 343 | // [ reject_list | resolve_list ].disable; progress_list.lock 344 | }, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock ); 345 | } 346 | 347 | // deferred[ resolve | reject | notify ] = list.fire 348 | deferred[ tuple[0] ] = list.fire; 349 | deferred[ tuple[0] + "With" ] = list.fireWith; 350 | }); 351 | 352 | // Make the deferred a promise 353 | promise.promise( deferred ); 354 | 355 | // Call given func if any 356 | if ( func ) { 357 | func.call( deferred, deferred ); 358 | } 359 | 360 | // All done! 361 | return deferred; 362 | }; 363 | 364 | // Deferred helper 365 | _d.when = function( subordinate /* , ..., subordinateN */ ) { 366 | var i = 0, 367 | resolveValues = slice.call( arguments ), 368 | length = resolveValues.length, 369 | 370 | // the count of uncompleted subordinates 371 | remaining = length !== 1 || ( subordinate && _isFunction( subordinate.promise ) ) ? length : 0, 372 | 373 | // the master Deferred. If resolveValues consist of only a single Deferred, just use that. 374 | deferred = remaining === 1 ? subordinate : _d.Deferred(), 375 | 376 | // Update function for both resolve and progress values 377 | updateFunc = function( i, contexts, values ) { 378 | return function( value ) { 379 | contexts[ i ] = this; 380 | values[ i ] = arguments.length > 1 ? slice.call( arguments ) : value; 381 | if( values === progressValues ) { 382 | deferred.notifyWith( contexts, values ); 383 | } else if ( !( --remaining ) ) { 384 | deferred.resolveWith( contexts, values ); 385 | } 386 | }; 387 | }, 388 | 389 | progressValues, progressContexts, resolveContexts; 390 | 391 | // add listeners to Deferred subordinates; treat others as resolved 392 | if ( length > 1 ) { 393 | progressValues = new Array( length ); 394 | progressContexts = new Array( length ); 395 | resolveContexts = new Array( length ); 396 | for ( ; i < length; i++ ) { 397 | if ( resolveValues[ i ] && _isFunction( resolveValues[ i ].promise ) ) { 398 | resolveValues[ i ].promise() 399 | .done( updateFunc( i, resolveContexts, resolveValues ) ) 400 | .fail( deferred.reject ) 401 | .progress( updateFunc( i, progressContexts, progressValues ) ); 402 | } else { 403 | --remaining; 404 | } 405 | } 406 | } 407 | 408 | // if we're not waiting on anything, resolve the master 409 | if ( !remaining ) { 410 | deferred.resolveWith( resolveContexts, resolveValues ); 411 | } 412 | 413 | return deferred.promise(); 414 | }; 415 | 416 | // Try exporting as a Common.js Module 417 | if ( typeof module !== "undefined" && module.exports ) { 418 | module.exports = _d; 419 | 420 | // Or mixin to Underscore.js 421 | } else if ( typeof root._ !== "undefined" ) { 422 | root._.mixin(_d); 423 | 424 | // Or assign it to window._ 425 | } else { 426 | root._ = _d; 427 | } 428 | 429 | })(this); 430 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "miso.storyboard", 3 | "title" : "Miso.Storyboard", 4 | "description" : "Storyboard is a state management library that makes orchestrating the flows of your application easy using storyboards.", 5 | "version" : "0.1.0", 6 | "homepage" : "http://github.com/misoproject/storyboard", 7 | "authors" : "Alex Graul, Irene Ros, Rich Harris", 8 | 9 | "bugs" : { 10 | "url" : "http://github.com/misoproject/storyboard/issues" 11 | }, 12 | 13 | "licenses" : [ 14 | { 15 | "type" : "MIT", 16 | "url" : "http://github.com/misoproject/storyboard/blob/master/LICENSE-MIT" 17 | }, 18 | { 19 | "type" : "GPL", 20 | "url" : "http://github.com/misoproject/storyboard/blob/master/LICENSE-GPL" 21 | } 22 | ], 23 | 24 | "main": "dist/node/miso.storyboard.deps.0.0.1", 25 | 26 | "repository": { 27 | "type": "git", 28 | "url": "git@github.com:misoproject/storyboard.git" 29 | }, 30 | 31 | "keywords" : [ "state", "state management" ], 32 | 33 | "engines": { 34 | "node": ">= 0.8.0" 35 | }, 36 | 37 | "devDependencies" : { 38 | "grunt" : "~0.4.0", 39 | "grunt-contrib-jshint": "~0.1.1", 40 | "grunt-contrib-uglify": "~0.1.2", 41 | "grunt-contrib-watch": "~0.3.0", 42 | "grunt-contrib-copy": "~0.4.0", 43 | "grunt-contrib-connect" : "~0.2.0", 44 | "grunt-contrib-concat": "~0.1.3", 45 | "grunt-contrib-clean": "~0.4.0", 46 | "grunt-contrib-qunit": "~0.2.0" 47 | }, 48 | 49 | "dependencies": { 50 | "lodash": "0.9.1", 51 | "underscore.deferred": "0.2.0" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Miso.Storyboard # 2 | 3 | Miso.Storyboard is a control flow library for organising your interactive content as scenes, making it easy to 4 | handle complex transitions and manage state. Each scene can have handlers for how it should act 5 | when it becomes the active scene and when it is no longer the active scene. Storyboards nest, 6 | in that every single scene can actually be its own complex storyboard. 7 | 8 | ```javascript 9 | var app = new Miso.Storyboard({ 10 | initial : 'unloaded', 11 | scenes : { 12 | 'unloaded' : { 13 | exit : function() { 14 | console.log('exiting of unloaded complete'); 15 | } 16 | }, 17 | 'loaded' : { 18 | enter : function() { 19 | console.log('entering the loaded scene'); 20 | } 21 | } 22 | } 23 | }); 24 | 25 | app.start(); 26 | app.to('loaded'); 27 | ``` 28 | 29 | ## Handling Asynchronous transitions ## 30 | 31 | Every scene has two handlers `enter` and `exit`, which are called when the 32 | scene is entered and exited respectively. Both of these handlers can be made to be asynchronous, making 33 | it possible to use them to handle complex animated transions in a relatively simple manner. Handlers 34 | are made asynchronous by calling a `this.async` in the handler. This will return a function when can 35 | be called when the final callback is complete, this is called the resolution function. The `to` function 36 | will in turn then return a deferred that will not resolve (or reject) until all the handlers involved are complete. 37 | 38 | ```javascript 39 | var app = new Miso.Storyboard({ 40 | initial : 'unloaded', 41 | scenes : { 42 | 'unloaded' : { 43 | exit : function() { 44 | var done = this.async(); 45 | $('#loading').fadeOut(500, function() { 46 | done(); 47 | }); 48 | } 49 | }, 50 | 'loaded' : { 51 | enter : function() { 52 | var done = this.async(); 53 | $('#mainscreen').fadeIn(500, function() { 54 | done(); 55 | }); 56 | } 57 | } 58 | } 59 | }); 60 | 61 | app.start(); 62 | var loadingComplete = app.to('loaded'); 63 | loadingComplete.done(function() { 64 | console.log('app now loaded'); 65 | }); 66 | ``` 67 | 68 | 69 | ```javascript 70 | var app = new Miso.Storyboard({ 71 | initial : 'unloaded', 72 | scenes : { 73 | unloaded : {}, 74 | loaded : {} 75 | } 76 | }); 77 | 78 | app.start(); 79 | var complete = app.to('loaded'); 80 | complete.done(function() { 81 | console.log('transition complete!'); 82 | }); 83 | ``` 84 | 85 | It is also possible to pass in your own deferred to the `to` method, along 86 | with an array of arguments that will be passed to the `exit` and `enter` handlers. 87 | 88 | ```javascript 89 | var app = new Miso.Storyboard({ 90 | initial : 'unloaded', 91 | scenes : { 92 | unloaded : {}, 93 | loaded : { 94 | enter : function(id) { 95 | console.log('user ID is '+id); 96 | } 97 | } 98 | } 99 | }); 100 | 101 | var complete = _.Deferred(); 102 | complete.done(function() { 103 | console.log('done!'); 104 | }); 105 | 106 | app.start(); 107 | app.to('overview', [userID], complete); 108 | ``` 109 | 110 | ## Conditional movement between scenes ## 111 | 112 | The `enter` and `exit` handlers can also be used to control whether it's possible to move between scenes. 113 | If a handler returns false, or if it is asynchronous, passes `false` to its resolution function the 114 | transition will be rejected. This can be managed inside handlers or by binding functions to the `fail` 115 | method of the deferred returned by `to`. 116 | 117 | ```javascript 118 | var app = new Miso.Storyboard({ 119 | initial : 'unloaded', 120 | scenes : { 121 | unloaded : { 122 | exit : function() { 123 | var done = this.async(); 124 | 125 | data.remoteFetch({ 126 | error : function() { 127 | // data fetch failed? don't continue transitioning. 128 | done(false) 129 | }, 130 | success : function() { 131 | // data fetch succeeded, continue transitioning. 132 | done(); 133 | } 134 | }); 135 | } 136 | }, 137 | loaded : { 138 | enter : function() { 139 | return false; 140 | } 141 | } 142 | } 143 | }); 144 | 145 | app.start(); 146 | var complete = app.to('loaded'); 147 | complete.fail(function() { 148 | console.log('transition failed!'); 149 | }); 150 | ``` 151 | 152 | ## Organising your code ## 153 | 154 | You can pass additional methods to the definitions scenes in your storyboard 155 | to help structure your code in a more logical manner and break down big functions. 156 | 157 | ```javascript 158 | var app = new Miso.Storyboard({ 159 | initial : 'unloaded', 160 | scenes : { 161 | unloaded : { 162 | loadData : function() { ... }, 163 | displayLoadingScreen : function() { ... }, 164 | enter : function() { 165 | this.displayLoadingScreen(); 166 | this.loadData(); 167 | } 168 | }, 169 | loaded : {} 170 | } 171 | }); 172 | app.start(); 173 | ``` 174 | 175 | ## Nesting Storyboards ## 176 | 177 | It is also possible to nest Miso Storyboards inside each other, making it possible 178 | to control state at each level of your code. For example if you had a slideshow inside 179 | a larger storyboard, it could in turn be its own storyboard, with each slide being a scene, with handlers 180 | defining each move between slides in a custom manner. 181 | 182 | ```javascript 183 | var walkthrough = new Miso.Scene({ 184 | initial : 'one', 185 | scenes : { 186 | one : {}, 187 | two : {}, 188 | three : {} 189 | } 190 | }); 191 | 192 | var app = new Miso.Scene({ 193 | inital 'unloaded', 194 | scenes : { 195 | unloaded : {}, 196 | loaded : walkthrough 197 | } 198 | }); 199 | ``` 200 | 201 | ## Simplified Storyboards ## 202 | 203 | While the main goal of Storyboard is to help you manage your transition enter and exit phases, you can create simplified storyboards that only have `enter` behaviour like so: 204 | 205 | ```javascript 206 | var walkthrough = new Miso.Scene({ 207 | initial : 'one', 208 | scenes : { 209 | one : function() { 210 | // do something 211 | console.log("Doing task one."); 212 | }, 213 | two : function() { 214 | // do something else 215 | console.log("Doing task two."); 216 | } 217 | three : {} 218 | } 219 | }); 220 | 221 | walkthrough.start().then(function() { 222 | walkthrough.to("two"); 223 | }); 224 | 225 | // output: 226 | // Doing task one. 227 | // Doing task two. 228 | ``` 229 | 230 | The above is functionaly equivalent to: 231 | 232 | ```javascript 233 | var walkthrough = new Miso.Scene({ 234 | initial : 'one', 235 | scenes : { 236 | one : { 237 | enter : function() { 238 | // do something 239 | console.log("Doing task one."); 240 | } 241 | }, 242 | two : { 243 | enter : function() { 244 | // do something else 245 | console.log("Doing task two."); 246 | } 247 | }, 248 | three : {} 249 | } 250 | }); 251 | 252 | walkthrough.start().then(function() { 253 | walkthrough.to("two"); 254 | }); 255 | 256 | // output: 257 | // Doing task one. 258 | // Doing task two. 259 | ``` 260 | 261 | ## Contributing ## 262 | 263 | To build Miso.Storyboard you'll need npm, node.js's package management system and grunt 264 | 265 | `npm install miso.storyboard` 266 | 267 | To build Miso.Storyboard, call 268 | 269 | `grunt` 270 | 271 | from the project root. 272 | 273 | -------------------------------------------------------------------------------- /src/events.js: -------------------------------------------------------------------------------- 1 | /* global _ */ 2 | (function(global, _) { 3 | 4 | var Miso = global.Miso = (global.Miso || {}); 5 | 6 | /** 7 | * Miso Events is a small set of methods that can be mixed into any object to 8 | * make it evented. It allows one to then subscribe to specific object 9 | * events, to publish events, unsubscribe and subscribeOnce. 10 | * 11 | * @namespace Events 12 | * @memberof Miso 13 | */ 14 | Miso.Events = 15 | /** @lends Miso.Events */ 16 | { 17 | 18 | /** 19 | * Triggers a specific event and passes any additional arguments 20 | * to the callbacks subscribed to that event. 21 | * 22 | * @param {String} name - the name of the event to trigger 23 | * @param {...mixed} - any additional arguments to pass to callbacks. 24 | */ 25 | publish : function(name) { 26 | var args = _.toArray(arguments); 27 | args.shift(); 28 | 29 | if (this._events && this._events[name]) { 30 | _.each(this._events[name], function(subscription) { 31 | subscription.callback.apply(subscription.context || this, args); 32 | }, this); 33 | } 34 | return this; 35 | }, 36 | 37 | /** 38 | * Allows subscribing on an evented object to a specific name. Provide a 39 | * callback to trigger. 40 | * 41 | * @param {String} name - event to subscribe to 42 | * @param {Function} callback - callback to trigger 43 | * @param {Object} [options] 44 | * @param {Number} [options.priority] - allows rearranging of existing 45 | * callbacks based on priority 46 | * @param {Object} [options.context] - allows attaching diff context to 47 | * callback 48 | * @param {String} [options.token] - allows callback identification by 49 | * token. 50 | */ 51 | subscribe : function(name, callback, options) { 52 | options = options || {}; 53 | this._events = this._events || {}; 54 | this._events[name] = this._events[name] || []; 55 | 56 | var subscription = { 57 | callback : callback, 58 | priority : options.priority || 0, 59 | token : options.token || _.uniqueId("t"), 60 | context : options.context || this 61 | }; 62 | var position; 63 | _.each(this._events[name], function(event, index) { 64 | if (!_.isUndefined(position)) { return; } 65 | if (event.priority <= subscription.priority) { 66 | position = index; 67 | } 68 | }); 69 | 70 | this._events[name].splice(position, 0, subscription); 71 | return subscription.token; 72 | }, 73 | 74 | /** 75 | * Allows subscribing to an event once. When the event is triggered this 76 | * subscription will be removed. 77 | * 78 | * @param {String} name - name of event 79 | * @param {Function} callback - The callback to trigger 80 | */ 81 | subscribeOnce : function(name, callback) { 82 | this._events = this._events || {}; 83 | var token = _.uniqueId("t"); 84 | return this.subscribe(name, function() { 85 | this.unsubscribe(name, { token : token }); 86 | callback.apply(this, arguments); 87 | }, this, token); 88 | }, 89 | 90 | /** 91 | * Allows unsubscribing from a specific event 92 | * 93 | * @param {String} name - event to unsubscribe from 94 | * @param {Function|String} identifier - callback to remove OR token. 95 | */ 96 | unsubscribe : function(name, identifier) { 97 | 98 | if (_.isUndefined(this._events[name])) { return this; } 99 | 100 | if (_.isFunction(identifier)) { 101 | this._events[name] = _.reject(this._events[name], function(b) { 102 | return b.callback === identifier; 103 | }); 104 | 105 | } else if ( _.isString(identifier)) { 106 | this._events[name] = _.reject(this._events[name], function(b) { 107 | return b.token === identifier; 108 | }); 109 | 110 | } else { 111 | this._events[name] = []; 112 | } 113 | return this; 114 | } 115 | 116 | }; 117 | 118 | }(this, _)); 119 | -------------------------------------------------------------------------------- /src/node/compat.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | _.mixin(require("underscore.deferred")); 3 | 4 | <%= misoStoryboard %> 5 | 6 | // Expose the module 7 | module.exports = this.Miso; 8 | -------------------------------------------------------------------------------- /src/require.js: -------------------------------------------------------------------------------- 1 | /* global exports,define,module */ 2 | (function(global) { 3 | 4 | var Miso = global.Miso || {}; 5 | delete window.Miso; 6 | 7 | // CommonJS module is defined 8 | if (typeof exports !== "undefined") { 9 | if (typeof module !== "undefined" && module.exports) { 10 | // Export module 11 | module.exports = Miso; 12 | } 13 | exports.miso = Miso; 14 | 15 | } else if (typeof define === "function" && define.amd) { 16 | // Register as a named module with AMD. 17 | define("miso", [], function() { 18 | return Miso; 19 | }); 20 | } 21 | }(this)); -------------------------------------------------------------------------------- /src/storyboard.js: -------------------------------------------------------------------------------- 1 | /* global _ */ 2 | 3 | (function(global, _) { 4 | 5 | /** 6 | * @namespace 7 | */ 8 | var Miso = global.Miso = (global.Miso || {}); 9 | 10 | /** 11 | * Creates a new storyboard. 12 | * 13 | * @constructor 14 | * @name Storyboard 15 | * @memberof Miso 16 | * 17 | * @param {Object} [options] 18 | * @param {Object} [options.context] - Set a different context for the 19 | * storyboard. by default it's the scene 20 | * that is being executed. 21 | */ 22 | var Storyboard = Miso.Storyboard = function(options) { 23 | 24 | options = options || {}; 25 | 26 | // save all options so we can clone this later... 27 | this._originalOptions = options; 28 | 29 | // Set up the context for this storyboard. This will be 30 | // available as "this" inside the transition functions. 31 | this._context = options.context || this; 32 | 33 | // Assign custom id to the storyboard. 34 | this._id = _.uniqueId("scene"); 35 | 36 | // If there are scenes defined, initialize them. 37 | if (options.scenes) { 38 | 39 | // if the scenes are actually just set to a function, change them 40 | // to an enter property 41 | _.each(options.scenes, function(scene, name) { 42 | if (typeof scene === "function") { 43 | options.scenes[name] = { 44 | enter : scene 45 | }; 46 | } 47 | }); 48 | 49 | // make sure enter/exit are defined as passthroughs if not present. 50 | _.each(Storyboard.HANDLERS, function(action) { 51 | options.scenes[action] = options.scenes[action] || function() { return true; }; 52 | }); 53 | 54 | // Convert the scenes to actually nested storyboards. A "scene" 55 | // is really just a storyboard of one action with no child scenes. 56 | this._buildScenes(options.scenes); 57 | 58 | // Save the initial scene that we will start from. When .start is called 59 | // on the storyboard, this is the scene we transition to. 60 | this._initial = options.initial; 61 | 62 | // Transition function given that there are child scenes. 63 | this.to = children_to; 64 | 65 | } else { 66 | 67 | // This is a terminal storyboad in that it doesn't actually have any child 68 | // scenes, just its own enter and exit functions. 69 | 70 | this.handlers = {}; 71 | 72 | _.each(Storyboard.HANDLERS, function(action) { 73 | 74 | // save the enter and exit functions and if they don't exist, define them. 75 | options[action] = options[action] || function() { return true; }; 76 | 77 | // wrap functions so they can declare themselves as optionally 78 | // asynchronous without having to worry about deferred management. 79 | this.handlers[action] = wrap(options[action], action); 80 | 81 | }, this); 82 | 83 | // Transition function given that this is a terminal storyboard. 84 | this.to = leaf_to; 85 | } 86 | 87 | 88 | // Iterate over all the properties defiend in the options and as long as they 89 | // are not on a black list, save them on the actual scene. This allows us to define 90 | // helper methods that are not going to be wrapped (and thus instrumented with 91 | // any deferred and async behavior.) 92 | _.each(options, function(prop, name) { 93 | 94 | if (_.indexOf(Storyboard.BLACKLIST, name) !== -1) { 95 | return; 96 | } 97 | 98 | if (_.isFunction(prop)) { 99 | this[name] = (function(contextOwner) { 100 | return function() { 101 | prop.apply(contextOwner._context || contextOwner, arguments); 102 | }; 103 | }(this)); 104 | } else { 105 | this[name] = prop; 106 | } 107 | 108 | }, this); 109 | 110 | }; 111 | 112 | Storyboard.HANDLERS = ["enter","exit"]; 113 | Storyboard.BLACKLIST = ["_id", "initial","scenes","enter","exit","context","_current"]; 114 | 115 | _.extend(Storyboard.prototype, Miso.Events, 116 | /** 117 | * @lends Miso.Storyboard.prototype 118 | */ 119 | { 120 | 121 | /** 122 | * Allows for cloning of a storyboard 123 | * 124 | * @returns {Miso.Storyboard} 125 | */ 126 | clone : function() { 127 | 128 | // clone nested storyboard 129 | if (this.scenes) { 130 | _.each(this._originalOptions.scenes, function(scene, name) { 131 | if (scene instanceof Miso.Storyboard) { 132 | this._originalOptions.scenes[name] = scene.clone(); 133 | } 134 | }, this); 135 | } 136 | 137 | return new Miso.Storyboard(this._originalOptions); 138 | }, 139 | 140 | /** 141 | * Attach a new scene to an existing storyboard. 142 | * 143 | * @param {String} name - The name of the scene 144 | * @param {Miso.Storyboard} parent - The storyboard to attach this current 145 | * scene to. 146 | */ 147 | attach : function(name, parent) { 148 | 149 | this.name = name; 150 | this.parent = parent; 151 | 152 | // if the parent has a custom context the child should inherit it 153 | if (parent._context && (parent._context._id !== parent._id)) { 154 | 155 | this._context = parent._context; 156 | if (this.scenes) { 157 | _.each(this.scenes , function(scene) { 158 | scene.attach(scene.name, this); 159 | }, this); 160 | } 161 | } 162 | return this; 163 | }, 164 | 165 | /** 166 | * Instruct a storyboard to kick off its initial scene. 167 | * If the initial scene is asynchronous, you will need to define a .then 168 | * callback to wait on the start scene to end its enter transition. 169 | * 170 | * @returns {Deferred} 171 | */ 172 | start : function() { 173 | // if we've already started just return a happily resoved deferred 174 | if (typeof this._current !== "undefined") { 175 | return _.Deferred().resolve(); 176 | } else { 177 | return this.to(this._initial); 178 | } 179 | }, 180 | 181 | /** 182 | * Cancels a transition in action. This doesn't actually kill the function 183 | * that is currently in play! It does reject the deferred one was awaiting 184 | * from that transition. 185 | */ 186 | cancelTransition : function() { 187 | this._complete.reject(); 188 | this._transitioning = false; 189 | }, 190 | 191 | /** 192 | * Returns the current scene. 193 | * 194 | * @returns {String|null} current scene name 195 | */ 196 | scene : function() { 197 | return this._current ? this._current.name : null; 198 | }, 199 | 200 | /** 201 | * Checks if the current scene is of a specific name. 202 | * 203 | * @param {String} scene - scene to check as to whether it is the current 204 | * scene 205 | * 206 | * @returns {Boolean} true if it is, false otherwise. 207 | */ 208 | is : function( scene ) { 209 | return (scene === this._current.name); 210 | }, 211 | 212 | /** 213 | * @returns {Boolean} true if storyboard is in the middle of a transition. 214 | */ 215 | inTransition : function() { 216 | return (this._transitioning === true); 217 | }, 218 | 219 | /** 220 | * Allows the changing of context. This will alter what "this" will be set 221 | * to inside the transition methods. 222 | */ 223 | setContext : function(context) { 224 | this._context = context; 225 | if (this.scenes) { 226 | _.each(this.scenes, function(scene) { 227 | scene.setContext(context); 228 | }); 229 | } 230 | }, 231 | 232 | _buildScenes : function( scenes ) { 233 | this.scenes = {}; 234 | _.each(scenes, function(scene, name) { 235 | this.scenes[name] = scene instanceof Miso.Storyboard ? scene : new Miso.Storyboard(scene); 236 | this.scenes[name].attach(name, this); 237 | }, this); 238 | } 239 | }); 240 | 241 | // Used as the to function to scenes which do not have children 242 | // These scenes only have their own enter and exit. 243 | function leaf_to( sceneName, argsArr, deferred ) { 244 | 245 | this._transitioning = true; 246 | var complete = this._complete = deferred || _.Deferred(), 247 | args = argsArr ? argsArr : [], 248 | handlerComplete = _.Deferred() 249 | .done(_.bind(function() { 250 | this._transitioning = false; 251 | this._current = sceneName; 252 | complete.resolve(); 253 | }, this)) 254 | .fail(_.bind(function() { 255 | this._transitioning = false; 256 | complete.reject(); 257 | }, this)); 258 | 259 | this.handlers[sceneName].call(this._context, args, handlerComplete); 260 | 261 | return complete.promise(); 262 | } 263 | 264 | // Used as the function to scenes that do have children. 265 | function children_to( sceneName, argsArr, deferred ) { 266 | var toScene = this.scenes[sceneName], 267 | fromScene = this._current, 268 | args = argsArr ? argsArr : [], 269 | complete = this._complete = deferred || _.Deferred(), 270 | exitComplete = _.Deferred(), 271 | enterComplete = _.Deferred(), 272 | publish = _.bind(function(name, isExit) { 273 | var sceneName = isExit ? fromScene : toScene; 274 | sceneName = sceneName ? sceneName.name : ""; 275 | 276 | this.publish(name, fromScene, toScene); 277 | if (name !== "start" || name !== "end") { 278 | this.publish(sceneName + ":" + name); 279 | } 280 | 281 | }, this), 282 | bailout = _.bind(function() { 283 | this._transitioning = false; 284 | this._current = fromScene; 285 | publish("fail"); 286 | complete.reject(); 287 | }, this), 288 | success = _.bind(function() { 289 | publish("enter"); 290 | this._transitioning = false; 291 | this._current = toScene; 292 | publish("end"); 293 | complete.resolve(); 294 | }, this); 295 | 296 | 297 | if (!toScene) { 298 | throw "Scene \"" + sceneName + "\" not found!"; 299 | } 300 | 301 | // we in the middle of a transition? 302 | if (this._transitioning) { 303 | return complete.reject(); 304 | } 305 | 306 | publish("start"); 307 | 308 | this._transitioning = true; 309 | 310 | if (fromScene) { 311 | 312 | // we are coming from a scene, so transition out of it. 313 | fromScene.to("exit", args, exitComplete); 314 | exitComplete.done(function() { 315 | publish("exit", true); 316 | }); 317 | 318 | } else { 319 | exitComplete.resolve(); 320 | } 321 | 322 | // when we're done exiting, enter the next set 323 | _.when(exitComplete).then(function() { 324 | 325 | toScene.to(toScene._initial || "enter", args, enterComplete); 326 | 327 | }).fail(bailout); 328 | 329 | enterComplete 330 | .then(success) 331 | .fail(bailout); 332 | 333 | return complete.promise(); 334 | } 335 | 336 | function wrap(func, name) { 337 | 338 | //don't wrap non-functions 339 | if ( !_.isFunction(func)) { return func; } 340 | //don't wrap private functions 341 | if ( /^_/.test(name) ) { return func; } 342 | //don't wrap wrapped functions 343 | if (func.__wrapped) { return func; } 344 | 345 | var wrappedFunc = function(args, deferred) { 346 | var async = false, 347 | result; 348 | 349 | deferred = deferred || _.Deferred(); 350 | 351 | this.async = function() { 352 | async = true; 353 | return function(pass) { 354 | return (pass !== false) ? deferred.resolve() : deferred.reject(); 355 | }; 356 | }; 357 | 358 | result = func.apply(this, args); 359 | this.async = undefined; 360 | if (!async) { 361 | return (result !== false) ? deferred.resolve() : deferred.reject(); 362 | } 363 | return deferred.promise(); 364 | }; 365 | 366 | wrappedFunc.__wrapped = true; 367 | return wrappedFunc; 368 | } 369 | 370 | }(this, _)); 371 | -------------------------------------------------------------------------------- /tasks/node.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function(grunt) { 3 | 4 | // TODO: ditch this when grunt v0.4 is released 5 | grunt.util = grunt.util || grunt.utils; 6 | 7 | var _ = grunt.util._; 8 | // Shorthand Grunt functions 9 | var log = grunt.log; 10 | 11 | // Task specific for building Node compatible version 12 | grunt.registerTask('node', function() { 13 | var nodeConfig = grunt.config("node"); 14 | var read = grunt.file.read; 15 | 16 | var output = grunt.template.process(read(nodeConfig.wrapper), { 17 | misoScene: read(grunt.template.process(nodeConfig.misoScene)) 18 | }); 19 | 20 | // Write the contents out 21 | grunt.file.write("dist/node/miso.storyboard." + grunt.template.process(grunt.config("pkg").version) + ".js", output); 22 | }); 23 | 24 | }; -------------------------------------------------------------------------------- /tasks/server.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Grunt Task File 3 | * --------------- 4 | * 5 | * Task: Server 6 | * Description: Serve the web application. 7 | * Dependencies: express 8 | * 9 | */ 10 | module.exports = function(grunt) { 11 | 12 | // TODO: ditch this when grunt v0.4 is released 13 | grunt.util = grunt.util || grunt.utils; 14 | 15 | var _ = grunt.util._; 16 | // Shorthand Grunt functions 17 | var log = grunt.log; 18 | 19 | grunt.registerTask('testserver', 'Start a custom static web server.', function() { 20 | var connect = require('connect'); 21 | var path = require('path'); 22 | var port = grunt.config('testserver.port') || 9292; 23 | var base = path.resolve(grunt.config('testserver.base') || '.'); 24 | 25 | // Start server. 26 | var app = connect() 27 | .use( connect.static(base) ) 28 | .use( connect.directory(base) ) 29 | .listen( port ) 30 | 31 | grunt.log.writeln('Starting test server on port ' + port + '.'); 32 | }); 33 | 34 | grunt.registerTask('server', 'Start a custom static web server.', function() { 35 | var connect = require('connect'); 36 | var path = require('path'); 37 | var done = this.async(); 38 | var port = grunt.config('server.port') || 8000; 39 | var base = path.resolve(grunt.config('server.base') || '.'); 40 | 41 | connect.logger.format('grunt', (':status: :method :url').green); 42 | 43 | // Start server. 44 | var app = connect() 45 | .use( connect.logger('grunt') ) 46 | .use( connect.static(base) ) 47 | .use( connect.directory(base) ) 48 | .listen( port ) 49 | .on('close', done); 50 | grunt.log.writeln('Starting server on port ' + port + '.'); 51 | }); 52 | 53 | }; -------------------------------------------------------------------------------- /test/index.build.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |' + platform + ' | |
---|---|
Test | Ops/sec |