├── .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 | Scene Test Suite 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 |

Scene Test Suite

31 |

32 |

33 |
    34 |
    35 |

    Speed Suite

    36 |
    37 | 38 |
    39 | 42 | 43 | 44 |
    45 | 46 | 47 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Scene Test Suite 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
    32 |

    Scene Test Suite

    33 |

    34 |

    35 |
      36 |
      37 |

      Speed Suite

      38 |
      39 | 40 |
      41 | 44 | 45 | 46 |
      47 | 48 | 49 | -------------------------------------------------------------------------------- /test/support/template.html: -------------------------------------------------------------------------------- 1 | <%= name %> 2 | -------------------------------------------------------------------------------- /test/unit/async.js: -------------------------------------------------------------------------------- 1 | /* global Miso,module,test,equals,ok,stop,start,_ */ 2 | module("Asynchronous tests"); 3 | 4 | test("Asynchronous enter", function() { 5 | var done; 6 | var app = new Miso.Storyboard({ 7 | initial : "unloaded", 8 | scenes : { 9 | unloaded : {}, 10 | loaded : { 11 | enter : function() { 12 | done = this.async(); 13 | } 14 | } 15 | } 16 | }); 17 | app.start().then(function() { 18 | app.to("loaded"); 19 | ok(app.scene("unloaded"), "should still be unloaded during transition"); 20 | ok(app.inTransition(), "should be in transition"); 21 | ok(app.to("loaded").state(),"rejected", "can't start a second transition"); 22 | done(); 23 | ok(app.scene("loaded")); 24 | ok(!app.inTransition(), "should no longer be in transition"); 25 | }); 26 | }); 27 | 28 | test("Cancelling a transition in progress", 4, function() { 29 | var done; 30 | var app = new Miso.Storyboard({ 31 | initial : "unloaded", 32 | scenes : { 33 | unloaded : { 34 | exit : function() { 35 | done = this.async(); 36 | } 37 | }, 38 | loaded : {} 39 | } 40 | }); 41 | 42 | app.start().then(function() { 43 | var promise = app.to("loaded"); 44 | ok(app.inTransition(), "entered transition"); 45 | promise.fail(function() { 46 | ok(true, "transition promise rejected"); 47 | }); 48 | app.cancelTransition(); 49 | ok(!app.inTransition(), "no longer in transition"); 50 | promise = app.to("loaded"); 51 | promise.done(function() { 52 | ok(true, "second attempt succeeds"); 53 | }); 54 | done(true); 55 | }); 56 | }); 57 | 58 | test("async handlers are executed in the correct order", 1, function() { 59 | var order = []; 60 | var app = new Miso.Storyboard({ 61 | initial : "unloaded", 62 | scenes : { 63 | unloaded : { 64 | exit: function() { 65 | var done = this.async(); 66 | setTimeout(function() { 67 | order.push("a"); 68 | done(); 69 | }, 100); 70 | } 71 | }, 72 | loaded : { 73 | enter : function() { 74 | order.push("b"); 75 | } 76 | } 77 | } 78 | }); 79 | 80 | app.start().then(function() { 81 | app.to("loaded"); 82 | stop(); 83 | setTimeout(function() { 84 | start(); 85 | equals(order.join(""), "ab", "handlers fired in the corect order"); 86 | }, 200); 87 | }); 88 | }); 89 | 90 | test("async fail on enter stops transition", 4, function() { 91 | var pass; 92 | var app = new Miso.Storyboard({ 93 | initial : "unloaded", 94 | scenes : { 95 | unloaded : {}, 96 | loaded : { 97 | enter : function() { 98 | pass = this.async(); 99 | } 100 | } 101 | } 102 | }); 103 | 104 | app.start().then(function() { 105 | var promise = app.to("loaded"); 106 | ok(app.inTransition()); 107 | pass(false); 108 | promise.fail(function() { 109 | ok(true); 110 | }); 111 | ok(!app.inTransition()); 112 | equals(app.scene(), "unloaded"); 113 | }); 114 | }); 115 | 116 | test("async fail on exit stops transition", 4, function() { 117 | var pass; 118 | var app = new Miso.Storyboard({ 119 | initial : "unloaded", 120 | scenes : { 121 | unloaded : { 122 | exit : function() { 123 | pass = this.async(); 124 | } 125 | }, 126 | loaded : {} 127 | } 128 | }); 129 | 130 | app.start().then(function() { 131 | var promise = app.to("loaded"); 132 | ok(app.inTransition()); 133 | pass(false); 134 | promise.fail(function() { 135 | ok(true); 136 | }); 137 | ok(!app.inTransition()); 138 | equals(app.scene(), "unloaded"); 139 | }); 140 | }); 141 | 142 | test("passing a custom deferred to to", 1, function() { 143 | var app = new Miso.Storyboard({ 144 | initial : "unloaded", 145 | scenes : { 146 | unloaded : {}, 147 | loaded : {} 148 | } 149 | }); 150 | 151 | app.start().then(function() { 152 | var done = _.Deferred(); 153 | done.done(function() { 154 | ok(true); 155 | }); 156 | app.to("loaded", [], done); 157 | }); 158 | }); 159 | -------------------------------------------------------------------------------- /test/unit/base.js: -------------------------------------------------------------------------------- 1 | /* global Miso,module,test,ok,_ */ 2 | 3 | var app; 4 | module("base", { 5 | setup : function() { 6 | app = new Miso.Storyboard({ 7 | counter : 0, 8 | initial : "a", 9 | scenes : { 10 | a : { 11 | enter : function() { 12 | this.counter = 0; 13 | }, 14 | exit : function() { 15 | this.helper(); 16 | ok(this.counter === 1, "a counter is 1"); 17 | }, 18 | helper : function() { 19 | this.counter++; 20 | this.parent.helper(); 21 | } 22 | }, 23 | 24 | b : { 25 | enter : function() { 26 | this.counter = 0; 27 | }, 28 | exit : function() { 29 | this.helper(); 30 | ok(this.counter === 1, "b counter is 1"); 31 | }, 32 | helper : function() { 33 | this.counter++; 34 | this.parent.helper(); 35 | } 36 | }, 37 | 38 | ending : {} 39 | }, 40 | 41 | helper : function() { 42 | this.counter += 10; 43 | } 44 | 45 | }); 46 | }, 47 | teardown : function() { 48 | app = null; 49 | } 50 | }); 51 | 52 | test("Function only scenes", function() { 53 | var nums = []; 54 | var sb = new Miso.Storyboard({ 55 | initial : "a", 56 | scenes: { 57 | a : function() { 58 | nums.push(1); 59 | }, 60 | b : function() { 61 | nums.push(2); 62 | }, 63 | c : function() { 64 | nums.push(3); 65 | }, 66 | d : { 67 | enter : function() { 68 | nums.push(4); 69 | } 70 | } 71 | } 72 | }); 73 | 74 | sb.start().then(function() { 75 | sb.to("b").then(function() { 76 | sb.to("c").then(function() { 77 | sb.to("d").then(function() { 78 | ok(_.isEqual(nums, [1,2,3,4]), "nums are equal"); 79 | }); 80 | }); 81 | }); 82 | }); 83 | }); 84 | 85 | test("Create storyboard", 3, function() { 86 | app.start().then(function() { 87 | app.to("b").then(function() { 88 | app.to("ending").then(function() { 89 | ok(app.counter === 20, app.counter); 90 | }); 91 | }); 92 | }); 93 | }); 94 | 95 | test("Cloning", 6, function() { 96 | app.start().then(function() { 97 | app.to("b").then(function() { 98 | app.to("ending").then(function() { 99 | ok(app.counter === 20, app.counter); 100 | }); 101 | }); 102 | }); 103 | 104 | var app2 = app.clone(); 105 | // counter now starts at 20! 106 | app2.start().then(function() { 107 | app2.to("b").then(function() { 108 | app2.to("ending").then(function() { 109 | ok(app2.counter === 20, app2.counter); 110 | }); 111 | }); 112 | }); 113 | }); 114 | 115 | test("Cloning deeply", function() { 116 | app = new Miso.Storyboard({ 117 | counter : 0, 118 | initial : "a", 119 | scenes : { 120 | a : new Miso.Storyboard({ 121 | enter : function() { 122 | this.counter = 0; 123 | }, 124 | exit : function() { 125 | this.helper(); 126 | ok(this.counter === 1, "a counter is 1"); 127 | }, 128 | helper : function() { 129 | this.counter++; 130 | this.parent.helper(); 131 | } 132 | }), 133 | b : { 134 | enter : function() { 135 | this.counter = 0; 136 | }, 137 | exit : function() { 138 | this.helper(); 139 | ok(this.counter === 1, "b counter is 1"); 140 | }, 141 | helper : function() { 142 | this.counter++; 143 | this.parent.helper(); 144 | } 145 | }, 146 | ending : {} 147 | }, 148 | helper : function() { 149 | this.counter += 10; 150 | } 151 | }); 152 | 153 | app.start().then(function() { 154 | app.to("b").then(function() { 155 | app.to("ending").then(function() { 156 | ok(app.counter === 20, app.counter); 157 | }); 158 | }); 159 | }); 160 | 161 | var app2 = app.clone(); 162 | // counter now starts at 20! 163 | app2.start().then(function() { 164 | app2.to("b").then(function() { 165 | app2.to("ending").then(function() { 166 | ok(app2.counter === 20, app2.counter); 167 | }); 168 | }); 169 | }); 170 | }); -------------------------------------------------------------------------------- /test/unit/context.js: -------------------------------------------------------------------------------- 1 | /* global Miso,module,test,equals */ 2 | module("Context and arguments"); 3 | 4 | test("extending a scene with additional methods", function() { 5 | var done = false; 6 | var app = new Miso.Storyboard({ 7 | boom : function() { 8 | done = true; 9 | }, 10 | enter : function() { 11 | this.boom(); 12 | } 13 | }); 14 | 15 | app.to("enter"); 16 | equals(done, true); 17 | }); 18 | 19 | test("handlers have access arguments passed to transition", 4, function() { 20 | var app = new Miso.Storyboard({ 21 | initial : "unloaded", 22 | scenes : { 23 | unloaded : { 24 | exit : function(a, b) { 25 | equals(a, 44); 26 | equals(b.power, "full"); 27 | } 28 | }, 29 | loaded : { 30 | enter : function(a, b) { 31 | equals(a, 44); 32 | equals(b.power, "full"); 33 | } 34 | } 35 | } 36 | }); 37 | 38 | app.start().then(function() { 39 | app.to("loaded", [44, { power : "full" }]); 40 | }); 41 | 42 | }); 43 | 44 | test("Applying a context to a simple scene", function() { 45 | var context = { 46 | a : true, 47 | b : 96 48 | }; 49 | 50 | var app = new Miso.Storyboard({ 51 | context : context, 52 | initial : "unloaded", 53 | scenes : { 54 | unloaded : { 55 | enter : function() { 56 | equals(this.a, true); 57 | equals(this.b, 96); 58 | }, 59 | exit : function() { 60 | equals(this.a, true); 61 | equals(this.b, 96); 62 | } 63 | } 64 | } 65 | }); 66 | app.start(); 67 | 68 | }); 69 | 70 | test("Applying a context to a simple scene and then switching it", function() { 71 | stop(); 72 | var context1 = { 73 | a : true, 74 | b : 96 75 | }; 76 | 77 | var context2 = { 78 | a : false, 79 | b : 1 80 | }; 81 | 82 | var app = new Miso.Storyboard({ 83 | context : context1, 84 | initial : "c1", 85 | scenes : { 86 | c1 : { 87 | enter : function() { 88 | equals(this.a, true); 89 | equals(this.b, 96); 90 | }, 91 | exit : function() { 92 | equals(this.a, true); 93 | equals(this.b, 96); 94 | } 95 | }, 96 | c2 : { 97 | enter : function() { 98 | equals(this.a, false); 99 | equals(this.b, 1); 100 | }, 101 | exit : function() { 102 | equals(this.a, false); 103 | equals(this.b, 1); 104 | start(); 105 | } 106 | }, 107 | end : {} 108 | } 109 | }); 110 | 111 | app.subscribe("c1:exit", function() { 112 | app.setContext(context2); 113 | }); 114 | 115 | app.start().then(function() { 116 | app.to("c2").then(function() { 117 | app.to("end"); 118 | }); 119 | }); 120 | 121 | }); 122 | 123 | test("applying a context to nested rigs", 4, function() { 124 | var context = { 125 | a : true, 126 | b : 96 127 | }; 128 | 129 | var app = new Miso.Storyboard({ 130 | context : context, 131 | initial : "unloaded", 132 | scenes : { 133 | unloaded : { 134 | enter : function() { 135 | equals(this.a, true); 136 | equals(this.b, 96); 137 | }, 138 | exit : function() { 139 | equals(this.a, true); 140 | equals(this.b, 96); 141 | } 142 | }, 143 | loaded : {} 144 | } 145 | }); 146 | 147 | app.start().then(function() { 148 | app.to("loaded"); 149 | }); 150 | 151 | }); 152 | 153 | -------------------------------------------------------------------------------- /test/unit/events.js: -------------------------------------------------------------------------------- 1 | /* global Miso,module,test,ok,_,equals */ 2 | module("Basic events"); 3 | 4 | test("subscriptions", 1, function() { 5 | var app = _.extend({}, Miso.Events); 6 | app.subscribe("test", function() { 7 | ok(true); 8 | }); 9 | app.publish("test"); 10 | }); 11 | 12 | test("unsubscription with callback", 2, function() { 13 | var app = _.extend({}, Miso.Events); 14 | function tester() { 15 | ok(true); 16 | } 17 | 18 | app.subscribe("test", tester); 19 | app.subscribe("test", tester); 20 | app.publish("test"); 21 | app.unsubscribe("test", tester); 22 | app.publish("test"); 23 | }); 24 | 25 | test("unsubscription with token", 1, function() { 26 | var app = _.extend({}, Miso.Events); 27 | function tester() { 28 | ok(true); 29 | } 30 | 31 | var token = app.subscribe("test", tester); 32 | app.publish("test"); 33 | app.unsubscribe("test", { token : token }); 34 | app.publish("test"); 35 | }); 36 | 37 | test("subscribeOnce", 1, function() { 38 | var app = _.extend({}, Miso.Events); 39 | function tester() { 40 | ok(true); 41 | } 42 | 43 | app.subscribeOnce("test", tester); 44 | app.publish("test"); 45 | app.publish("test"); 46 | }); 47 | 48 | test("priority", function() { 49 | var app = _.extend({}, Miso.Events); 50 | var output = []; 51 | app.subscribe("test", function() { 52 | output.push("d"); 53 | }, { priority : -10 }); 54 | app.subscribe("test", function() { 55 | output.push("c"); 56 | }, { priority : 0 }); 57 | app.subscribe("test", function() { 58 | output.push("a"); 59 | }, { priority : 100 }); 60 | app.subscribe("test", function() { 61 | output.push("b"); 62 | }, { priority : 10 }); 63 | app.publish("test"); 64 | equals(output.join(""), "abcd"); 65 | }); 66 | -------------------------------------------------------------------------------- /test/unit/nesting.js: -------------------------------------------------------------------------------- 1 | /* global Miso,module,test,equals */ 2 | 3 | module("Building complex scenes"); 4 | 5 | test("scenes names get set when they're attached", function() { 6 | var myStoryboard = new Miso.Storyboard({}); 7 | var app = new Miso.Storyboard({ 8 | initial : "base", 9 | scenes : { base : myStoryboard } 10 | }); 11 | 12 | equals(app.scenes["base"].name, "base"); 13 | }); 14 | 15 | test("predefining scenes", function() { 16 | var order = []; 17 | var sceneA = new Miso.Storyboard({ 18 | enter : function() { 19 | order.push("a"); 20 | }, 21 | exit : function() { 22 | order.push("b"); 23 | } 24 | }); 25 | 26 | var sceneB = new Miso.Storyboard({ 27 | enter : function() { 28 | order.push("c"); 29 | } 30 | }); 31 | 32 | var app = new Miso.Storyboard({ 33 | initial : "unloaded", 34 | scenes : { 35 | unloaded : sceneA, 36 | loaded : sceneB 37 | } 38 | }); 39 | 40 | app.start().then(function() { 41 | app.to("loaded"); 42 | equals(order.join(""), "abc"); 43 | }); 44 | }); 45 | 46 | test("Using as engine as a scene", function() { 47 | var order = []; 48 | var subStoryboard = new Miso.Storyboard({ 49 | scenes : { 50 | enter : { 51 | enter : function() { 52 | order.push("a"); 53 | }, 54 | exit : function() { 55 | order.push("b"); 56 | } 57 | }, 58 | exit : { 59 | enter : function() { 60 | order.push("c"); 61 | } 62 | } 63 | }, 64 | defer : true, 65 | initial : "enter" 66 | }); 67 | 68 | var app = new Miso.Storyboard({ 69 | initial : "unloaded", 70 | scenes : { 71 | unloaded : subStoryboard, 72 | loaded : { 73 | enter : function() { 74 | order.push("d"); 75 | } 76 | } 77 | } 78 | }); 79 | 80 | app.start().then(function() { 81 | app.to("loaded"); 82 | equals(order.join(""), "abcd"); 83 | }); 84 | 85 | }); 86 | 87 | 88 | test("Nesting 3 engines inside each other", function() { 89 | var order = []; 90 | 91 | var inner = new Miso.Storyboard({ 92 | initial : "enter", 93 | scenes : { 94 | enter : { 95 | enter : function() { 96 | order.push("c"); 97 | } 98 | } 99 | }, 100 | defer : true 101 | }); 102 | 103 | var outer = new Miso.Storyboard({ 104 | initial : "enter", 105 | scenes : { 106 | enter : { 107 | enter : function() { 108 | order.push("b"); 109 | } 110 | }, 111 | exit : inner 112 | }, 113 | defer : true 114 | }); 115 | 116 | var app = new Miso.Storyboard({ 117 | initial : "a", 118 | scenes : { 119 | a : { 120 | enter : function() { 121 | order.push("a"); 122 | } 123 | }, 124 | b : outer, 125 | c : {} 126 | } 127 | }); 128 | 129 | app.start().then(function() { 130 | app.to("b"); 131 | app.to("c"); 132 | equals(order.join(""), "abc"); 133 | }); 134 | }); 135 | 136 | test("applying a context to nested rigs", 6, function() { 137 | var context = { 138 | a : true, 139 | b : 96 140 | }; 141 | 142 | var app = new Miso.Storyboard({ 143 | context : context, 144 | initial : "unloaded", 145 | scenes : { 146 | unloaded : { 147 | enter : function() { 148 | equals(this.a, true); 149 | equals(this.b, 96); 150 | }, 151 | exit : function() { 152 | equals(this.a, true); 153 | equals(this.b, 96); 154 | } 155 | }, 156 | 157 | loaded : new Miso.Storyboard({ 158 | initial : "enter", 159 | scenes : { 160 | 161 | enter : { 162 | enter : function() { 163 | equals(this.a, true, "true in nested scene"); 164 | equals(this.b, 96, "true in nested scene"); 165 | } 166 | }, 167 | 168 | exit : {} 169 | 170 | } 171 | }) 172 | } 173 | }); 174 | 175 | app.start().then(function() { 176 | app.to("loaded"); 177 | }); 178 | }); 179 | 180 | test("nesting with defaulted scene definitions on children", function() { 181 | var order = [ 182 | "unloaded:enter", 183 | "loading:files", 184 | "loading:templates", 185 | "something:enter" 186 | ], actualOrder = []; 187 | var loading = new Miso.Storyboard({ 188 | initial : "files", 189 | scenes : { 190 | files : { 191 | enter : function() { 192 | actualOrder.push("loading:files"); 193 | } 194 | }, 195 | templates : { 196 | enter : function() { 197 | actualOrder.push("loading:templates"); 198 | } 199 | } 200 | } 201 | }); 202 | 203 | var app = new Miso.Storyboard({ 204 | initial : "unloaded", 205 | scenes : { 206 | unloaded : { 207 | enter: function() { 208 | actualOrder.push("unloaded:enter"); 209 | } 210 | }, 211 | loaded : loading, 212 | something : { 213 | enter : function() { 214 | actualOrder.push("something:enter"); 215 | } 216 | } 217 | } 218 | }); 219 | 220 | app.start().then(function() { 221 | app.to("loaded").then(function() { 222 | loading.to("templates").then(function() { 223 | app.to("something"); 224 | equals(order.join(""), actualOrder.join("")); 225 | }); 226 | }); 227 | }); 228 | 229 | }); -------------------------------------------------------------------------------- /test/unit/scene_events.js: -------------------------------------------------------------------------------- 1 | /* global Miso,module,test,ok,_ */ 2 | 3 | module("Storyboard Event Integration"); 4 | 5 | test("Basic transition events", function() { 6 | 7 | var events = [ 8 | "start", 9 | "x:unloaded:enter", 10 | "enter", 11 | "unloaded:enter", 12 | "end", 13 | "start", 14 | "x:unloaded:exit", 15 | "exit", 16 | "unloaded:exit", 17 | "x:loaded:enter", 18 | "enter", 19 | "loaded:enter", 20 | "end", 21 | "start", 22 | "x:loaded:exit", 23 | "exit", 24 | "loaded:exit", 25 | "x:ending:enter", 26 | "enter", 27 | "ending:enter", 28 | "end" 29 | ], actualEvents = []; 30 | 31 | var app = new Miso.Storyboard({ 32 | initial : "unloaded", 33 | scenes : { 34 | unloaded : { 35 | enter : function() { 36 | actualEvents.push("x:unloaded:enter"); 37 | }, 38 | exit : function() { 39 | actualEvents.push("x:unloaded:exit"); 40 | } 41 | }, 42 | loaded : { 43 | enter : function() { 44 | actualEvents.push("x:loaded:enter"); 45 | }, 46 | exit : function() { 47 | actualEvents.push("x:loaded:exit"); 48 | } 49 | }, 50 | ending : { 51 | enter : function() { 52 | actualEvents.push("x:ending:enter"); 53 | } 54 | } 55 | } 56 | }); 57 | 58 | var eventList = [ 59 | "start","exit","enter","end", 60 | "unloaded:enter", "unloaded:exit", 61 | "loaded:enter", "loaded:exit", 62 | "ending:enter", "ending:exit" 63 | ]; 64 | _.each(eventList, function(event) { 65 | app.subscribe(event, function() { 66 | actualEvents.push(event); 67 | }); 68 | }); 69 | 70 | app.start().then(function() { 71 | app.to("loaded").then(function() { 72 | app.to("ending").then(function() { 73 | ok(_.isEqual(actualEvents, events), actualEvents); 74 | }); 75 | }); 76 | }); 77 | 78 | }); 79 | -------------------------------------------------------------------------------- /test/unit/sync.js: -------------------------------------------------------------------------------- 1 | /* global Miso,module,test,ok,equals */ 2 | module("Synchronous Tests"); 3 | 4 | test("Changing synchronous states", function() { 5 | var app = new Miso.Storyboard({ 6 | initial : "unloaded", 7 | scenes : { 8 | unloaded : {}, 9 | loaded : {} 10 | } 11 | }); 12 | 13 | app.start().then(function() { 14 | equals(app.scene(), "unloaded", "initial state is unloaded"); 15 | var done = app.to("loaded"); 16 | ok(app.is("loaded"), "changed state is loaded"); 17 | equals(done.state(), "resolved"); 18 | }); 19 | 20 | }); 21 | 22 | test("Changing between multiple synchronous states", function() { 23 | var app = new Miso.Storyboard({ 24 | initial : "unloaded", 25 | scenes : { 26 | unloaded : {}, 27 | loaded : {}, 28 | drilldown : {} 29 | } 30 | }); 31 | 32 | app.start().then(function() { 33 | ok(app.is("unloaded"), "initial state is unloaded"); 34 | app.to("loaded"); 35 | ok(app.is("loaded"), "state is loaded"); 36 | app.to("drilldown"); 37 | ok(app.is("drilldown"), "state is drill"); 38 | app.to("loaded"); 39 | ok(app.is("loaded"), "state is loaded"); 40 | }); 41 | }); 42 | 43 | test("returning false on enter stops transition", 2, function() { 44 | var app = new Miso.Storyboard({ 45 | initial : "unloaded", 46 | scenes : { 47 | unloaded : {}, 48 | loaded : { 49 | enter : function() { 50 | return false; 51 | } 52 | } 53 | } 54 | }); 55 | 56 | app.start().then(function() { 57 | var promise = app.to("loaded"); 58 | promise.fail(function() { 59 | ok(true); 60 | }); 61 | equals(app.scene(), "unloaded"); 62 | }); 63 | }); 64 | 65 | test("returning undefined on enter or exit does not cause a failure", 2, function() { 66 | var app = new Miso.Storyboard({ 67 | initial : "unloaded", 68 | scenes : { 69 | unloaded : { 70 | exit : function() {} 71 | }, 72 | loaded : { 73 | enter : function() {} 74 | } 75 | } 76 | }); 77 | 78 | app.start().then(function() { 79 | var promise = app.to("loaded"); 80 | promise.done(function() { 81 | ok(true); 82 | }); 83 | equals(app.scene(), "loaded"); 84 | }); 85 | }); 86 | 87 | test("returning false on exit stops transition", 2, function() { 88 | var app = new Miso.Storyboard({ 89 | initial : "unloaded", 90 | scenes : { 91 | loaded : {}, 92 | unloaded : { 93 | exit : function() { 94 | return false; 95 | } 96 | } 97 | } 98 | }); 99 | 100 | app.start().then(function() { 101 | var promise = app.to("loaded"); 102 | promise.fail(function() { 103 | ok(true); 104 | }); 105 | equals(app.scene(), "unloaded"); 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /test/vendor/jslitmus.js: -------------------------------------------------------------------------------- 1 | // JSLitmus.js 2 | // 3 | // Copyright (c) 2010, Robert Kieffer, http://broofa.com 4 | // Available under MIT license (http://en.wikipedia.org/wiki/MIT_License) 5 | 6 | (function() { 7 | // Private methods and state 8 | 9 | // Get platform info but don't go crazy trying to recognize everything 10 | // that's out there. This is just for the major platforms and OSes. 11 | var platform = 'unknown platform', ua = navigator.userAgent; 12 | 13 | // Detect OS 14 | var oses = ['Windows','iPhone OS','(Intel |PPC )?Mac OS X','Linux'].join('|'); 15 | var pOS = new RegExp('((' + oses + ') [^ \);]*)').test(ua) ? RegExp.$1 : null; 16 | if (!pOS) pOS = new RegExp('((' + oses + ')[^ \);]*)').test(ua) ? RegExp.$1 : null; 17 | 18 | // Detect browser 19 | var pName = /(Chrome|MSIE|Safari|Opera|Firefox)/.test(ua) ? RegExp.$1 : null; 20 | 21 | // Detect version 22 | var vre = new RegExp('(Version|' + pName + ')[ \/]([^ ;]*)'); 23 | var pVersion = (pName && vre.test(ua)) ? RegExp.$2 : null; 24 | var platform = (pOS && pName && pVersion) ? pName + ' ' + pVersion + ' on ' + pOS : 'unknown platform'; 25 | 26 | /** 27 | * A smattering of methods that are needed to implement the JSLitmus testbed. 28 | */ 29 | var jsl = { 30 | /** 31 | * Enhanced version of escape() 32 | */ 33 | escape: function(s) { 34 | s = s.replace(/,/g, '\\,'); 35 | s = escape(s); 36 | s = s.replace(/\+/g, '%2b'); 37 | s = s.replace(/ /g, '+'); 38 | return s; 39 | }, 40 | 41 | /** 42 | * Get an element by ID. 43 | */ 44 | $: function(id) { 45 | return document.getElementById(id); 46 | }, 47 | 48 | /** 49 | * Null function 50 | */ 51 | F: function() {}, 52 | 53 | /** 54 | * Set the status shown in the UI 55 | */ 56 | status: function(msg) { 57 | var el = jsl.$('jsl_status'); 58 | if (el) el.innerHTML = msg || ''; 59 | }, 60 | 61 | /** 62 | * Convert a number to an abbreviated string like, "15K" or "10M" 63 | */ 64 | toLabel: function(n) { 65 | if (n == Infinity) { 66 | return 'Infinity'; 67 | } else if (n > 1e9) { 68 | n = Math.round(n/1e8); 69 | return n/10 + 'B'; 70 | } else if (n > 1e6) { 71 | n = Math.round(n/1e5); 72 | return n/10 + 'M'; 73 | } else if (n > 1e3) { 74 | n = Math.round(n/1e2); 75 | return n/10 + 'K'; 76 | } 77 | return n; 78 | }, 79 | 80 | /** 81 | * Copy properties from src to dst 82 | */ 83 | extend: function(dst, src) { 84 | for (var k in src) dst[k] = src[k]; return dst; 85 | }, 86 | 87 | /** 88 | * Like Array.join(), but for the key-value pairs in an object 89 | */ 90 | join: function(o, delimit1, delimit2) { 91 | if (o.join) return o.join(delimit1); // If it's an array 92 | var pairs = []; 93 | for (var k in o) pairs.push(k + delimit1 + o[k]); 94 | return pairs.join(delimit2); 95 | }, 96 | 97 | /** 98 | * Array#indexOf isn't supported in IE, so we use this as a cross-browser solution 99 | */ 100 | indexOf: function(arr, o) { 101 | if (arr.indexOf) return arr.indexOf(o); 102 | for (var i = 0; i < this.length; i++) if (arr[i] === o) return i; 103 | return -1; 104 | } 105 | }; 106 | 107 | /** 108 | * Test manages a single test (created with 109 | * JSLitmus.test()) 110 | * 111 | * @private 112 | */ 113 | var Test = function (name, f) { 114 | if (!f) throw new Error('Undefined test function'); 115 | if (!/function[^\(]*\(([^,\)]*)/.test(f.toString())) { 116 | throw new Error('"' + name + '" test: Test is not a valid Function object'); 117 | } 118 | this.loopArg = RegExp.$1; 119 | this.name = name; 120 | this.f = f; 121 | }; 122 | 123 | jsl.extend(Test, /** @lends Test */ { 124 | /** Calibration tests for establishing iteration loop overhead */ 125 | CALIBRATIONS: [ 126 | new Test('calibrating loop', function(count) {while (count--);}), 127 | new Test('calibrating function', jsl.F) 128 | ], 129 | 130 | /** 131 | * Run calibration tests. Returns true if calibrations are not yet 132 | * complete (in which case calling code should run the tests yet again). 133 | * onCalibrated - Callback to invoke when calibrations have finished 134 | */ 135 | calibrate: function(onCalibrated) { 136 | for (var i = 0; i < Test.CALIBRATIONS.length; i++) { 137 | var cal = Test.CALIBRATIONS[i]; 138 | if (cal.running) return true; 139 | if (!cal.count) { 140 | cal.isCalibration = true; 141 | cal.onStop = onCalibrated; 142 | //cal.MIN_TIME = .1; // Do calibrations quickly 143 | cal.run(2e4); 144 | return true; 145 | } 146 | } 147 | return false; 148 | } 149 | }); 150 | 151 | jsl.extend(Test.prototype, {/** @lends Test.prototype */ 152 | /** Initial number of iterations */ 153 | INIT_COUNT: 10, 154 | /** Max iterations allowed (i.e. used to detect bad looping functions) */ 155 | MAX_COUNT: 1e9, 156 | /** Minimum time a test should take to get valid results (secs) */ 157 | MIN_TIME: .5, 158 | 159 | /** Callback invoked when test state changes */ 160 | onChange: jsl.F, 161 | 162 | /** Callback invoked when test is finished */ 163 | onStop: jsl.F, 164 | 165 | /** 166 | * Reset test state 167 | */ 168 | reset: function() { 169 | delete this.count; 170 | delete this.time; 171 | delete this.running; 172 | delete this.error; 173 | }, 174 | 175 | /** 176 | * Run the test (in a timeout). We use a timeout to make sure the browser 177 | * has a chance to finish rendering any UI changes we've made, like 178 | * updating the status message. 179 | */ 180 | run: function(count) { 181 | count = count || this.INIT_COUNT; 182 | jsl.status(this.name + ' x ' + count); 183 | this.running = true; 184 | var me = this; 185 | setTimeout(function() {me._run(count);}, 200); 186 | }, 187 | 188 | /** 189 | * The nuts and bolts code that actually runs a test 190 | */ 191 | _run: function(count) { 192 | var me = this; 193 | 194 | // Make sure calibration tests have run 195 | if (!me.isCalibration && Test.calibrate(function() {me.run(count);})) return; 196 | this.error = null; 197 | 198 | try { 199 | var start, f = this.f, now, i = count; 200 | 201 | // Start the timer 202 | start = new Date(); 203 | 204 | // Now for the money shot. If this is a looping function ... 205 | if (this.loopArg) { 206 | // ... let it do the iteration itself 207 | f(count); 208 | } else { 209 | // ... otherwise do the iteration for it 210 | while (i--) f(); 211 | } 212 | 213 | // Get time test took (in secs) 214 | this.time = Math.max(1,new Date() - start)/1000; 215 | 216 | // Store iteration count and per-operation time taken 217 | this.count = count; 218 | this.period = this.time/count; 219 | 220 | // Do we need to do another run? 221 | this.running = this.time <= this.MIN_TIME; 222 | 223 | // ... if so, compute how many times we should iterate 224 | if (this.running) { 225 | // Bump the count to the nearest power of 2 226 | var x = this.MIN_TIME/this.time; 227 | var pow = Math.pow(2, Math.max(1, Math.ceil(Math.log(x)/Math.log(2)))); 228 | count *= pow; 229 | if (count > this.MAX_COUNT) { 230 | throw new Error('Max count exceeded. If this test uses a looping function, make sure the iteration loop is working properly.'); 231 | } 232 | } 233 | } catch (e) { 234 | // Exceptions are caught and displayed in the test UI 235 | this.reset(); 236 | this.error = e; 237 | } 238 | 239 | // Figure out what to do next 240 | if (this.running) { 241 | me.run(count); 242 | } else { 243 | jsl.status(''); 244 | me.onStop(me); 245 | } 246 | 247 | // Finish up 248 | this.onChange(this); 249 | }, 250 | 251 | /** 252 | * Get the number of operations per second for this test. 253 | * 254 | * @param normalize if true, iteration loop overhead taken into account 255 | */ 256 | getHz: function(/**Boolean*/ normalize) { 257 | var p = this.period; 258 | 259 | // Adjust period based on the calibration test time 260 | if (normalize && !this.isCalibration) { 261 | var cal = Test.CALIBRATIONS[this.loopArg ? 0 : 1]; 262 | 263 | // If the period is within 20% of the calibration time, then zero the 264 | // it out 265 | p = p < cal.period*1.2 ? 0 : p - cal.period; 266 | } 267 | 268 | return Math.round(1/p); 269 | }, 270 | 271 | /** 272 | * Get a friendly string describing the test 273 | */ 274 | toString: function() { 275 | return this.name + ' - ' + this.time/this.count + ' secs'; 276 | } 277 | }); 278 | 279 | // CSS we need for the UI 280 | var STYLESHEET = ''; 360 | 361 | // HTML markup for the UI 362 | var MARKUP = '
      \ 363 | \ 364 | \ 365 |
      \ 366 |
      \ 367 | Normalize results \ 368 | \ 369 | \ 370 | \ 371 | \ 372 | \ 373 | \ 374 | \ 375 | \ 376 | \ 377 | \ 378 | \ 379 |
      ' + platform + '
      TestOps/sec
      \ 380 |
      \ 381 | \ 386 | Powered by JSLitmus \ 387 |
      '; 388 | 389 | /** 390 | * The public API for creating and running tests 391 | */ 392 | window.JSLitmus = { 393 | /** The list of all tests that have been registered with JSLitmus.test */ 394 | _tests: [], 395 | /** The queue of tests that need to be run */ 396 | _queue: [], 397 | 398 | /** 399 | * The parsed query parameters the current page URL. This is provided as a 400 | * convenience for test functions - it's not used by JSLitmus proper 401 | */ 402 | params: {}, 403 | 404 | /** 405 | * Initialize 406 | */ 407 | _init: function() { 408 | // Parse query params into JSLitmus.params[] hash 409 | var match = (location + '').match(/([^?#]*)(#.*)?$/); 410 | if (match) { 411 | var pairs = match[1].split('&'); 412 | for (var i = 0; i < pairs.length; i++) { 413 | var pair = pairs[i].split('='); 414 | if (pair.length > 1) { 415 | var key = pair.shift(); 416 | var value = pair.length > 1 ? pair.join('=') : pair[0]; 417 | this.params[key] = value; 418 | } 419 | } 420 | } 421 | 422 | // Write out the stylesheet. We have to do this here because IE 423 | // doesn't honor sheets written after the document has loaded. 424 | document.write(STYLESHEET); 425 | 426 | // Setup the rest of the UI once the document is loaded 427 | if (window.addEventListener) { 428 | window.addEventListener('load', this._setup, false); 429 | } else if (document.addEventListener) { 430 | document.addEventListener('load', this._setup, false); 431 | } else if (window.attachEvent) { 432 | window.attachEvent('onload', this._setup); 433 | } 434 | 435 | return this; 436 | }, 437 | 438 | /** 439 | * Set up the UI 440 | */ 441 | _setup: function() { 442 | var el = jsl.$('jslitmus_container'); 443 | if (!el) document.body.appendChild(el = document.createElement('div')); 444 | 445 | el.innerHTML = MARKUP; 446 | 447 | // Render the UI for all our tests 448 | for (var i=0; i < JSLitmus._tests.length; i++) 449 | JSLitmus.renderTest(JSLitmus._tests[i]); 450 | }, 451 | 452 | /** 453 | * (Re)render all the test results 454 | */ 455 | renderAll: function() { 456 | for (var i = 0; i < JSLitmus._tests.length; i++) 457 | JSLitmus.renderTest(JSLitmus._tests[i]); 458 | JSLitmus.renderChart(); 459 | }, 460 | 461 | /** 462 | * (Re)render the chart graphics 463 | */ 464 | renderChart: function() { 465 | var url = JSLitmus.chartUrl(); 466 | jsl.$('chart_link').href = url; 467 | jsl.$('chart_image').src = url; 468 | jsl.$('chart').style.display = ''; 469 | 470 | // Update the tiny URL 471 | jsl.$('tiny_url').src = 'http://tinyurl.com/api-create.php?url='+escape(url); 472 | }, 473 | 474 | /** 475 | * (Re)render the results for a specific test 476 | */ 477 | renderTest: function(test) { 478 | // Make a new row if needed 479 | if (!test._row) { 480 | var trow = jsl.$('test_row_template'); 481 | if (!trow) return; 482 | 483 | test._row = trow.cloneNode(true); 484 | test._row.style.display = ''; 485 | test._row.id = ''; 486 | test._row.onclick = function() {JSLitmus._queueTest(test);}; 487 | test._row.title = 'Run ' + test.name + ' test'; 488 | trow.parentNode.appendChild(test._row); 489 | test._row.cells[0].innerHTML = test.name; 490 | } 491 | 492 | var cell = test._row.cells[1]; 493 | var cns = [test.loopArg ? 'test_looping' : 'test_nonlooping']; 494 | 495 | if (test.error) { 496 | cns.push('test_error'); 497 | cell.innerHTML = 498 | '
      ' + test.error + '
      ' + 499 | ''; 502 | } else { 503 | if (test.running) { 504 | cns.push('test_running'); 505 | cell.innerHTML = 'running'; 506 | } else if (jsl.indexOf(JSLitmus._queue, test) >= 0) { 507 | cns.push('test_pending'); 508 | cell.innerHTML = 'pending'; 509 | } else if (test.count) { 510 | cns.push('test_done'); 511 | var hz = test.getHz(jsl.$('test_normalize').checked); 512 | cell.innerHTML = hz != Infinity ? hz : '∞'; 513 | cell.title = 'Looped ' + test.count + ' times in ' + test.time + ' seconds'; 514 | } else { 515 | cell.innerHTML = 'ready'; 516 | } 517 | } 518 | cell.className = cns.join(' '); 519 | }, 520 | 521 | /** 522 | * Create a new test 523 | */ 524 | test: function(name, f) { 525 | // Create the Test object 526 | var test = new Test(name, f); 527 | JSLitmus._tests.push(test); 528 | 529 | // Re-render if the test state changes 530 | test.onChange = JSLitmus.renderTest; 531 | 532 | // Run the next test if this one finished 533 | test.onStop = function(test) { 534 | if (JSLitmus.onTestFinish) JSLitmus.onTestFinish(test); 535 | JSLitmus.currentTest = null; 536 | JSLitmus._nextTest(); 537 | }; 538 | 539 | // Render the new test 540 | this.renderTest(test); 541 | }, 542 | 543 | /** 544 | * Add all tests to the run queue 545 | */ 546 | runAll: function(e) { 547 | e = e || window.event; 548 | var reverse = e && e.shiftKey, len = JSLitmus._tests.length; 549 | for (var i = 0; i < len; i++) { 550 | JSLitmus._queueTest(JSLitmus._tests[!reverse ? i : (len - i - 1)]); 551 | } 552 | }, 553 | 554 | /** 555 | * Remove all tests from the run queue. The current test has to finish on 556 | * it's own though 557 | */ 558 | stop: function() { 559 | while (JSLitmus._queue.length) { 560 | var test = JSLitmus._queue.shift(); 561 | JSLitmus.renderTest(test); 562 | } 563 | }, 564 | 565 | /** 566 | * Run the next test in the run queue 567 | */ 568 | _nextTest: function() { 569 | if (!JSLitmus.currentTest) { 570 | var test = JSLitmus._queue.shift(); 571 | if (test) { 572 | jsl.$('stop_button').disabled = false; 573 | JSLitmus.currentTest = test; 574 | test.run(); 575 | JSLitmus.renderTest(test); 576 | if (JSLitmus.onTestStart) JSLitmus.onTestStart(test); 577 | } else { 578 | jsl.$('stop_button').disabled = true; 579 | JSLitmus.renderChart(); 580 | } 581 | } 582 | }, 583 | 584 | /** 585 | * Add a test to the run queue 586 | */ 587 | _queueTest: function(test) { 588 | if (jsl.indexOf(JSLitmus._queue, test) >= 0) return; 589 | JSLitmus._queue.push(test); 590 | JSLitmus.renderTest(test); 591 | JSLitmus._nextTest(); 592 | }, 593 | 594 | /** 595 | * Generate a Google Chart URL that shows the data for all tests 596 | */ 597 | chartUrl: function() { 598 | var n = JSLitmus._tests.length, markers = [], data = []; 599 | var d, min = 0, max = -1e10; 600 | var normalize = jsl.$('test_normalize').checked; 601 | 602 | // Gather test data 603 | for (var i=0; i < JSLitmus._tests.length; i++) { 604 | var test = JSLitmus._tests[i]; 605 | if (test.count) { 606 | var hz = test.getHz(normalize); 607 | var v = hz != Infinity ? hz : 0; 608 | data.push(v); 609 | markers.push('t' + jsl.escape(test.name + '(' + jsl.toLabel(hz)+ ')') + ',000000,0,' + 610 | markers.length + ',10'); 611 | max = Math.max(v, max); 612 | } 613 | } 614 | if (markers.length <= 0) return null; 615 | 616 | // Build chart title 617 | var title = document.getElementsByTagName('title'); 618 | title = (title && title.length) ? title[0].innerHTML : null; 619 | var chart_title = []; 620 | if (title) chart_title.push(title); 621 | chart_title.push('Ops/sec (' + platform + ')'); 622 | 623 | // Build labels 624 | var labels = [jsl.toLabel(min), jsl.toLabel(max)]; 625 | 626 | var w = 250, bw = 15; 627 | var bs = 5; 628 | var h = markers.length*(bw + bs) + 30 + chart_title.length*20; 629 | 630 | var params = { 631 | chtt: escape(chart_title.join('|')), 632 | chts: '000000,10', 633 | cht: 'bhg', // chart type 634 | chd: 't:' + data.join(','), // data set 635 | chds: min + ',' + max, // max/min of data 636 | chxt: 'x', // label axes 637 | chxl: '0:|' + labels.join('|'), // labels 638 | chsp: '0,1', 639 | chm: markers.join('|'), // test names 640 | chbh: [bw, 0, bs].join(','), // bar widths 641 | // chf: 'bg,lg,0,eeeeee,0,eeeeee,.5,ffffff,1', // gradient 642 | chs: w + 'x' + h 643 | }; 644 | return 'http://chart.apis.google.com/chart?' + jsl.join(params, '=', '&'); 645 | } 646 | }; 647 | 648 | JSLitmus._init(); 649 | })(); 650 | -------------------------------------------------------------------------------- /test/vendor/qunit.css: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit 1.2.0pre - A JavaScript Unit Testing Framework 3 | * 4 | * http://docs.jquery.com/QUnit 5 | * 6 | * Copyright (c) 2011 John Resig, Jörn Zaefferer 7 | * Dual licensed under the MIT (MIT-LICENSE.txt) 8 | * or GPL (GPL-LICENSE.txt) licenses. 9 | * Pulled Live from Git Mon Oct 31 14:00:02 UTC 2011 10 | * Last Commit: ee156923cdb01820e35e6bb579d5cf6bf55736d4 11 | */ 12 | 13 | /** Font Family and Sizes */ 14 | 15 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult { 16 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; 17 | } 18 | 19 | #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } 20 | #qunit-tests { font-size: smaller; } 21 | 22 | 23 | /** Resets */ 24 | 25 | #qunit-tests, #qunit-tests ol, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult { 26 | margin: 0; 27 | padding: 0; 28 | } 29 | 30 | 31 | /** Header */ 32 | 33 | #qunit-header { 34 | padding: 0.5em 0 0.5em 1em; 35 | 36 | color: #8699a4; 37 | background-color: #0d3349; 38 | 39 | font-size: 1.5em; 40 | line-height: 1em; 41 | font-weight: normal; 42 | 43 | border-radius: 15px 15px 0 0; 44 | -moz-border-radius: 15px 15px 0 0; 45 | -webkit-border-top-right-radius: 15px; 46 | -webkit-border-top-left-radius: 15px; 47 | } 48 | 49 | #qunit-header a { 50 | text-decoration: none; 51 | color: #c2ccd1; 52 | } 53 | 54 | #qunit-header a:hover, 55 | #qunit-header a:focus { 56 | color: #fff; 57 | } 58 | 59 | #qunit-banner { 60 | height: 5px; 61 | } 62 | 63 | #qunit-testrunner-toolbar { 64 | padding: 0.5em 0 0.5em 2em; 65 | color: #5E740B; 66 | background-color: #eee; 67 | } 68 | 69 | #qunit-userAgent { 70 | padding: 0.5em 0 0.5em 2.5em; 71 | background-color: #2b81af; 72 | color: #fff; 73 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; 74 | } 75 | 76 | 77 | /** Tests: Pass/Fail */ 78 | 79 | #qunit-tests { 80 | list-style-position: inside; 81 | } 82 | 83 | #qunit-tests li { 84 | padding: 0.4em 0.5em 0.4em 2.5em; 85 | border-bottom: 1px solid #fff; 86 | list-style-position: inside; 87 | } 88 | 89 | #qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running { 90 | display: none; 91 | } 92 | 93 | #qunit-tests li strong { 94 | cursor: pointer; 95 | } 96 | 97 | #qunit-tests li a { 98 | padding: 0.5em; 99 | color: #c2ccd1; 100 | text-decoration: none; 101 | } 102 | #qunit-tests li a:hover, 103 | #qunit-tests li a:focus { 104 | color: #000; 105 | } 106 | 107 | #qunit-tests ol { 108 | margin-top: 0.5em; 109 | padding: 0.5em; 110 | 111 | background-color: #fff; 112 | 113 | border-radius: 15px; 114 | -moz-border-radius: 15px; 115 | -webkit-border-radius: 15px; 116 | 117 | box-shadow: inset 0px 2px 13px #999; 118 | -moz-box-shadow: inset 0px 2px 13px #999; 119 | -webkit-box-shadow: inset 0px 2px 13px #999; 120 | } 121 | 122 | #qunit-tests table { 123 | border-collapse: collapse; 124 | margin-top: .2em; 125 | } 126 | 127 | #qunit-tests th { 128 | text-align: right; 129 | vertical-align: top; 130 | padding: 0 .5em 0 0; 131 | } 132 | 133 | #qunit-tests td { 134 | vertical-align: top; 135 | } 136 | 137 | #qunit-tests pre { 138 | margin: 0; 139 | white-space: pre-wrap; 140 | word-wrap: break-word; 141 | } 142 | 143 | #qunit-tests del { 144 | background-color: #e0f2be; 145 | color: #374e0c; 146 | text-decoration: none; 147 | } 148 | 149 | #qunit-tests ins { 150 | background-color: #ffcaca; 151 | color: #500; 152 | text-decoration: none; 153 | } 154 | 155 | /*** Test Counts */ 156 | 157 | #qunit-tests b.counts { color: black; } 158 | #qunit-tests b.passed { color: #5E740B; } 159 | #qunit-tests b.failed { color: #710909; } 160 | 161 | #qunit-tests li li { 162 | margin: 0.5em; 163 | padding: 0.4em 0.5em 0.4em 0.5em; 164 | background-color: #fff; 165 | border-bottom: none; 166 | list-style-position: inside; 167 | } 168 | 169 | /*** Passing Styles */ 170 | 171 | #qunit-tests li li.pass { 172 | color: #5E740B; 173 | background-color: #fff; 174 | border-left: 26px solid #C6E746; 175 | } 176 | 177 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } 178 | #qunit-tests .pass .test-name { color: #366097; } 179 | 180 | #qunit-tests .pass .test-actual, 181 | #qunit-tests .pass .test-expected { color: #999999; } 182 | 183 | #qunit-banner.qunit-pass { background-color: #C6E746; } 184 | 185 | /*** Failing Styles */ 186 | 187 | #qunit-tests li li.fail { 188 | color: #710909; 189 | background-color: #fff; 190 | border-left: 26px solid #EE5757; 191 | white-space: pre; 192 | } 193 | 194 | #qunit-tests > li:last-child { 195 | border-radius: 0 0 15px 15px; 196 | -moz-border-radius: 0 0 15px 15px; 197 | -webkit-border-bottom-right-radius: 15px; 198 | -webkit-border-bottom-left-radius: 15px; 199 | } 200 | 201 | #qunit-tests .fail { color: #000000; background-color: #EE5757; } 202 | #qunit-tests .fail .test-name, 203 | #qunit-tests .fail .module-name { color: #000000; } 204 | 205 | #qunit-tests .fail .test-actual { color: #EE5757; } 206 | #qunit-tests .fail .test-expected { color: green; } 207 | 208 | #qunit-banner.qunit-fail { background-color: #EE5757; } 209 | 210 | 211 | /** Result */ 212 | 213 | #qunit-testresult { 214 | padding: 0.5em 0.5em 0.5em 2.5em; 215 | 216 | color: #2b81af; 217 | background-color: #D2E0E6; 218 | 219 | border-bottom: 1px solid white; 220 | } 221 | 222 | /** Fixture */ 223 | 224 | #qunit-fixture { 225 | position: absolute; 226 | top: -10000px; 227 | left: -10000px; 228 | } 229 | --------------------------------------------------------------------------------