cloak.router
Version:
Cloak.js router module
226 lines (186 loc) • 5.19 kB
JavaScript
var History;
var config = require('cloak.core/config');
var Class = require('cloak.core/utils/class');
var events = require('cloak.core/utils/events');
var EventEmitter = require('cloak.core/utils/eventemitter');
// Create the config module
var conf = config.module('router', {
html4: true,
historyjs: true
});
//
// Parses for variable spots in pathnames
//
var pathVariable = /:([^\/]+)/g;
//
// Router class
//
var Router = module.exports = Class.extend(EventEmitter, {
//
// Initialize
//
init: function() {
var self = this;
// Setup EventEmitter2
EventEmitter.call(this, config.module('core').get('ee'));
this.running = false;
this.routes = Object.keys(this.routes).map(function(route) {
return self._prepareRoute(route, self.routes[route]);
});
if (typeof this.initialize === 'function') {
this.initialize();
}
},
//
// Start the router ...
//
// @return void
//
start: function() {
var self = this;
if (conf.get('historyjs')) {
History = History || require('html5-history');
if (conf.get('html4')) {
require('html5-history/html4');
History.initHtml4();
}
}
if (! this.running) {
this.emit('start');
this.running = true;
if (History) {
History.Adapter.bind(window, 'statechange', this._handleStateChange.bind(this));
} else {
window.addEventListener('popstate', this._handleStateChange.bind(this));
}
// Bind an event to all absolute local anchors in the document to use pushState
// instead of normal navigation
events.select(document).on('click', 'a[href^="/"]', function(evt) {
// Allow anything other than left-click to pass through normally
if ((evt.which == null && evt.which < 2) || (evt.which != null && evt.button < 2)) {
// Allow any click which also involved a modifier key to pass through normally
if (! evt.altKey && ! evt.ctrlKey && ! evt.shiftKey && ! evt.metaKey) {
evt.preventDefault();
self.go(evt.target.getAttribute('href'));
return false;
}
}
});
this.emit('started');
this._handleStateChange();
}
},
//
// Includes a sub-router into this router
//
// @param {SubRouter} the router class to include
// @return this
//
use: function(SubRouter) {
var subRouter = new SubRouter();
// Do this so the sub-router cannot run by itself
subRouter.running = true;
// Steal the sub-router's routes
this.routes.push.apply(this.routes, subRouter.routes);
return this;
},
//
// Redirect the app
//
// @param {to} either a number to traverse the history, or a path to redirect to
// @param {data} optional; when redirecting, this data will be made available to the recieving route function
// @return void
//
go: function(to, data) {
if (typeof to === 'number') {
History ? History.go(to) : history.go(to);
} else {
History
? History.pushState(data || null, null, to)
: history.pushState(data || null, null, to);
}
},
// --------------------------------------------------------
//
// Parse the routes object into regex->function associations
//
// @param {path} the route path string
// @param {funcName} the function name
// @return object
//
_prepareRoute: function(path, funcName) {
if (typeof this[funcName] !== 'function') {
throw new Error('Router: cannot bind URI "' + path + '" to missing method "' + funcName + '"');
}
var result = {
path: path,
func: this[funcName].bind(this),
params: [ ]
};
result.regex = path.replace(pathVariable, function(match, $1) {
result.params.push($1);
return '([^/]+)';
});
pathVariable.lastIndex = 0;
result.regex += (result.regex.slice(-1) === '/') ? '?' : '/?';
result.regex = new RegExp('^' + result.regex + '$');
return result;
},
//
// Runs when the route changes
//
// @return void
//
_handleStateChange: function() {
var state = History ? History.getState() : getState();
var route = this._findRoute(state.hash);
if (! route) {
this.emit('notfound', state);
return;
}
this.emit('redirect', route);
route.func(route.params, state);
},
//
// Find the correct route object for the given path string
//
// @param {path} the path to route to
// @return object
//
_findRoute: function(path) {
path = path.split('?')[0];
path = path.replace('#', '');
for (var i = 0, c = this.routes.length; i < c; i++) {
var route = this.routes[i];
var match = route.regex.exec(path);
if (match) {
return this._prepareMatch(path, route, match);
}
}
},
//
// When a matching route is found, prepare it for use, parsing out params
//
// @param {path} the pathname redirected to
// @param {route} the route object
// @param {match} the matched param values
// @return object
//
_prepareMatch: function(path, route, match) {
var params = { };
route.params.forEach(function(param, index) {
params[param] = match[index + 1] || null;
});
return {
path: path,
func: route.func,
params: params
};
}
});
// -------------------------------------------------------------
function getState() {
return {
hash: location.pathname
};
}