diff --git a/Gruntfile.js b/Gruntfile.js
index 4ad54940f..59a32f577 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -71,6 +71,12 @@ module.exports = function (grunt) {
],
dest: 'dist/widget.js'
},
+ routerJs: {
+ src: [
+ 'src/router/**/*.js'
+ ],
+ dest: 'dist/router.js'
+ },
chartJs: {
src: [
'src/addons/chart/chart.js',
@@ -185,6 +191,12 @@ module.exports = function (grunt) {
],
dest: 'bi/widget.js'
},
+ bi_routerJs: {
+ src: [
+ 'src/router/**/*.js'
+ ],
+ dest: 'bi/router.js'
+ },
bi_chartJs: {
src: [
'src/addons/chart/chart.js',
diff --git a/demo/app.js b/demo/app.js
index cfca022f7..8f4036789 100644
--- a/demo/app.js
+++ b/demo/app.js
@@ -1,12 +1,20 @@
$(function () {
var ref;
- BI.createWidget({
- type: "demo.main",
- ref: function (_ref) {
- console.log(_ref);
- ref = _ref;
+ var AppRouter = BI.inherit(BI.Router, {
+ routes: {
+ "": "index"
},
- element: '#wrapper'
+ index: function () {
+ BI.createWidget({
+ type: "demo.main",
+ ref: function (_ref) {
+ console.log(_ref);
+ ref = _ref;
+ },
+ element: '#wrapper'
+ });
+ }
});
- // ref.destroy();
+ new AppRouter;
+ BI.history.start();
});
\ No newline at end of file
diff --git a/dist/demo.js b/dist/demo.js
index 6c943bcff..a33d8afd6 100644
--- a/dist/demo.js
+++ b/dist/demo.js
@@ -1,14 +1,22 @@
$(function () {
var ref;
- BI.createWidget({
- type: "demo.main",
- ref: function (_ref) {
- console.log(_ref);
- ref = _ref;
+ var AppRouter = BI.inherit(BI.Router, {
+ routes: {
+ "": "index"
},
- element: '#wrapper'
+ index: function () {
+ BI.createWidget({
+ type: "demo.main",
+ ref: function (_ref) {
+ console.log(_ref);
+ ref = _ref;
+ },
+ element: '#wrapper'
+ });
+ }
});
- // ref.destroy();
+ new AppRouter;
+ BI.history.start();
});Demo.Button = BI.inherit(BI.Widget, {
props: {
baseCls: "demo-button"
diff --git a/dist/fix/fix-1.0.js b/dist/fix/fix-1.0.js
index 16ead6ea9..3425bde63 100644
--- a/dist/fix/fix-1.0.js
+++ b/dist/fix/fix-1.0.js
@@ -429,7 +429,7 @@ BI.Factory = {
// You might be wondering why there's a `while` loop here. Changes can
// be recursively nested within `"change"` events.
if (changing) return this;
- changed = BI.clone(this.changed);
+ changed = _.clone(this.changed);
if (!silent) {
while (this._pending) {
options = this._pending;
@@ -1373,438 +1373,6 @@ BI.Factory = {
// Override this if you'd like to use a different library.
BI.ajax = BI.ajax || $.ajax;
- // BI.Router
- // ---------------
-
- // Routers map faux-URLs to actions, and fire events when routes are
- // matched. Creating a new one sets its `routes` hash, if not set statically.
- var Router = BI.Router = function (options) {
- options || (options = {});
- if (options.routes) this.routes = options.routes;
- this._bindRoutes();
- this._init.apply(this, arguments);
- };
-
- // Cached regular expressions for matching named param parts and splatted
- // parts of route strings.
- var optionalParam = /\((.*?)\)/g;
- var namedParam = /(\(\?)?:\w+/g;
- var splatParam = /\*\w+/g;
- var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g;
-
- // Set up all inheritable **BI.Router** properties and methods.
- _.extend(Router.prototype, Events, {
-
- // _init is an empty function by default. Override it with your own
- // initialization logic.
- _init: function () {
- },
-
- // Manually bind a single named route to a callback. For example:
- //
- // this.route('search/:query/p:num', 'search', function(query, num) {
- // ...
- // });
- //
- route: function (route, name, callback) {
- if (!_.isRegExp(route)) route = this._routeToRegExp(route);
- if (_.isFunction(name)) {
- callback = name;
- name = '';
- }
- if (!callback) callback = this[name];
- var router = this;
- BI.history.route(route, function (fragment) {
- var args = router._extractParameters(route, fragment);
- if (router.execute(callback, args, name) !== false) {
- router.trigger.apply(router, ['route:' + name].concat(args));
- router.trigger('route', name, args);
- BI.history.trigger('route', router, name, args);
- }
- });
- return this;
- },
-
- // Execute a route handler with the provided parameters. This is an
- // excellent place to do pre-route setup or post-route cleanup.
- execute: function (callback, args, name) {
- if (callback) callback.apply(this, args);
- },
-
- // Simple proxy to `BI.history` to save a fragment into the history.
- navigate: function (fragment, options) {
- BI.history.navigate(fragment, options);
- return this;
- },
-
- // Bind all defined routes to `BI.history`. We have to reverse the
- // order of the routes here to support behavior where the most general
- // routes can be defined at the bottom of the route map.
- _bindRoutes: function () {
- if (!this.routes) return;
- this.routes = _.result(this, 'routes');
- var route, routes = _.keys(this.routes);
- while ((route = routes.pop()) != null) {
- this.route(route, this.routes[route]);
- }
- },
-
- // Convert a route string into a regular expression, suitable for matching
- // against the current location hash.
- _routeToRegExp: function (route) {
- route = route.replace(escapeRegExp, '\\$&')
- .replace(optionalParam, '(?:$1)?')
- .replace(namedParam, function (match, optional) {
- return optional ? match : '([^/?]+)';
- })
- .replace(splatParam, '([^?]*?)');
- return new RegExp('^' + route + '(?:\\?([\\s\\S]*))?$');
- },
-
- // Given a route, and a URL fragment that it matches, return the array of
- // extracted decoded parameters. Empty or unmatched parameters will be
- // treated as `null` to normalize cross-browser behavior.
- _extractParameters: function (route, fragment) {
- var params = route.exec(fragment).slice(1);
- return _.map(params, function (param, i) {
- // Don't decode the search params.
- if (i === params.length - 1) return param || null;
- return param ? decodeURIComponent(param) : null;
- });
- }
-
- });
-
- // BI.History
- // ----------------
-
- // Handles cross-browser history management, based on either
- // [pushState](http://diveintohtml5.info/history.html) and real URLs, or
- // [onhashchange](https://developer.mozilla.org/en-US/docs/DOM/window.onhashchange)
- // and URL fragments. If the browser supports neither (old IE, natch),
- // falls back to polling.
- var History = BI.History = function () {
- this.handlers = [];
- _.bindAll(this, 'checkUrl');
-
- // Ensure that `History` can be used outside of the browser.
- if (typeof window !== 'undefined') {
- this.location = window.location;
- this.history = window.history;
- }
- };
-
- // Cached regex for stripping a leading hash/slash and trailing space.
- var routeStripper = /^[#\/]|\s+$/g;
-
- // Cached regex for stripping leading and trailing slashes.
- var rootStripper = /^\/+|\/+$/g;
-
- // Cached regex for stripping urls of hash.
- var pathStripper = /#.*$/;
-
- // Has the history handling already been started?
- History.started = false;
-
- // Set up all inheritable **BI.History** properties and methods.
- _.extend(History.prototype, Events, {
-
- // The default interval to poll for hash changes, if necessary, is
- // twenty times a second.
- interval: 50,
-
- // Are we at the app root?
- atRoot: function () {
- var path = this.location.pathname.replace(/[^\/]$/, '$&/');
- return path === this.root && !this.getSearch();
- },
-
- // In IE6, the hash fragment and search params are incorrect if the
- // fragment contains `?`.
- getSearch: function () {
- var match = this.location.href.replace(/#.*/, '').match(/\?.+/);
- return match ? match[0] : '';
- },
-
- // Gets the true hash value. Cannot use location.hash directly due to bug
- // in Firefox where location.hash will always be decoded.
- getHash: function (window) {
- var match = (window || this).location.href.match(/#(.*)$/);
- return match ? match[1] : '';
- },
-
- // Get the pathname and search params, without the root.
- getPath: function () {
- var path = decodeURI(this.location.pathname + this.getSearch());
- var root = this.root.slice(0, -1);
- if (!path.indexOf(root)) path = path.slice(root.length);
- return path.charAt(0) === '/' ? path.slice(1) : path;
- },
-
- // Get the cross-browser normalized URL fragment from the path or hash.
- getFragment: function (fragment) {
- if (fragment == null) {
- if (this._hasPushState || !this._wantsHashChange) {
- fragment = this.getPath();
- } else {
- fragment = this.getHash();
- }
- }
- return fragment.replace(routeStripper, '');
- },
-
- // Start the hash change handling, returning `true` if the current URL matches
- // an existing route, and `false` otherwise.
- start: function (options) {
- if (History.started) throw new Error('BI.history has already been started');
- History.started = true;
-
- // Figure out the initial configuration. Do we need an iframe?
- // Is pushState desired ... is it available?
- this.options = _.extend({root: '/'}, this.options, options);
- this.root = this.options.root;
- this._wantsHashChange = this.options.hashChange !== false;
- this._hasHashChange = 'onhashchange' in window;
- this._wantsPushState = !!this.options.pushState;
- this._hasPushState = !!(this.options.pushState && this.history && this.history.pushState);
- this.fragment = this.getFragment();
-
- // Normalize root to always include a leading and trailing slash.
- this.root = ('/' + this.root + '/').replace(rootStripper, '/');
-
- // Transition from hashChange to pushState or vice versa if both are
- // requested.
- if (this._wantsHashChange && this._wantsPushState) {
-
- // If we've started off with a route from a `pushState`-enabled
- // browser, but we're currently in a browser that doesn't support it...
- if (!this._hasPushState && !this.atRoot()) {
- var root = this.root.slice(0, -1) || '/';
- this.location.replace(root + '#' + this.getPath());
- // Return immediately as browser will do redirect to new url
- return true;
-
- // Or if we've started out with a hash-based route, but we're currently
- // in a browser where it could be `pushState`-based instead...
- } else if (this._hasPushState && this.atRoot()) {
- this.navigate(this.getHash(), {replace: true});
- }
-
- }
-
- // Proxy an iframe to handle location events if the browser doesn't
- // support the `hashchange` event, HTML5 history, or the user wants
- // `hashChange` but not `pushState`.
- if (!this._hasHashChange && this._wantsHashChange && (!this._wantsPushState || !this._hasPushState)) {
- var iframe = document.createElement('iframe');
- iframe.src = 'javascript:0';
- iframe.style.display = 'none';
- iframe.tabIndex = -1;
- var body = document.body;
- // Using `appendChild` will throw on IE < 9 if the document is not ready.
- this.iframe = body.insertBefore(iframe, body.firstChild).contentWindow;
- this.iframe.document.open().close();
- this.iframe.location.hash = '#' + this.fragment;
- }
-
- // Add a cross-platform `addEventListener` shim for older browsers.
- var addEventListener = window.addEventListener || function (eventName, listener) {
- return attachEvent('on' + eventName, listener);
- };
-
- // Depending on whether we're using pushState or hashes, and whether
- // 'onhashchange' is supported, determine how we check the URL state.
- if (this._hasPushState) {
- addEventListener('popstate', this.checkUrl, false);
- } else if (this._wantsHashChange && this._hasHashChange && !this.iframe) {
- addEventListener('hashchange', this.checkUrl, false);
- } else if (this._wantsHashChange) {
- this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
- }
-
- if (!this.options.silent) return this.loadUrl();
- },
-
- // Disable BI.history, perhaps temporarily. Not useful in a real app,
- // but possibly useful for unit testing Routers.
- stop: function () {
- // Add a cross-platform `removeEventListener` shim for older browsers.
- var removeEventListener = window.removeEventListener || function (eventName, listener) {
- return detachEvent('on' + eventName, listener);
- };
-
- // Remove window listeners.
- if (this._hasPushState) {
- removeEventListener('popstate', this.checkUrl, false);
- } else if (this._wantsHashChange && this._hasHashChange && !this.iframe) {
- removeEventListener('hashchange', this.checkUrl, false);
- }
-
- // Clean up the iframe if necessary.
- if (this.iframe) {
- document.body.removeChild(this.iframe.frameElement);
- this.iframe = null;
- }
-
- // Some environments will throw when clearing an undefined interval.
- if (this._checkUrlInterval) clearInterval(this._checkUrlInterval);
- History.started = false;
- },
-
- // Add a route to be tested when the fragment changes. Routes added later
- // may override previous routes.
- route: function (route, callback) {
- this.handlers.unshift({route: route, callback: callback});
- },
-
- // Checks the current URL to see if it has changed, and if it has,
- // calls `loadUrl`, normalizing across the hidden iframe.
- checkUrl: function (e) {
- var current = this.getFragment();
-
- // If the user pressed the back button, the iframe's hash will have
- // changed and we should use that for comparison.
- if (current === this.fragment && this.iframe) {
- current = this.getHash(this.iframe);
- }
-
- if (current === this.fragment) return false;
- if (this.iframe) this.navigate(current);
- this.loadUrl();
- },
-
- // Attempt to load the current URL fragment. If a route succeeds with a
- // match, returns `true`. If no defined routes matches the fragment,
- // returns `false`.
- loadUrl: function (fragment) {
- fragment = this.fragment = this.getFragment(fragment);
- return _.any(this.handlers, function (handler) {
- if (handler.route.test(fragment)) {
- handler.callback(fragment);
- return true;
- }
- });
- },
-
- // Save a fragment into the hash history, or replace the URL state if the
- // 'replace' option is passed. You are responsible for properly URL-encoding
- // the fragment in advance.
- //
- // The options object can contain `trigger: true` if you wish to have the
- // route callback be fired (not usually desirable), or `replace: true`, if
- // you wish to modify the current URL without adding an entry to the history.
- navigate: function (fragment, options) {
- if (!History.started) return false;
- if (!options || options === true) options = {trigger: !!options};
-
- // Normalize the fragment.
- fragment = this.getFragment(fragment || '');
-
- // Don't include a trailing slash on the root.
- var root = this.root;
- if (fragment === '' || fragment.charAt(0) === '?') {
- root = root.slice(0, -1) || '/';
- }
- var url = root + fragment;
-
- // Strip the hash and decode for matching.
- fragment = decodeURI(fragment.replace(pathStripper, ''));
-
- if (this.fragment === fragment) return;
- this.fragment = fragment;
-
- // If pushState is available, we use it to set the fragment as a real URL.
- if (this._hasPushState) {
- this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url);
-
- // If hash changes haven't been explicitly disabled, update the hash
- // fragment to store history.
- } else if (this._wantsHashChange) {
- this._updateHash(this.location, fragment, options.replace);
- if (this.iframe && (fragment !== this.getHash(this.iframe))) {
- // Opening and closing the iframe tricks IE7 and earlier to push a
- // history entry on hash-tag change. When replace is true, we don't
- // want this.
- if (!options.replace) this.iframe.document.open().close();
- this._updateHash(this.iframe.location, fragment, options.replace);
- }
-
- // If you've told us that you explicitly don't want fallback hashchange-
- // based history, then `navigate` becomes a page refresh.
- } else {
- return this.location.assign(url);
- }
- if (options.trigger) return this.loadUrl(fragment);
- },
-
- // Update the hash location, either replacing the current entry, or adding
- // a new one to the browser history.
- _updateHash: function (location, fragment, replace) {
- if (replace) {
- var href = location.href.replace(/(javascript:|#).*$/, '');
- location.replace(href + '#' + fragment);
- } else {
- // Some browsers require that `hash` contains a leading #.
- location.hash = '#' + fragment;
- }
- }
-
- });
-
- // Create the default BI.history.
- BI.history = new History;
-
- // Helpers
- // -------
-
- // Helper function to correctly set up the prototype chain, for subclasses.
- // Similar to `goog.inherits`, but uses a hash of prototype properties and
- // class properties to be extended.
- var extend = function (protoProps, staticProps) {
- var parent = this;
- var child;
-
- // The constructor function for the new subclass is either defined by you
- // (the "constructor" property in your `extend` definition), or defaulted
- // by us to simply call the parent's constructor.
- if (protoProps && _.has(protoProps, 'constructor')) {
- child = protoProps.constructor;
- } else {
- child = function () {
- return parent.apply(this, arguments);
- };
- }
-
- // Add static properties to the constructor function, if supplied.
- _.extend(child, parent, staticProps);
-
- // Set the prototype chain to inherit from `parent`, without calling
- // `parent`'s constructor function.
- var Surrogate = function () {
- this.constructor = child;
- };
- Surrogate.prototype = parent.prototype;
- child.prototype = new Surrogate;
-
- // Add prototype properties (instance properties) to the subclass,
- // if supplied.
- if (protoProps) _.extend(child.prototype, protoProps);
-
- // Set a convenience property in case the parent's prototype is needed
- // later.
- child.__super__ = parent.prototype;
-
- return child;
- };
-
- // Set up inheritance for the model, collection, router, view and history.
- M.extend = Collection.extend = Router.extend = V.extend = History.extend = extend;
-
- // Throw an error when a URL is needed, and none is supplied.
- var urlError = function () {
- throw new Error('A "url" property or function must be specified');
- };
-
// Wrap an optional error callback with a fallback error event.
var wrapError = function (model, options) {
var error = options.error;
@@ -1822,13 +1390,13 @@ BI.Factory = {
* @extends BI.Router
* @type {*|void|Object}
*/
-BI.WRouter = BI.Router.extend({
- add: function(route, callback){
- this.handlers || (this.handlers=[]);
+BI.WRouter = BI.inherit(BI.Router, {
+ add: function (route, callback) {
+ this.handlers || (this.handlers = []);
this.handlers.unshift({route: route, callback: callback})
},
- route: function(route, name, callback) {
+ route: function (route, name, callback) {
if (!_.isRegExp(route)) route = this._routeToRegExp(route);
if (_.isFunction(name)) {
callback = name;
@@ -1836,7 +1404,7 @@ BI.WRouter = BI.Router.extend({
}
if (!callback) callback = this[name];
var self = this;
- this.add(route, function(fragment) {
+ this.add(route, function (fragment) {
var args = self._extractParameters(route, fragment);
var result = self.execute(callback, args, name)
if (result !== false) {
@@ -1848,14 +1416,14 @@ BI.WRouter = BI.Router.extend({
return this;
},
- execute: function(callback, args, name) {
+ execute: function (callback, args, name) {
if (callback) return callback.apply(this, args);
return name;
},
- get: function(fragment){
+ get: function (fragment) {
var result = null;
- _.any(this.handlers, function(handler) {
+ _.any(this.handlers, function (handler) {
if (handler.route.test(fragment)) {
result = handler.callback(fragment);
return true;
@@ -3212,4 +2780,6 @@ BI.FloatBoxRouter = BI.inherit(BI.WRouter, {
}
return this;
}
-});
\ No newline at end of file
+});BI.Fix = {
+ version: "1.0"
+}
\ No newline at end of file
diff --git a/dist/index.html b/dist/index.html
index 891b3e5b1..3a66e0ed1 100644
--- a/dist/index.html
+++ b/dist/index.html
@@ -19,7 +19,9 @@
-
+
+
+
diff --git a/src/router/router.js b/src/router/router.js
new file mode 100644
index 000000000..4ee73d096
--- /dev/null
+++ b/src/router/router.js
@@ -0,0 +1,585 @@
+;(function(){
+ var Events = {
+
+ // Bind an event to a `callback` function. Passing `"all"` will bind
+ // the callback to all events fired.
+ on: function (name, callback, context) {
+ if (!eventsApi(this, 'on', name, [callback, context]) || !callback) return this;
+ this._events || (this._events = {});
+ var events = this._events[name] || (this._events[name] = []);
+ events.push({callback: callback, context: context, ctx: context || this});
+ return this;
+ },
+
+ // Bind an event to only be triggered a single time. After the first time
+ // the callback is invoked, it will be removed.
+ once: function (name, callback, context) {
+ if (!eventsApi(this, 'once', name, [callback, context]) || !callback) return this;
+ var self = this;
+ var once = _.once(function () {
+ self.off(name, once);
+ callback.apply(this, arguments);
+ });
+ once._callback = callback;
+ return this.on(name, once, context);
+ },
+
+ // Remove one or many callbacks. If `context` is null, removes all
+ // callbacks with that function. If `callback` is null, removes all
+ // callbacks for the event. If `name` is null, removes all bound
+ // callbacks for all events.
+ off: function (name, callback, context) {
+ if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this;
+
+ // Remove all callbacks for all events.
+ if (!name && !callback && !context) {
+ this._events = void 0;
+ return this;
+ }
+
+ var names = name ? [name] : _.keys(this._events);
+ for (var i = 0, length = names.length; i < length; i++) {
+ name = names[i];
+
+ // Bail out if there are no events stored.
+ var events = this._events[name];
+ if (!events) continue;
+
+ // Remove all callbacks for this event.
+ if (!callback && !context) {
+ delete this._events[name];
+ continue;
+ }
+
+ // Find any remaining events.
+ var remaining = [];
+ for (var j = 0, k = events.length; j < k; j++) {
+ var event = events[j];
+ if (
+ callback && callback !== event.callback &&
+ callback !== event.callback._callback ||
+ context && context !== event.context
+ ) {
+ remaining.push(event);
+ }
+ }
+
+ // Replace events if there are any remaining. Otherwise, clean up.
+ if (remaining.length) {
+ this._events[name] = remaining;
+ } else {
+ delete this._events[name];
+ }
+ }
+
+ return this;
+ },
+
+ un: function () {
+ this.off.apply(this, arguments);
+ },
+
+ // Trigger one or many events, firing all bound callbacks. Callbacks are
+ // passed the same arguments as `trigger` is, apart from the event name
+ // (unless you're listening on `"all"`, which will cause your callback to
+ // receive the true name of the event as the first argument).
+ trigger: function (name) {
+ if (!this._events) return this;
+ var args = slice.call(arguments, 1);
+ if (!eventsApi(this, 'trigger', name, args)) return this;
+ var events = this._events[name];
+ var allEvents = this._events.all;
+ if (events) triggerEvents(events, args);
+ if (allEvents) triggerEvents(allEvents, arguments);
+ return this;
+ },
+
+ fireEvent: function () {
+ this.trigger.apply(this, arguments);
+ },
+
+ // Inversion-of-control versions of `on` and `once`. Tell *this* object to
+ // listen to an event in another object ... keeping track of what it's
+ // listening to.
+ listenTo: function (obj, name, callback) {
+ var listeningTo = this._listeningTo || (this._listeningTo = {});
+ var id = obj._listenId || (obj._listenId = _.uniqueId('l'));
+ listeningTo[id] = obj;
+ if (!callback && typeof name === 'object') callback = this;
+ obj.on(name, callback, this);
+ return this;
+ },
+
+ listenToOnce: function (obj, name, callback) {
+ if (typeof name === 'object') {
+ for (var event in name) this.listenToOnce(obj, event, name[event]);
+ return this;
+ }
+ if (eventSplitter.test(name)) {
+ var names = name.split(eventSplitter);
+ for (var i = 0, length = names.length; i < length; i++) {
+ this.listenToOnce(obj, names[i], callback);
+ }
+ return this;
+ }
+ if (!callback) return this;
+ var once = _.once(function () {
+ this.stopListening(obj, name, once);
+ callback.apply(this, arguments);
+ });
+ once._callback = callback;
+ return this.listenTo(obj, name, once);
+ },
+
+ // Tell this object to stop listening to either specific events ... or
+ // to every object it's currently listening to.
+ stopListening: function (obj, name, callback) {
+ var listeningTo = this._listeningTo;
+ if (!listeningTo) return this;
+ var remove = !name && !callback;
+ if (!callback && typeof name === 'object') callback = this;
+ if (obj) (listeningTo = {})[obj._listenId] = obj;
+ for (var id in listeningTo) {
+ obj = listeningTo[id];
+ obj.off(name, callback, this);
+ if (remove || _.isEmpty(obj._events)) delete this._listeningTo[id];
+ }
+ return this;
+ }
+
+ };
+
+ // Regular expression used to split event strings.
+ var eventSplitter = /\s+/;
+
+ // Implement fancy features of the Events API such as multiple event
+ // names `"change blur"` and jQuery-style event maps `{change: action}`
+ // in terms of the existing API.
+ var eventsApi = function (obj, action, name, rest) {
+ if (!name) return true;
+
+ // Handle event maps.
+ if (typeof name === 'object') {
+ for (var key in name) {
+ obj[action].apply(obj, [key, name[key]].concat(rest));
+ }
+ return false;
+ }
+
+ // Handle space separated event names.
+ if (eventSplitter.test(name)) {
+ var names = name.split(eventSplitter);
+ for (var i = 0, length = names.length; i < length; i++) {
+ obj[action].apply(obj, [names[i]].concat(rest));
+ }
+ return false;
+ }
+
+ return true;
+ };
+
+ // A difficult-to-believe, but optimized internal dispatch function for
+ // triggering events. Tries to keep the usual cases speedy (most internal
+ // BI events have 3 arguments).
+ var triggerEvents = function (events, args) {
+ var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2];
+ switch (args.length) {
+ case 0:
+ while (++i < l) (ev = events[i]).callback.call(ev.ctx);
+ return;
+ case 1:
+ while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1);
+ return;
+ case 2:
+ while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2);
+ return;
+ case 3:
+ while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3);
+ return;
+ default:
+ while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args);
+ return;
+ }
+ };
+
+ // BI.Router
+ // ---------------
+
+ // Routers map faux-URLs to actions, and fire events when routes are
+ // matched. Creating a new one sets its `routes` hash, if not set statically.
+ var Router = BI.Router = function (options) {
+ options || (options = {});
+ if (options.routes) this.routes = options.routes;
+ this._bindRoutes();
+ this._init.apply(this, arguments);
+ };
+
+ // Cached regular expressions for matching named param parts and splatted
+ // parts of route strings.
+ var optionalParam = /\((.*?)\)/g;
+ var namedParam = /(\(\?)?:\w+/g;
+ var splatParam = /\*\w+/g;
+ var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g;
+
+ // Set up all inheritable **BI.Router** properties and methods.
+ _.extend(Router.prototype, Events, {
+
+ // _init is an empty function by default. Override it with your own
+ // initialization logic.
+ _init: function () {
+ },
+
+ // Manually bind a single named route to a callback. For example:
+ //
+ // this.route('search/:query/p:num', 'search', function(query, num) {
+ // ...
+ // });
+ //
+ route: function (route, name, callback) {
+ if (!_.isRegExp(route)) route = this._routeToRegExp(route);
+ if (_.isFunction(name)) {
+ callback = name;
+ name = '';
+ }
+ if (!callback) callback = this[name];
+ var router = this;
+ BI.history.route(route, function (fragment) {
+ var args = router._extractParameters(route, fragment);
+ if (router.execute(callback, args, name) !== false) {
+ router.trigger.apply(router, ['route:' + name].concat(args));
+ router.trigger('route', name, args);
+ BI.history.trigger('route', router, name, args);
+ }
+ });
+ return this;
+ },
+
+ // Execute a route handler with the provided parameters. This is an
+ // excellent place to do pre-route setup or post-route cleanup.
+ execute: function (callback, args, name) {
+ if (callback) callback.apply(this, args);
+ },
+
+ // Simple proxy to `BI.history` to save a fragment into the history.
+ navigate: function (fragment, options) {
+ BI.history.navigate(fragment, options);
+ return this;
+ },
+
+ // Bind all defined routes to `BI.history`. We have to reverse the
+ // order of the routes here to support behavior where the most general
+ // routes can be defined at the bottom of the route map.
+ _bindRoutes: function () {
+ if (!this.routes) return;
+ this.routes = _.result(this, 'routes');
+ var route, routes = _.keys(this.routes);
+ while ((route = routes.pop()) != null) {
+ this.route(route, this.routes[route]);
+ }
+ },
+
+ // Convert a route string into a regular expression, suitable for matching
+ // against the current location hash.
+ _routeToRegExp: function (route) {
+ route = route.replace(escapeRegExp, '\\$&')
+ .replace(optionalParam, '(?:$1)?')
+ .replace(namedParam, function (match, optional) {
+ return optional ? match : '([^/?]+)';
+ })
+ .replace(splatParam, '([^?]*?)');
+ return new RegExp('^' + route + '(?:\\?([\\s\\S]*))?$');
+ },
+
+ // Given a route, and a URL fragment that it matches, return the array of
+ // extracted decoded parameters. Empty or unmatched parameters will be
+ // treated as `null` to normalize cross-browser behavior.
+ _extractParameters: function (route, fragment) {
+ var params = route.exec(fragment).slice(1);
+ return _.map(params, function (param, i) {
+ // Don't decode the search params.
+ if (i === params.length - 1) return param || null;
+ return param ? decodeURIComponent(param) : null;
+ });
+ }
+
+ });
+
+ // History
+ // ----------------
+
+ // Handles cross-browser history management, based on either
+ // [pushState](http://diveintohtml5.info/history.html) and real URLs, or
+ // [onhashchange](https://developer.mozilla.org/en-US/docs/DOM/window.onhashchange)
+ // and URL fragments. If the browser supports neither (old IE, natch),
+ // falls back to polling.
+ var History = function () {
+ this.handlers = [];
+ _.bindAll(this, 'checkUrl');
+
+ // Ensure that `History` can be used outside of the browser.
+ if (typeof window !== 'undefined') {
+ this.location = window.location;
+ this.history = window.history;
+ }
+ };
+
+ // Cached regex for stripping a leading hash/slash and trailing space.
+ var routeStripper = /^[#\/]|\s+$/g;
+
+ // Cached regex for stripping leading and trailing slashes.
+ var rootStripper = /^\/+|\/+$/g;
+
+ // Cached regex for stripping urls of hash.
+ var pathStripper = /#.*$/;
+
+ // Has the history handling already been started?
+ History.started = false;
+
+ // Set up all inheritable **BI.History** properties and methods.
+ _.extend(History.prototype, Events, {
+
+ // The default interval to poll for hash changes, if necessary, is
+ // twenty times a second.
+ interval: 50,
+
+ // Are we at the app root?
+ atRoot: function () {
+ var path = this.location.pathname.replace(/[^\/]$/, '$&/');
+ return path === this.root && !this.getSearch();
+ },
+
+ // In IE6, the hash fragment and search params are incorrect if the
+ // fragment contains `?`.
+ getSearch: function () {
+ var match = this.location.href.replace(/#.*/, '').match(/\?.+/);
+ return match ? match[0] : '';
+ },
+
+ // Gets the true hash value. Cannot use location.hash directly due to bug
+ // in Firefox where location.hash will always be decoded.
+ getHash: function (window) {
+ var match = (window || this).location.href.match(/#(.*)$/);
+ return match ? match[1] : '';
+ },
+
+ // Get the pathname and search params, without the root.
+ getPath: function () {
+ var path = decodeURI(this.location.pathname + this.getSearch());
+ var root = this.root.slice(0, -1);
+ if (!path.indexOf(root)) path = path.slice(root.length);
+ return path.charAt(0) === '/' ? path.slice(1) : path;
+ },
+
+ // Get the cross-browser normalized URL fragment from the path or hash.
+ getFragment: function (fragment) {
+ if (fragment == null) {
+ if (this._hasPushState || !this._wantsHashChange) {
+ fragment = this.getPath();
+ } else {
+ fragment = this.getHash();
+ }
+ }
+ return fragment.replace(routeStripper, '');
+ },
+
+ // Start the hash change handling, returning `true` if the current URL matches
+ // an existing route, and `false` otherwise.
+ start: function (options) {
+ if (History.started) throw new Error('BI.history has already been started');
+ History.started = true;
+
+ // Figure out the initial configuration. Do we need an iframe?
+ // Is pushState desired ... is it available?
+ this.options = _.extend({root: '/'}, this.options, options);
+ this.root = this.options.root;
+ this._wantsHashChange = this.options.hashChange !== false;
+ this._hasHashChange = 'onhashchange' in window;
+ this._wantsPushState = !!this.options.pushState;
+ this._hasPushState = !!(this.options.pushState && this.history && this.history.pushState);
+ this.fragment = this.getFragment();
+
+ // Normalize root to always include a leading and trailing slash.
+ this.root = ('/' + this.root + '/').replace(rootStripper, '/');
+
+ // Transition from hashChange to pushState or vice versa if both are
+ // requested.
+ if (this._wantsHashChange && this._wantsPushState) {
+
+ // If we've started off with a route from a `pushState`-enabled
+ // browser, but we're currently in a browser that doesn't support it...
+ if (!this._hasPushState && !this.atRoot()) {
+ var root = this.root.slice(0, -1) || '/';
+ this.location.replace(root + '#' + this.getPath());
+ // Return immediately as browser will do redirect to new url
+ return true;
+
+ // Or if we've started out with a hash-based route, but we're currently
+ // in a browser where it could be `pushState`-based instead...
+ } else if (this._hasPushState && this.atRoot()) {
+ this.navigate(this.getHash(), {replace: true});
+ }
+
+ }
+
+ // Proxy an iframe to handle location events if the browser doesn't
+ // support the `hashchange` event, HTML5 history, or the user wants
+ // `hashChange` but not `pushState`.
+ if (!this._hasHashChange && this._wantsHashChange && (!this._wantsPushState || !this._hasPushState)) {
+ var iframe = document.createElement('iframe');
+ iframe.src = 'javascript:0';
+ iframe.style.display = 'none';
+ iframe.tabIndex = -1;
+ var body = document.body;
+ // Using `appendChild` will throw on IE < 9 if the document is not ready.
+ this.iframe = body.insertBefore(iframe, body.firstChild).contentWindow;
+ this.iframe.document.open().close();
+ this.iframe.location.hash = '#' + this.fragment;
+ }
+
+ // Add a cross-platform `addEventListener` shim for older browsers.
+ var addEventListener = window.addEventListener || function (eventName, listener) {
+ return attachEvent('on' + eventName, listener);
+ };
+
+ // Depending on whether we're using pushState or hashes, and whether
+ // 'onhashchange' is supported, determine how we check the URL state.
+ if (this._hasPushState) {
+ addEventListener('popstate', this.checkUrl, false);
+ } else if (this._wantsHashChange && this._hasHashChange && !this.iframe) {
+ addEventListener('hashchange', this.checkUrl, false);
+ } else if (this._wantsHashChange) {
+ this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
+ }
+
+ if (!this.options.silent) return this.loadUrl();
+ },
+
+ // Disable BI.history, perhaps temporarily. Not useful in a real app,
+ // but possibly useful for unit testing Routers.
+ stop: function () {
+ // Add a cross-platform `removeEventListener` shim for older browsers.
+ var removeEventListener = window.removeEventListener || function (eventName, listener) {
+ return detachEvent('on' + eventName, listener);
+ };
+
+ // Remove window listeners.
+ if (this._hasPushState) {
+ removeEventListener('popstate', this.checkUrl, false);
+ } else if (this._wantsHashChange && this._hasHashChange && !this.iframe) {
+ removeEventListener('hashchange', this.checkUrl, false);
+ }
+
+ // Clean up the iframe if necessary.
+ if (this.iframe) {
+ document.body.removeChild(this.iframe.frameElement);
+ this.iframe = null;
+ }
+
+ // Some environments will throw when clearing an undefined interval.
+ if (this._checkUrlInterval) clearInterval(this._checkUrlInterval);
+ History.started = false;
+ },
+
+ // Add a route to be tested when the fragment changes. Routes added later
+ // may override previous routes.
+ route: function (route, callback) {
+ this.handlers.unshift({route: route, callback: callback});
+ },
+
+ // Checks the current URL to see if it has changed, and if it has,
+ // calls `loadUrl`, normalizing across the hidden iframe.
+ checkUrl: function (e) {
+ var current = this.getFragment();
+
+ // If the user pressed the back button, the iframe's hash will have
+ // changed and we should use that for comparison.
+ if (current === this.fragment && this.iframe) {
+ current = this.getHash(this.iframe);
+ }
+
+ if (current === this.fragment) return false;
+ if (this.iframe) this.navigate(current);
+ this.loadUrl();
+ },
+
+ // Attempt to load the current URL fragment. If a route succeeds with a
+ // match, returns `true`. If no defined routes matches the fragment,
+ // returns `false`.
+ loadUrl: function (fragment) {
+ fragment = this.fragment = this.getFragment(fragment);
+ return _.any(this.handlers, function (handler) {
+ if (handler.route.test(fragment)) {
+ handler.callback(fragment);
+ return true;
+ }
+ });
+ },
+
+ // Save a fragment into the hash history, or replace the URL state if the
+ // 'replace' option is passed. You are responsible for properly URL-encoding
+ // the fragment in advance.
+ //
+ // The options object can contain `trigger: true` if you wish to have the
+ // route callback be fired (not usually desirable), or `replace: true`, if
+ // you wish to modify the current URL without adding an entry to the history.
+ navigate: function (fragment, options) {
+ if (!History.started) return false;
+ if (!options || options === true) options = {trigger: !!options};
+
+ // Normalize the fragment.
+ fragment = this.getFragment(fragment || '');
+
+ // Don't include a trailing slash on the root.
+ var root = this.root;
+ if (fragment === '' || fragment.charAt(0) === '?') {
+ root = root.slice(0, -1) || '/';
+ }
+ var url = root + fragment;
+
+ // Strip the hash and decode for matching.
+ fragment = decodeURI(fragment.replace(pathStripper, ''));
+
+ if (this.fragment === fragment) return;
+ this.fragment = fragment;
+
+ // If pushState is available, we use it to set the fragment as a real URL.
+ if (this._hasPushState) {
+ this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url);
+
+ // If hash changes haven't been explicitly disabled, update the hash
+ // fragment to store history.
+ } else if (this._wantsHashChange) {
+ this._updateHash(this.location, fragment, options.replace);
+ if (this.iframe && (fragment !== this.getHash(this.iframe))) {
+ // Opening and closing the iframe tricks IE7 and earlier to push a
+ // history entry on hash-tag change. When replace is true, we don't
+ // want this.
+ if (!options.replace) this.iframe.document.open().close();
+ this._updateHash(this.iframe.location, fragment, options.replace);
+ }
+
+ // If you've told us that you explicitly don't want fallback hashchange-
+ // based history, then `navigate` becomes a page refresh.
+ } else {
+ return this.location.assign(url);
+ }
+ if (options.trigger) return this.loadUrl(fragment);
+ },
+
+ // Update the hash location, either replacing the current entry, or adding
+ // a new one to the browser history.
+ _updateHash: function (location, fragment, replace) {
+ if (replace) {
+ var href = location.href.replace(/(javascript:|#).*$/, '');
+ location.replace(href + '#' + fragment);
+ } else {
+ // Some browsers require that `hash` contains a leading #.
+ location.hash = '#' + fragment;
+ }
+ }
+
+ });
+
+ // Create the default BI.history.
+ BI.history = new History;
+}());
\ No newline at end of file