UNPKG

marbles

Version:

Front-end framework for routing, http, and data handling

1,550 lines (1,351 loc) 115 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>JSDoc: Source: marbles.js</title> <script src="scripts/prettify/prettify.js"> </script> <script src="scripts/prettify/lang-css.js"> </script> <!--[if lt IE 9]> <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script> <![endif]--> <link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css"> <link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css"> </head> <body> <div id="main"> <h1 class="page-title">Source: marbles.js</h1> <section> <article> <pre class="prettyprint source linenums"><code>/** * @global * @namespace Marbles */ (function(__m__) { "use strict"; __m__["marbles/version"] = {}; /** * @global * @namespace Marbles */ var VERSION = '0.0.5'; if (true) { __m__["marbles/version"].VERSION = VERSION; } })(window.____modules____ = window.____modules____ || {}); (function(__m__) { "use strict"; __m__["marbles/utils"] = {}; var __extend = function (obj, others, options) { var override = options.override; for (var i = 0, _len = others.length; i &lt; _len; i++) { var other = others[i]; for (var key in other) { if (other.hasOwnProperty(key)) { if (override === false) { continue; } obj[key] = other[key]; } } } return obj; }; /** * @memberof Marbles * @namespace Utils */ var Utils = { /** * @memberof Marbles.Utils * @func * @param {Object} obj The object to extend * @param {...Object} other One or more objects to extend it with */ extend: function (obj) { var others = Array.prototype.slice.call(arguments, 1); return __extend(obj, others, {override: true}); }, lazyExtend: function (obj) { var others = Array.prototype.slice.call(arguments, 1); return __extend(obj, others, {override: false}); }, /** * @memberof Marbles.Utils * @func * @param {*} obj * @param {*} otherObj * @returns {bool} * @desc compare two objects of any type */ assertEqual: function (obj, other) { if (typeof obj !== typeof other) { return false; } if (typeof obj !== "object") { return obj === other; } if (Array.isArray(obj)) { if ( !Array.isArray(other) ) { return false; } if (obj.length !== other.length) { return false; } for (var i = 0, len = obj.length; i &lt; len; i++) { if ( !this.assertEqual(obj[i], other[i]) ) { return false; } } return true; } // both ids are objects for (var k in obj) { if (obj.hasOwnProperty(k)) { if (obj[k] !== other[k]) { return false; } } } for (k in other) { if (other.hasOwnProperty(k)) { if (other[k] !== obj[k]) { return false; } } } return true; }, /** * @memberof Marbles.Utils * @func * @param {Class} child * @param {Class} parent * @returns {Class} child * @private * @desc The prototype of child is made to inherit from parent. * A `__super__` property is added to the child constructor to access the parent prototype. */ inheritPrototype: function(child, parent) { Utils.extend(child, parent); function Ctor() { this.constructor = child; } Ctor.prototype = parent.prototype; child.prototype = new Ctor(); child.__super__ = parent.prototype; return child; }, /** * @memberof Marbles.Utils * @func * @param {Object} proto * @returns {Class} ctor * @desc Creates a constructor with given prototype * @example * Utils.createClass({ * displayName: "MyClass", // ctor.displayName * * // Array of objects to mix-into prototype * mixins: [ * Marbles.State * ], * * parentClass: MyOtherClass, // inherit from MyOtherClass (optional) * * willInitialize: function () {}, // called before parent ctor is called * * didInitialize: function () {}, // called after parent ctor is called * * myProperty: 123 // no special meaning * }); */ createClass: function (proto) { var ctor, willInitialize = proto.willInitialize, didInitialize = proto.didInitialize, k, i, _len, mixin, mixin_callbacks; mixin_callbacks = { didExtendCtor: [], didExtendProto: [], willInitialize: [], didInitialize: [] }; if (proto.hasOwnProperty('willInitialize')) { delete proto.willInitialize; } if (proto.hasOwnProperty('didInitialize')) { delete proto.didInitialize; } // Override willInitialize hook to // call mixin hooks first var __willInitialize = willInitialize; willInitialize = function () { var args = arguments; mixin_callbacks.willInitialize.forEach(function (callback) { callback.apply(this, args); }.bind(this)); if (__willInitialize) { return __willInitialize.apply(this, args); } }; // Override didInitialize hook to // call mixin hooks first var __didInitialize = didInitialize; didInitialize = function () { var args = arguments; mixin_callbacks.didInitialize.forEach(function (callback) { callback.apply(this, args); }.bind(this)); if (__didInitialize) { __didInitialize.apply(this, args); } }; if (proto.hasOwnProperty('parentClass')) { var parent = proto.parentClass; delete proto.parentClass; ctor = function () { // Handle any initialization before // we call the parent constructor var res = willInitialize.apply(this, arguments); if (res) { return res; } // Call the parent constructor ctor.__super__.constructor.apply(this, arguments); // Handle any initialization after // we call the parent constructor didInitialize.apply(this, arguments); return this; }; Utils.inheritPrototype(ctor, parent); } else { ctor = function () { // Call initialization functions if (typeof willInitialize === 'function') { var res = willInitialize.apply(this, arguments); if (res &amp;&amp; res.constructor === ctor) { return res; } } if (typeof didInitialize === 'function') { didInitialize.apply(this, arguments); } return this; }; } // If a displayName property is given // move it to the constructor if (proto.hasOwnProperty('displayName')) { ctor.displayName = proto.displayName; delete proto.displayName; } // Grab any given mixins from proto var mixins = []; if (proto.hasOwnProperty('mixins')) { mixins = proto.mixins; delete proto.mixins; } // Convenience method for creating subclass ctor.createClass = function (proto) { var _child_ctor = Utils.createClass(Utils.extend({}, proto, { parentClass: ctor })); ['didExtendCtor', 'didExtendProto'].forEach(function (callback_name) { mixin_callbacks[callback_name].forEach(function (callback) { callback(_child_ctor); }); }); return _child_ctor; }; // Add all remaining properties // on proto to the prototype for (k in proto) { if (proto.hasOwnProperty(k)) { ctor.prototype[k] = proto[k]; } } // Extend the prototype and/or ctor with any given mixins for (i = 0, _len = mixins.length; i &lt; _len; i++) { mixin = mixins[i]; if (mixin.hasOwnProperty('ctor') || mixin.hasOwnProperty('proto')) { // extend ctor if (mixin.hasOwnProperty('ctor')) { Utils.extend(ctor, mixin.ctor); if (typeof mixin.didExtendCtor === 'function') { mixin_callbacks.didExtendCtor.push(mixin.didExtendCtor); mixin.didExtendCtor(ctor); } } // extend proto if (mixin.hasOwnProperty('proto')) { Utils.extend(ctor.prototype, mixin.proto); if (typeof mixin.didExtendProto === 'function') { mixin_callbacks.didExtendCtor.push(mixin.didExtendProto); mixin.didExtendProto(ctor); } } if (typeof mixin.willInitialize === 'function') { mixin_callbacks.willInitialize.push(mixin.willInitialize); } if (typeof mixin.didInitialize === 'function') { mixin_callbacks.didInitialize.push(mixin.didInitialize); } } else { // It's a plain old object // extend the prototype with it Utils.extend(ctor.prototype, mixin); } } return ctor; } }; __m__["marbles/utils"].default = Utils; })(window.____modules____ = window.____modules____ || {}); (function(__m__) { "use strict"; __m__["marbles/dispatcher"] = {}; var __callbacks = []; /** * @memberof Marbles * @mixin * @desc Simple FLUX Dispatcher */ var Dispatcher = { /** * @method * @param {function} callback Function to call events with * @returns {Number} Dispatch index */ register: function (callback) { __callbacks.push(callback); var dispatchIndex = __callbacks.length - 1; return dispatchIndex; }, /** * @method * @param {Object} event An event object * @returns {Promise} Resolves when all registered callbacks have been called */ dispatch: function (event) { var promises = __callbacks.map(function (callback) { return new Promise(function (resolve) { resolve(callback(event)); }); }); return Promise.all(promises); } }; __m__["marbles/dispatcher"].default = Dispatcher; })(window.____modules____ = window.____modules____ || {}); /** * @memberof Marbles * @mixin * @desc Manages a state object. You must define `state` {Object} and `__changeListeners` {Array} on the object this is mixed into. */ (function(__m__) { "use strict"; __m__["marbles/state"] = {}; /** * @memberof Marbles * @mixin * @desc Manages a state object. You must define `state` {Object} and `__changeListeners` {Array} on the object this is mixed into. */ var State = { /** * @method * @param {function} handler Function to call when the state object changes */ addChangeListener: function (handler) { this.__changeListeners.push(handler); }, /** * @method * @param {function} handler * @desc Prevents handler from being called for future changes */ removeChangeListener: function (handler) { this.__changeListeners = this.__changeListeners.filter(function (fn) { return fn !== handler; }); }, /** * @method * @param {Object} newState * @desc Copies properties of newState to the existing state object */ setState: function (newState) { this.willUpdate(); var state = this.state; Object.keys(newState).forEach(function (key) { state[key] = newState[key]; }); this.handleChange(); this.didUpdate(); }, /** * @method * @param {Object} newState * @desc Replaces the existing state object with newState */ replaceState: function (newState) { this.willUpdate(); this.state = newState; this.handleChange(); this.didUpdate(); }, handleChange: function () { this.__changeListeners.forEach(function (handler) { handler(); }); }, /** * @method * @desc Called before state object is mutated */ willUpdate: function () {}, /** * @method * @desc Called after state object is mutated */ didUpdate: function () {} }; __m__["marbles/state"].default = State; })(window.____modules____ = window.____modules____ || {}); (function(__m__) { "use strict"; __m__["marbles/store"] = {}; var Utils = __m__["marbles/utils"].Utils; var State = __m__["marbles/state"].State; /** * @memberof Marbles * @class * @param {*} id Anything serializable as JSON * @desc This class is meant to be sub-classed using Store.createClass */ var Store = function (id) { this.id = id; this.constructor.__trackInstance(this); this.__changeListeners = []; this.willInitialize.apply(this, Array.prototype.slice.call(arguments, 1)); this.state = this.getInitialState(); this.didInitialize.apply(this, Array.prototype.slice.call(arguments, 1)); }; Store.displayName = "Marbles.Store"; Utils.extend(Store.prototype, State, { /** * @memberof Marbles.Store * @instance * @method * @returns {Object} Initial state object */ getInitialState: function () { return {}; }, /** * @memberof Marbles.Store * @instance * @method * @desc Called before state is initialized */ willInitialize: function () {}, /** * @memberof Marbles.Store * @instance * @method * @desc Called after state is initialized */ didInitialize: function () {}, /** * @memberof Marbles.Store * @instance * @method * @desc Called when first change listener is added */ didBecomeActive: function () {}, /** * @memberof Marbles.Store * @instance * @method * @desc Called when last change listener is removed and when the instance is otherwise perceived as inactive */ didBecomeInactive: function () {}, /** * @memberof Marbles.Store * @instance * @method * @param {Object} event * @desc Called with Dispatcher events */ handleEvent: function () {} }); // Call didBecomeActive when first change listener added Store.prototype.addChangeListener = function () { this.__changeListenerExpected = false; State.addChangeListener.apply(this, arguments); if ( !this.__active &amp;&amp; this.__changeListeners.length === 1 ) { this.__active = true; this.didBecomeActive(); } }; // Call didBecomeInactive when last change listener removed Store.prototype.removeChangeListener = function () { var done = function () { this.__active = false; }.bind(this); State.removeChangeListener.apply(this, arguments); if (this.__changeListeners.length === 0 &amp;&amp; !this.__changeListenerExpected) { Promise.resolve(this.didBecomeInactive()).then(done); } }; Store.prototype.expectChangeListener = function () { this.__changeListenerExpected = true; }; Store.prototype.unexpectChangeListener = function () { this.__changeListenerExpected = false; if (this.__changeListeners.length === 0) { Promise.resolve(this.didBecomeInactive()).then(function () { this.__active = false; }.bind(this)); } }; Store.__instances = {}; function stringifyStoreId(id) { if (id &amp;&amp; typeof id === "object" &amp;&amp; !Array.isArray(id)) { var keys = Object.keys(id); var values = keys.map(function (k) { return id[k]; }); return JSON.stringify(keys.sort().concat(values.sort())); } else if (Array.isArray(id)) { return "["+ id.map(stringifyStoreId).join(",") +"]"; } else { return JSON.stringify(id); } } Store.__getInstance = function (id, opts) { opts = opts || {}; var key = stringifyStoreId(id); return this.__instances[key] || (opts.allowNull ? null : new this(id)); }; Store.__trackInstance = function (instance) { var key = stringifyStoreId(instance.id); this.__instances[key] = instance; }; /** * @memberof Marbles.Store * @func * @param {Marbles.Store} store * @desc Give Store instance up for garbage collection */ Store.discardInstance = function (instance) { var key = stringifyStoreId(instance.id); delete this.__instances[key]; }; /** * @memberof Marbles.Store * @func * @param {Store#id} id */ Store.addChangeListener = function (id) { var instance = this.__getInstance(id); return instance.addChangeListener.apply(instance, Array.prototype.slice.call(arguments, 1)); }; /** * @memberof Marbles.Store * @func * @param {Store#id} id */ Store.removeChangeListener = function (id) { var instance = this.__getInstance(id); return instance.removeChangeListener.apply(instance, Array.prototype.slice.call(arguments, 1)); }; /** * @memberof Marbles.Store * @func * @param {Store#id} id * @desc Force store to remain active until the next change listener is added */ Store.expectChangeListener = function (id) { var instance = this.__getInstance(id, {allowNull: true}); if (instance) { instance.expectChangeListener(); } }; /** * @memberof Marbles.Store * @func * @param {Store#id} id * @desc Undo expectation from expectChangeListener */ Store.unexpectChangeListener = function (id) { var instance = this.__getInstance(id, {allowNull: true}); if (instance) { instance.unexpectChangeListener(); } }; /** * @memberof Marbles.Store * @prop {Number} */ Store.dispatcherIndex = null; /** * @memberof Marbles.Store * @func * @param {Marbles.Dispatcher} dispatcher */ Store.registerWithDispatcher = function (dispatcher) { this.dispatcherIndex = dispatcher.register(function (event) { if (event.storeId &amp;&amp; (!this.isValidId || this.isValidId(event.storeId))) { var instance = this.__getInstance(event.storeId); var res = Promise.resolve(instance.handleEvent(event)); var after = function (isError, args) { if (instance.__changeListeners.length === 0) { instance.didBecomeInactive(); } if (isError) { return Promise.reject(args); } else { return Promise.resolve(args); } }; res.then(after.bind(null, false), after.bind(null, true)); return res; } else { return Promise.all(Object.keys(this.__instances).sort().map(function (key) { var instance = this.__instances[key]; return new Promise(function (resolve) { resolve(instance.handleEvent(event)); }); }.bind(this))); } }.bind(this)); }; /** * @memberof Marbles.Store * @func * @param {Object} proto Prototype of new child class * @desc Creates a new class that inherits from Store * @example * var MyStore = Marbles.Store.createClass({ * displayName: "MyStore", * * getInitialState: function () { * return { my: "state" }; * }, * * willInitialize: function () { * // do something * }, * * didInitialize: function () { * // do something * }, * * didBecomeActive: function () { * // do something * }, * * didBecomeInactive: function () { * // do something * }, * * handleEvent: function (event) { * // do something * } * }); * */ Store.createClass = function (proto) { var parent = this; var store = Utils.inheritPrototype(function () { parent.apply(this, arguments); }, parent); store.__instances = {}; if (proto.hasOwnProperty("displayName")) { store.displayName = proto.displayName; delete proto.displayName; } Utils.extend(store.prototype, proto); function wrappedFn(name, id) { var instance = this.__getInstance(id); var res = instance[name].apply(instance, Array.prototype.slice.call(arguments, 2)); var after = function (isError, args) { if (instance.__changeListeners.length === 0) { instance.didBecomeInactive(); } if (isError) { return Promise.reject(args); } else { return Promise.resolve(args); } }; Promise.resolve(res).then(after.bind(null, false), after.bind(null, true)); return res; } for (var k in proto) { if (proto.hasOwnProperty(k) &amp;&amp; k.slice(0, 1) !== "_" &amp;&amp; typeof proto[k] === "function") { store[k] = wrappedFn.bind(store, k); } } return store; }; __m__["marbles/store"].default = Store; })(window.____modules____ = window.____modules____ || {}); (function(__m__) { "use strict"; __m__["marbles/events"] = {}; var EVENT_SPLITTER = /\s+/; function initEvents(obj) { if (!obj.__events) { obj.__events = {}; } } /** * @deprecated Use the Dispatcher instead * @see Marbles.Dispatcher * @memberof Marbles * @mixin */ var Events = { on: function (events, callback, context, options) { initEvents(this); if (!Array.isArray(events)) { events = events.split(EVENT_SPLITTER); } var name; for (var i = 0, _len = events.length; i &lt; _len; i++) { name = events[i]; if (!this.__events[name]) { this.__events[name] = []; } this.__events[name].push({ callback: callback, context: context || this, options: options || {} }); } return this; // chainable }, once: function (events, callback, context, options) { if (!Array.isArray(events)) { events = events.split(EVENT_SPLITTER); } var bindEvent = function (name) { var __callback = function () { this.off(name, __callback, this); callback.apply(context, arguments); }; this.on(name, __callback, this, options); }.bind(this); for (var i = 0, _len = events.length; i &lt; _len; i++) { bindEvent(events[i]); } return this; // chainable }, off: function (events, callback, context) { // Allow unbinding all events at once if (arguments.length === 0) { if (this.hasOwnProperty('__events')) { delete this.__events; } return this; // chainable } if (!Array.isArray(events)) { events = events.split(EVENT_SPLITTER); } if (!this.__events) { return this; // chainable } var __filterFn = function (binding) { if (context &amp;&amp; context !== binding.context) { return true; } if (callback &amp;&amp; callback !== binding.callback) { return true; } return false; }; var name, i, _len, bindings; for (i = 0, _len = events.length; i &lt; _len; i++) { name = events[i]; if (callback === undefined &amp;&amp; context === undefined) { if (this.__events.hasOwnProperty(name)) { delete this.__events[name]; } continue; } bindings = this.__events[name]; if (!bindings) { continue; } this.__events[name] = Array.prototype.filter.call(bindings, __filterFn); } return this; // chainable }, trigger: function (events) { var args = Array.prototype.slice.call(arguments, 1); if (!Array.isArray(events)) { events = events.split(EVENT_SPLITTER); } if (!this.__events) { return this; // chainable } var bindings, binding, i, j, _len, _l; for (i = 0, _len = events.length; i &lt; _len; i++) { bindings = this.__events[events[i]]; if (!bindings) { continue; } for (j = 0, _l = bindings.length; j &lt; _l; j++) { binding = bindings[j]; if (binding.options.args === false) { binding.callback.call(binding.context); } else { binding.callback.apply(binding.context, args); } } } return this; // chainable } }; __m__["marbles/events"].default = Events; })(window.____modules____ = window.____modules____ || {}); /** * @memberof Marbles * @mixin */ (function(__m__) { "use strict"; __m__["marbles/query_params"] = {}; /** * @memberof Marbles * @mixin */ var QueryParams = { /** * @method * @param {String} queryString * @returns {Array} params * @desc transforms a query string into an array of param objects (the first occurance of each param will be placed at index 0, the second at index 1, and so on). * @example * Marbles.QueryParams.deserializeParams("?a=1&amp;b=2&amp;c=3"); * //=> [{a: 1, b:2, c:3}] * @example * Marbles.QueryParams.deserializeParams("a=1&amp;b=2&amp;c=3"); * //=> [{a: 1, b:2, c:3}] * @example * Marbles.QueryParams.deserializeParams("?a=1&amp;a=2&amp;b=3&amp;c=4&amp;c=5"); * //=> [{a: 1, b:3, c:4}, {a: 2, c: 5}] */ deserializeParams: function (query) { if (query.substr(0, 1) === '?') { query = query.substring(1).split('&amp;'); } else { query = query.split('&amp;'); } var params = [{}]; for (var i = 0, _len = query.length; i &lt; _len; i++) { var q = query[i].split('='), key = decodeURIComponent(q[0]), val = decodeURIComponent(q[1] || '').replace('+', ' ').trim(); // + doesn't decode if (typeof val === 'string' &amp;&amp; !val) { // ignore empty values continue; } if (val.indexOf(',') !== -1) { // decode comma delemited values into arrays val = val.split(','); } // loop through param objects until we find one without key for (var j = 0, _l = params.length; j &lt; _l; j++) { if (params[j].hasOwnProperty(key)) { if (j === _l-1) { // create additional param objects as needed params.push({}); _l++; } continue; } else { params[j][key] = val; break; } } } return params; }, /** * @method * @desc Combines the first params array with the contents of all the others. Duplicate params are pushed into the next param object they do not comflict with. The mutated params array is returned. * @param {Array} params An array of param objects * @param {...Object} others Any number of param objects * @retuns {Array} params */ combineParams: function (params) { var others = Array.prototype.slice.call(arguments, 1); function addValue(key, val) { // loop through param objects until we find one without key for (var i = 0, _len = params.length; i &lt; _len; i++) { if (params[i].hasOwnProperty(key)) { if (i === _len-1) { // create additional param objects as needed params.push({}); _len++; } continue; } else { params[i][key] = val; break; } } } var key; for (var i = 0, _len = others.length; i &lt; _len; i++) { for (key in others[i]) { if (others[i].hasOwnProperty(key)) { addValue(key, others[i][key]); } } } return params; }, /** * @method * @desc Combines the first params array with the contents of all the others. Duplicate params are overwritten if they are at the same params index. * @param {Array} params An array of param objects * @param {...Object} others Any number of param objects * @retuns {Array} params */ replaceParams: function (params) { var others = Array.prototype.slice.call(arguments, 1); function replaceValue(index, key, val) { params[index] = params[index] || {}; params[index][key] = val; } var key; for (var i = 0, _len = others.length; i &lt; _len; i++) { for (key in others[i]) { if (others[i].hasOwnProperty(key)) { replaceValue(i, key, others[i][key]); } } } return params; }, /** * @method * @desc Transforms an array of param objects into a query string. * @param {Array} params An array of param objects * @retuns {String} queryString * @example * Marbles.QueryParams.serializeParams([{a:1, b:2}, {a:3, b:4}]); * //= "?a=1&amp;b=2&amp;a=3&amp;b=4" */ serializeParams: function (params) { var query = []; for (var i = 0, _len = params.length; i &lt; _len; i++) { for (var key in params[i]) { if (params[i].hasOwnProperty(key)) { var val = params[i][key]; if ((typeof val === 'string' &amp;&amp; !val) || val === undefined || val === null) { // ignore empty values continue; } if (typeof val === 'object' &amp;&amp; val.hasOwnProperty('length')) { // encode arrays as comma delemited strings val = val.join(','); } query.push(encodeURIComponent(key) + '=' + encodeURIComponent(val)); } } } return "?" + query.join('&amp;'); } }; __m__["marbles/query_params"].default = QueryParams; })(window.____modules____ = window.____modules____ || {}); (function(__m__) { "use strict"; __m__["marbles/history"] = {}; var Utils = __m__["marbles/utils"].Utils; var Dispatcher = __m__["marbles/dispatcher"].Dispatcher; var QueryParams = __m__["marbles/query_params"].QueryParams; /* * * * * * * * * * * * * * * * * * * * Inspired by Backbone.js History * * * * * * * * * * * * * * * * * * * */ /** * @memberof Marbles * @class * @see Marbles.Router * @desc You should never need to explicitly instantiate this class */ var History = Utils.createClass({ displayName: 'Marbles.History', mixins: [QueryParams], willInitialize: function () { this.started = false; this.handlers = []; this.options = {}; this.path = null; this.prevPath = null; this.handlePopState = this.handlePopState.bind(this); }, // register route handler // handlers are checked in the reverse order // they are defined, so if more than one // matches, only the one defined last will // be called route: function (route, name, callback, paramNames, opts, router) { if (typeof callback !== 'function') { throw new Error(this.constructor.displayName + ".prototype.route(): callback is not a function: "+ JSON.stringify(callback)); } if (typeof route.test !== 'function') { throw new Error(this.constructor.displayName + ".prototype.route(): expected route to be a RegExp: "+ JSON.stringify(route)); } this.handlers.push({ route: route, name: name, paramNames: paramNames, callback: callback, opts: opts, router: router }); }, // navigate to given path via pushState // if available/enabled or by mutation // of window.location.href // // pass options.trigger = false to prevent // route handler from being called // // pass options.replaceState = true to // replace the current history item // // pass options.force = true to force // handler to be called even if path is // already loaded navigate: function (path, options) { if (Marbles.history !== this || !this.started) { throw new Error("Marbles.history has not been started or is set to a different instance"); } if (!options) { options = {}; } if (!options.hasOwnProperty('trigger')) { options.trigger = true; } if (!options.hasOwnProperty('replace')) { options.replace = false; } if (!options.hasOwnProperty('force')) { options.force = false; } if (path[0] === "/") { // trim / prefix path = path.substring(1); } if (options.params) { path = this.pathWithParams(path, options.params); } if (path === this.path &amp;&amp; !options.force) { // we are already there and handler is not forced return; } path = this.pathWithRoot(path); if (!this.options.pushState) { // pushState is unavailable/disabled window.location.href = path; return; } // push or replace state var method = 'pushState'; if (options.replace) { method = 'replaceState'; } window.history[method]({}, document.title, path); if (options.trigger) { // cause route handler to be called this.loadURL({ replace: options.replace }); } }, pathWithParams: function (path, params) { if (params.length === 0) { return path; } // clone params array params = [].concat(params); // we mutate the first param obj, so clone that params[0] = Marbles.Utils.extend({}, params[0]); // expand named params in path path = path.replace(/:([^\/]+)/g, function (m, key) { var paramObj = params[0]; if (paramObj.hasOwnProperty(key)) { var val = paramObj[key]; delete paramObj[key]; return encodeURIComponent(val); } else { return ":"+ key; } }); // add remaining params to query string var queryString = this.serializeParams(params); if (queryString.length > 1) { if (path.indexOf('?') !== -1) { path = path +'&amp;'+ queryString.substring(1); } else { path = path + queryString; } } return path; }, pathWithRoot: function (path) { // add path root if it's not already there var root = this.options.root; if (root &amp;&amp; path.substr(0, root.length) !== root) { if (root.substring(root.length-1) !== '/' &amp;&amp; path.substr(0, 1) !== '/') { // add path seperator if not present in root or path path = '/' + path; } path = root + path; } return path; }, getURLFromPath: function (path, params) { if (params &amp;&amp; params.length !== 0) { path = this.pathWithParams(path, params); } return window.location.protocol +'//'+ window.location.host + this.pathWithRoot(path); }, // start pushState handling start: function (options) { if (Marbles.history &amp;&amp; Marbles.history.started) { throw new Error("Marbles.history has already been started"); } if (!options) { options = {}; } if (!options.hasOwnProperty('trigger')) { options.trigger = true; } if (!Marbles.history) { Marbles.history = this; } this.dispatcher = options.dispatcher || Dispatcher; this.options = Utils.extend({root: '/', pushState: true}, options); this.path = this.getPath(); if (this.options.pushState) { // set pushState to false if it's not supported this.options.pushState = !!(window.history &amp;&amp; window.history.pushState); } if (this.options.pushState) { // init back button binding window.addEventListener('popstate', this.handlePopState, false); } this.started = true; this.trigger('start'); if (options.trigger) { this.loadURL(); } }, // stop pushState handling stop: function () { if (this.options.pushState) { window.removeEventListener('popstate', this.handlePopState, false); } this.started = false; this.trigger('stop'); }, getPath: function () { var path = window.location.pathname; if (window.location.search) { path += window.location.search; } var root = this.options.root.replace(/([^\/])\/$/, '$1'); if (path.indexOf(root) !== -1) { // trim root from path path = path.substr(root.length); } return path.replace(this.constructor.regex.routeStripper, ''); }, handlePopState: function () { this.checkURL(); }, // check if path has changed checkURL: function () { var current = this.getPath(); if (current === this.path) { // path is the same, do nothing return; } this.loadURL(); }, getHandler: function (path) { path = path || this.getPath(); path = path.split('?')[0]; var handler = null; for (var i = 0, _len = this.handlers.length; i &lt; _len; i++) { if (this.handlers[i].route.test(path)) { handler = this.handlers[i]; break; } } return handler; }, // Attempt to find handler for current path // returns matched handler or null loadURL: function (options) { options = options || {}; var prevPath = this.path; var prevParams = this.pathParams; if ( !options.replace ) { this.prevPath = prevPath; this.prevParams = prevParams; } var path = this.path = this.getPath(); var parts = path.match(this.constructor.regex.routeParts); path = parts[1]; var params = this.deserializeParams(parts[2] || ''); this.pathParams = params; var prevHandler; if (this.path !== this.prevPath) { prevHandler = this.getHandler(prevPath); } var handler = this.getHandler(path); var __handlerAbort = false; var handlerAbort = function () { __handlerAbort = true; }; if (prevHandler) { var handlerUnloadEvent = { handler: prevHandler, nextHandler: handler, path: prevPath, nextPath: path, params: prevParams, nextParams: params, abort: handlerAbort }; if (prevHandler.router.beforeHandlerUnlaod) { prevHandler.router.beforeHandlerUnlaod.call(prevHandler.router, handlerUnloadEvent); } if ( !__handlerAbort ) { this.trigger('handler:before-unload', handlerUnloadEvent); } } if (handler &amp;&amp; !__handlerAbort) { var router = handler.router; params = QueryParams.combineParams(params, router.extractNamedParams.call(router, handler.route, path, handler.paramNames)); var event = { handler: handler, path: path, params: params, abort: handlerAbort }; if (handler.router.beforeHandler) { handler.router.beforeHandler.call(handler.router, event); } if ( !__handlerAbort ) { this.trigger('handler:before', event); } if ( !__handlerAbort ) { handler.callback.call(router, params, handler.opts); this.trigger('handler:after', { handler: handler, path: path, params: params }); } } return handler; }, trigger: function (eventName, args) { return this.dispatcher.dispatch(Utils.extend({ source: "Marbles.History", name: eventName }, args)); } }); History.regex = { routeStripper: /^[\/]/, routeParts: /^([^?]*)(?:\?(.*))?$/ // 1: path, 2: params }; /** * @memberof Marbles.History * @func * @param {Object} options * @desc Starts listenening to pushState events and calls route handlers when appropriate * @example * Marbles.History.start({ * root: "/", // if your app is mounted anywhere other than the domain root, enter the path prefix here * pushState: true, // set to `false` in the unlikely event you wish to disable pushState (falls back to manipulating window.location) * dispatcher: Marbles.Dispatcher // The Dispatcher all events are passed to * }) */ History.start = function () { if (!Marbles.history) { Marbles.history = new History(); } return Marbles.history.start.apply(Marbles.history, arguments); }; __m__["marbles/history"].default = History; })(window.____modules____ = window.____modules____ || {}); (function(__m__) { "use strict"; __m__["marbles/router"] = {}; var Utils = __m__["marbles/utils"].Utils; var History = __m__["marbles/history"].History; /* * * * * * * * * * * * * * * * * * * * Inspired by Backbone.js Router * * * * * * * * * * * * * * * * * * * */ /** * @memberof Marbles * @class * @see Marbles.History * @example * var MyRouter = Marbles.Router.createClass({ * displayName: "MyRouter", * * // routes are evaluated in the order they are defined * routes: [ * { path: "posts", handler: "posts" }, * * // :id will be available in the params * { path: "posts/:id", handler: "posts" }, * * // * will be available in the params as `splat` * { path: "posts/:id/*", handler: "posts" }, * ], * * beforeHandler: function (event) { // optional before hook * // same as handler:before event sent through dispatcher * // but only called for the router the handler belongs to * // and called before event is sent through dispatcher * }, * * posts: function (params, opts) { * // params is an array of objects, * // params[0] should be all you need unless * // you have multiple params of the same name * * // opts contains any extra properties given in a route object * } * }); * new MyRouter(); // bring it to life */ var Router = Utils.createClass({ displayName: 'Marbles.Router', willInitialize: function () { this.bindRoutes(); }, navigate: function (path, options) { return Marbles.history.navigate(path, options); }, // register route handler // handler will be called with an array // of param objects, the first of which // will contain any named params route: function (route, handler, opts) { if (!Marbles.history) {