/* * jQuery routes - v2.0 * Routing in javascript using hashchange event. * http://thorsteinsson.is/projects/jquery-routes/ * * Copyright (c) 2010 Ægir Þorsteinsson * Licensed under a Creative Commons Attribution 3.0 license * http://creativecommons.org/licenses/by/3.0/ */ (function($) { var routecount = 0, datelpad = function(d) { d = d + ''; return d.length < 2 ? '0' + d : d; }, legacyDateTypes = { 'date': { regexp: /(\d{1,2})\-(\d{1,2})\-(\d{4})/, parse: function(d, dd, mm, yyyy) { return new Date(yyyy, datelpad(mm - 1), datelpad(dd)); }, stringify: function(date) { return datelpad(date.getDate()) + '-' + datelpad(date.getMonth() + 1) + '-' + date.getFullYear(); } }, 'dateend': { regexp: /(\d{1,2})\-(\d{1,2})\-(\d{4})/, parse: function(d, dd, mm, yyyy) { return new Date(yyyy, mm - 1, dd, 23, 59, 59, 999); }, stringify: function(date) { return datelpad(date.getDate()) + '-' + datelpad(date.getMonth() + 1) + '-' + date.getFullYear(); } } }, isoDateTypes = { 'date': { regexp: /(\d{4})\-(\d{1,2})\-(\d{1,2})/, parse: function(d, yyyy, mm, dd) { return new Date(yyyy, mm - 1, dd); }, stringify: function(date) { return date.toISOString().split('T')[0]; } }, 'dateend': { regexp: /(\d{4})\-(\d{1,2})\-(\d{1,2})/, parse: function(d, yyyy, mm, dd) { return new Date(yyyy, mm - 1, dd, 23, 59, 59, 999); }, stringify: function(date) { return date.toISOString().split('T')[0]; } } }; $.extend({ routes: { useIsoDates: function(iso){ if(iso === true){ this.datatypes.date = isoDateTypes.date; this.datatypes.dateend = isoDateTypes.dateend; } else { this.datatypes.date = legacyDateTypes.date; this.datatypes.dateend = legacyDateTypes.dateend; } }, // datatypes for parameters, regexp groups are passed as parameters to parsers datatypes: { 'int': { regexp: /\d+?/, parse: function(d) { return parseInt(d, 10); } }, 'float': { regexp: /\d+\.\d+?/, parse: function(d) { return parseFloat(d); } }, 'word': { regexp: /\w+?/ }, 'string': { regexp: /[\s\w]+?/ }, 'datetime': { regexp: /(\d{4})\-(\d{2})\-(\d{2})T(\d{2}):(\d{2}):(\d{2})\.(\d{3})Z/, parse: function(d, yyyy, mm, dd, hh, MM, ss, ms) { return new Date(yyyy, mm - 1, dd, hh, MM, ss, ms); }, stringify: function(date) { return date.toISOString(); } }, 'date': isoDateTypes.date, 'dateend': isoDateTypes.dateend }, // object containing all the routes by name list: {}, // current executed route with parameter info current: null, // reload the current route with same or different parameters reload: function(args) { if (this.current) { args = $.extend(this.current.arguments, args); var url = this.current.url(args); if (url !== location.hash) { location.hash = url; } else { this.current.execute(args); } } else { location.href.replace(location.href); } }, // add a route, they can be named and with default parameters add: function(route, name, defaults, func) { // parameter overload if (typeof(name) === 'function') { func = name; name = undefined; } if (typeof(defaults) === 'function') { func = defaults; defaults = undefined; } if (typeof(name) === 'object') { defaults = name; name = undefined; } // all routes have a name if (name === undefined) { name = 'route' + (++routecount); } if (route[route.length - 1] !== '/') { route += '/'; } var regex = /\{\s*?([a-zA-Z0-9:\|\\*\?\.]*?)\s*?\}/gim; var items = ['var s="#' + route + '";']; var item = route; var routeexp = '^' + route; var vars = []; // find parameters in route and create a regexp while (match = regex.exec(item)) { var arr = match[1].split(':'); var first = arr[0]; var constraint = arr[1]; items.push('s=s.replace("' + match[0] + '",p.' + first + ');'); item = item.replace(match[0], ''); if (constraint) { // check for known datatype if ($.routes.datatypes[constraint]) { constraint = $.routes.datatypes[constraint].regexp.toString(); constraint = constraint.replace(/\/[gim]+$/g, ''); // Remove flags constraint = constraint.replace(/\/$|^\//g, ''); // Wrapping slashes constraint = constraint.replace(/[\(\)]/g, ''); // Remove groups } constraint = '(' + constraint + ')'; } else { constraint = '(.*?)'; } if(defaults && defaults[first]) { constraint += '?'; // If there is a default, make parameter optional } routeexp = routeexp.replace(match[0], constraint); vars.push(match[1].split(':')); regex.lastIndex = match.index; } items.push('return s;'); // find a datatype var getDatatype = function(parameter) { for (var i = 0, len = vars.length; i < len; i++) { if (vars[i][0] === parameter) { return $.routes.datatypes[vars[i][1]]; } } }; // prepare parameters for url var getParams = function(obj) { var args = $.extend({}, defaults, obj), dt; for (var item in args) { if (args.hasOwnProperty(item)) { dt = getDatatype(item); if (dt && dt.stringify) { args[item] = dt.stringify(args[item]); } } } return args; }; // convert route with parameters to url var combine = new Function('p', items.join('')); var url = function(p) { return combine(getParams(p)); }; // extract parameters from hash var extract = function(hash) { // load the defaults var context = defaults ? defaults : {}; // get parameters $.each(vars, function(x, vars) { var datatype = $.routes.datatypes[vars[1]]; var val = args[x+1]; // parse parameter if (datatype && datatype.parse) { val = [val]; // get arguments for parse if (datatype.regexp) { var p = datatype.regexp.exec(args[x+1] || datatype.stringify(defaults[vars[x]])); for (var y = 1; y < p.length; y++) { val.push(p[y]); } } val = datatype.parse.apply(datatype, val); } context[vars[0]] = val; }); return context; }; // add to list of routes return this.list[name] = { name: name, exp: new RegExp(routeexp, 'i'), route: route, func: func, url: url, routeTo: function(data) { location.href = this.url(data); }, execute: function(data) { this.func.apply($.extend({}, defaults, data)); }, vars: vars, defaults: defaults, level: route.length - route.replace(new RegExp("/", "g"), '').length, extract: extract }; }, addDataType: function(name, dt) { $.routes.datatypes[name] = dt; createDatatypeArray(name); }, // get a registered route with name find: function(name) { return this.list[name]; }, findRoute: function(hash) { var route; // search for routes $.each(this.list, function(i, match) { // check for a match var exp = match.exp.exec(hash); if (exp) { if ((route && match.level > route.level) || !route) { route = match; args = exp; } } }); return route; }, // load a hash and find the correct route to execute load: function(hash) { var route = $.routes.findRoute(hash.substr(1)); // route not found if (!route) { return; } // extract parameters from url var params = route.extract(hash); // execute route $.routes.current = route; $.routes.current.arguments = params; route.execute(params); } } }); $.expr[':'].hasRoute = function(elem, index, match) { return $(elem).has('a[href$=' + location.hash + ']').length > 0; }; $.fn.updateRoutes = function(params) { $('a', this).each(function() { var item = $(this), hash = item.attr('href').substr(1), route = $.routes.findRoute(hash); if (!route) { return; } var linkparams = route.extract(hash); var newhash = route.url($.extend({}, linkparams, params)); item.attr('href', newhash); }); }; function createDatatypeArray(name) { var dt = $.routes.datatypes[name]; $.routes.datatypes[name + 'array'] = { datatype: name, regexp: new RegExp('[' + dt.regexp.toString().slice(1, dt.regexp.toString().length - 1) + '\\/\?]*', 'i'), parse: function(d) { var arr = d.split('/'), newarr = [], dt = $.routes.datatypes[this.datatype]; for (var i = 0; i < arr.length; i++) { var groups = dt.regexp.exec(arr[i]).slice(1); var args = [arr[i]].concat(groups); newarr.push(dt.parse.apply(dt, args)); } return newarr; }, stringify: function(arr) { for (var i = 0; i < arr.length; i++) { var dt = $.routes.datatypes[this.datatype]; arr[i] = (!!dt.stringify ? dt.stringify(arr[i]) : arr[i].toString()); } return arr.join('/'); } }; } // create array data types var dts = $.extend({},$.routes.datatypes); for (var name in dts) { if (dts.hasOwnProperty(name)) { createDatatypeArray(name); } } // Listen for hash change event $(window).bind('hashchange', function() { $.routes.load(location.hash); }); // Run route on document ready $(function() { var hash = location.hash; if (hash.length > 0) { if(hash[hash.length-1] !== '/'){ hash += '/'; } $.routes.load(hash); } }); }(jQuery));