(function($) { // functions for handling the path // thanks sammy.js var PATH_REPLACER = "([^\/]+)", PATH_NAME_MATCHER = /:([\w\d]+)/g, QUERY_STRING_MATCHER = /\?([^#]*)$/, SPLAT_MATCHER = /(\*)/, SPLAT_REPLACER = "(.+)", _currentPath, _lastPath, _pathInterval; function hashChanged() { _currentPath = getPath(); // if path is actually changed from what we thought it was, then react if (_lastPath != _currentPath) { _lastPath = _currentPath; return triggerOnPath(_currentPath); } } $.pathbinder = { changeFuns : [], paths : [], begin : function(defaultPath) { // this should trigger the defaultPath if there's not a path in the URL // otherwise it should trigger the URL's path $(function() { var loadPath = getPath(); if (loadPath) { triggerOnPath(loadPath); } else { goPath(defaultPath); triggerOnPath(defaultPath); } }) }, go : function(path) { goPath(path); triggerOnPath(path); }, currentPath : function() { return getPath(); }, onChange : function (fun) { $.pathbinder.changeFuns.push(fun); } }; function pollPath(every) { function hashCheck() { _currentPath = getPath(); // path changed if _currentPath != _lastPath if (_lastPath != _currentPath) { setTimeout(function() { $(window).trigger('hashchange'); }, 1); } }; hashCheck(); _pathInterval = setInterval(hashCheck, every); $(window).bind('unload', function() { clearInterval(_pathInterval); }); } function triggerOnPath(path) { path = path.replace(/^#/,''); $.pathbinder.changeFuns.forEach(function(fun) {fun(path)}); var pathSpec, path_params, params = {}, param_name, param; for (var i=0; i < $.pathbinder.paths.length; i++) { pathSpec = $.pathbinder.paths[i]; // $.log("pathSpec", pathSpec); if ((path_params = pathSpec.matcher.exec(path)) !== null) { // $.log("path_params", path_params); path_params.shift(); for (var j=0; j < path_params.length; j++) { param_name = pathSpec.param_names[j]; param = decodeURIComponent(path_params[j]); if (param_name) { params[param_name] = param; } else { if (!params.splat) params.splat = []; params.splat.push(param); } }; pathSpec.callback(params); // return true; // removed this to allow for multi match } }; }; // bind the event $(function() { if ('onhashchange' in window) { // we have a native event } else { pollPath(10); } // setTimeout(hashChanged,50); $(window).bind('hashchange', hashChanged); }); function registerPath(pathSpec) { $.pathbinder.paths.push(pathSpec); }; function setPath(pathSpec, params) { var newPath = $.mustache(pathSpec.template, params); goPath(newPath); }; function goPath(newPath) { if (newPath) { // $.log("goPath", newPath) window.location = '#'+newPath; } _lastPath = getPath(); }; function getPath() { var matches = window.location.toString().match(/^[^#]*(#.+)$/); return matches ? matches[1] : ''; }; function makePathSpec(path, callback) { var param_names = []; var template = ""; PATH_NAME_MATCHER.lastIndex = 0; while ((path_match = PATH_NAME_MATCHER.exec(path)) !== null) { param_names.push(path_match[1]); } return { param_names : param_names, matcher : new RegExp("^" + path.replace( PATH_NAME_MATCHER, PATH_REPLACER).replace( SPLAT_MATCHER, SPLAT_REPLACER) + "/?$"), template : path.replace(PATH_NAME_MATCHER, function(a, b) { return '{{'+b+'}}'; }).replace(SPLAT_MATCHER, '{{splat}}'), callback : callback }; }; $.fn.pathbinder = function(name, paths, options) { options = options || {}; var self = $(this), pathList = paths.split(/\n/); $.each(pathList, function() { var path = this; if (path) { // $.log("bind path", path); var pathSpec = makePathSpec(path, function(params) { // $.log("path cb", name, path, self) // $.log("trigger path: "+path+" params: ", params); self.trigger(name, [params]); }); // set the path when the event triggered through other means if (options.bindPath) { self.bind(name, function(ev, params) { params = params || {}; // $.log("set path", name, pathSpec) setPath(pathSpec, params); }); } // trigger when the path matches registerPath(pathSpec); } }); }; })(jQuery);