Skip to content

Instantly share code, notes, and snippets.

@indexzero
Created September 26, 2011 19:19
Show Gist options
  • Save indexzero/1243112 to your computer and use it in GitHub Desktop.
Save indexzero/1243112 to your computer and use it in GitHub Desktop.
Comments in SugarSkull
(function(window, undefined) {
var dloc = document.location;
function regifyString(str) {
if(~str.indexOf('*')) {
str = str.replace(/\*/g, '([_\.\(\)!\\ %@&a-zA-Z0-9-]+)');
}
if(~str.indexOf(':')) {
str = str.replace(/:.*?\/|:.*?$/g, '([a-zA-Z0-9-]+)/');
str = str.slice(0, -1);
}
return str;
}
function regifyObject(routes) { // convert all simple param routes to regex
if (typeof routes === 'string') {
return false;
}
for (var key in routes) {
regifyObject(routes[key]);
if (key.indexOf(':') !== -1 || key.indexOf('*') !== -1) {
var newKey = regifyString(key);
routes[newKey] = routes[key];
delete routes[key];
}
}
}
window.Router = Router;
function Router(routes) {
if(!(this instanceof Router)) return new Router(routes);
var self = this;
this.routes = routes;
var add;
this.recurse = function(value) {
if (value === undefined) return recurse;
add = (this._recurse = value) === 'forward' ? 'unshift' : 'push';
};
this._recurse = null;
this.recurse(null);
this.resource = null;
this.state = {};
this.events = {};
this.events.on = [];
this.events.oneach = [];
this.events.after = [];
this.events.aftereach = [];
this.events.once = [];
this.events.notfound = null;
this.route = null;
this.lastroutevalue = null;
this.alreadyrun = false;
regifyObject(this.routes);
function dispatch(src) {
for (var i=0, l = self.events[src].length; i < l; i++) {
var listener = self.events[src][i];
var val = listener.val === null ? self.lastroutevalue : listener.val;
if (typeof listener.fn === 'string') {
listener.fn = self.resource[listener.fn];
}
if (typeof val === 'string') {
val = [val];
}
if (listener.fn.apply(self.resource || self, val || []) === false) {
self[src] = [];
return false;
}
if (listener.val !== null) {
self.lastroutevalue = listener.val;
}
}
}
function parse(routes, path, len, matched) {
var roughmatch, exactmatch, _len;
var route = routes[path];
if(!route || path === '/') {
for (var r in routes) {
//
// We dont have an exact match, lets explore.
// e.g. r: `/foo/bar`, path: `/foo/bar/buzz`
//
// '/foo/bar': {
// '/buzz': function() {}
// }
//
if (routes.hasOwnProperty(r)) {
//
// exactmatch: ['/foo/bar']
// roughmatch: ['/foo/bar/buzz', '/buzz']
//
exactmatch = path.match(new RegExp('^' + r));
roughmatch = path.match(new RegExp('^' + r + '(.*)?'));
if (exactmatch) {
//
// Convert roughmatch to an array of names without
// the **leading** `/`.
//
// roughmatch: ['foo/bar/buzz', 'buzz']
//
for (var i = roughmatch.length; i >= 0; i--) {
if (roughmatch[i]) {
roughmatch[i] = roughmatch[i].replace(/^\//, '');
}
else {
roughmatch.splice(i, 1);
}
}
//
// If we are at the toplevel (e.g. `/`) and there is a more
// liberal route available then do not continue
// evaluation of this branch of the routing table.
//
if (exactmatch[0] === '/' &&
roughmatch.length > 1 &&
'/([a-zA-Z0-9-]+)' in routes) {
continue;
}
//
// partsCount: 1
// _path: buzz
// _len: [3] - 1 = 2
// _matched: [].concat([]) = []
//
var partsCount = exactmatch[0].split('/').length - 1,
_path = roughmatch.slice(exactmatch.length),
_matched = matched.concat(exactmatch.slice(1));
_len = len - partsCount;
if (exactmatch.length > 1) {
//
// If we have capture groups in the string `r`.
// e.g. `/(\w+)\/bar/`
//
route = routes[r];
}
else {
//
// Otherwise it is just a plain string literal,
// so use the exact route. e.g. `foo/bar`
//
route = routes[exactmatch[0]];
}
//
// route: { '/bazz': {...} }
//
if (next(_path, _len, _matched)) return true;
}
}
}
} else {
//
// If we hit an _exact_ match then essentially stop
// recursing.
//
// _len: 2 - 1 + 1 = 0
//
_len = len - path.split('/').length + 1;
}
next('/', _len, matched);
function next(path, len, matched) {
if (route) {
//
// If the path is not a `string` assume it is an Array
// and join it with the delimiter. This occurs for
// large regexps or deep nesting structures.
//
if (typeof path !== 'string') {
path = '/' + path.join('/');
}
//
// _Recursive case_: If we have not yet reached the end of
// the `path` then recurse with the new values
//
if (path !== '/') {
parse(route, path, len, matched);
}
if (len === 0 || self._recurse) {
function queue(fn, type) {
if(fn && typeof fn !== 'string' && fn[0]) {
for (var j = 0, m = fn.length; j < m; j++) {
self.events[type][add]({ fn: fn[j], val: matched || path });
}
}
else {
self.events[type][add]({ fn: fn, val: matched || path });
}
};
if (typeof route === 'function' || route.on) {
queue(route.on || route, 'on');
}
if (route.once && !route.once._fired){
route.once._fired = true;
queue(route.once, 'once');
}
if (route.after){
queue(route.after, 'after');
}
return true;
}
}
else {
self.noroute(matched);
}
};
return true;
}
function route(event) {
var loc = dloc.hash.slice(1);
var len = loc.split('/').length-1;
self.events.after = [];
if(parse(self.routes, loc, len, [])) {
dispatch('on');
dispatch('once');
dispatch('oneach');
self.events.on = [];
self.events.once = [];
}
dispatch('after');
dispatch('aftereach');
}
this.init = function(r) {
if(dloc.hash === '' && r) {
dloc.hash = r;
}
if(dloc.hash.length > 0) {
route();
}
listener.init(route);
return this;
};
this.destroy = function() {
listener.destroy(route);
return this;
};
return this;
}
Router.prototype.use = function(conf) {
for(var item in conf) {
if(conf.hasOwnProperty(item)) {
if(item === 'recurse') {
this.recurse(conf[item]);
continue;
}
if(item === 'resource') {
this.resource = conf[item];
continue;
}
if(item === 'notfound') {
this.events.notfound = conf[item];
continue;
}
var fn = conf[item];
var store = null;
var type = ({}).toString.call(fn);
if(item === 'on') { store = 'oneach'; }
if(item === 'after') { store = 'aftereach'; }
if(type.indexOf('Array') !== -1) {
for (var i=0, l = fn.length; i < l; i++) {
this.events[store].push({ fn: fn[i], val: null });
}
}
else {
this.events[store].push({ fn: fn, val: null });
}
}
}
return this;
};
Router.prototype.noroute = function(routename) {
if(this.events.notfound) {
if(({}).toString.call(this.events.notfound).indexOf('Array') !== -1) {
for (var i=0, l = this.events.notfound.length; i < l; i++) {
this.events.notfound[i](routename);
}
}
else {
this.events.notfound(routename);
}
}
};
Router.prototype.explode = function() {
var v = dloc.hash;
if(v[1] === '/') { v=v.slice(1); }
return v.slice(1, v.length).split("/");
};
Router.prototype.setRoute = function(i, v, val) {
var url = this.explode();
if(typeof i === 'number' && typeof v === 'string') {
url[i] = v;
}
else if(typeof val === 'string') {
url.splice(i, v, s);
}
else {
url = [i];
}
listener.setHash(url.join('/'));
return url;
};
Router.prototype.getState = function() {
return this.state;
};
Router.prototype.getRoute = function(v) {
var ret = v;
if(typeof v === "number") {
ret = this.explode()[v];
}
else if(typeof v === "string"){
var h = this.explode();
ret = h.indexOf(v);
}
else {
ret = this.explode();
}
return ret;
};
Router.prototype.on = function(route, cb) {
var compiledRoute;
if(this.route) {
compiledRoute = this.route[regifyString(route)];
}
else {
compiledRoute = this.route = this.routes[regifyString(route)] = {
on: null
};
}
if(compiledRoute) {
compiledRoute.on = cb;
}
};
var version = '0.4.0',
mode = 'modern',
listener = {
hash: dloc.hash,
check: function () {
var h = dloc.hash;
if (h != this.hash) {
this.hash = h;
this.onHashChanged();
}
},
fire: function() {
if(mode === 'modern') {
window.onhashchange();
}
else {
this.onHashChanged();
}
},
init: function (fn) {
var self = this;
if(!window.Router.listeners) {
window.Router.listeners = [];
}
function onchange() {
for(var i = 0, l = window.Router.listeners.length; i < l; i++) {
window.Router.listeners[i]();
}
}
//note IE8 is being counted as 'modern' because it has the hashchange event
if('onhashchange' in window &&
(document.documentMode === undefined || document.documentMode > 7)) {
window.onhashchange = onchange;
mode = 'modern';
}
else { // IE support, based on a concept by Erik Arvidson ...
var frame = document.createElement('iframe');
frame.id = 'state-frame';
frame.style.display = 'none';
document.body.appendChild(frame);
this.writeFrame('');
if ('onpropertychange' in document && 'attachEvent' in document) {
document.attachEvent('onpropertychange', function () {
if (event.propertyName === 'location') {
self.check();
}
});
}
window.setInterval(function () { self.check(); }, 50);
this.onHashChanged = onchange;
mode = 'legacy';
}
window.Router.listeners.push(fn);
return mode;
},
destroy: function (fn) {
if (!window.Router || !window.Router.listeners) return;
var listeners = window.Router.listeners;
for (var i = listeners.length - 1; i >= 0; i--) {
if (listeners[i] === fn) {
listeners.splice(i, 1);
}
}
},
setHash: function (s) {
// Mozilla always adds an entry to the history
if (mode === 'legacy') {
this.writeFrame(s);
}
dloc.hash = (s[0] === '/') ? s : '/' + s;
return this;
},
writeFrame: function (s) {
// IE support...
var f = document.getElementById('state-frame');
var d = f.contentDocument || f.contentWindow.document;
d.open();
d.write("<script>_hash = '" + s + "'; onload = parent.listener.syncHash;<script>");
d.close();
},
syncHash: function () {
// IE support...
var s = this._hash;
if (s != dloc.hash) {
dloc.hash = s;
}
return this;
},
onHashChanged: function () {}
};
}(window));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment