├── jquery.router.js └── readme.md /jquery.router.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | plugin name: router 4 | jquery plugin to handle routes with both hash and push state 5 | why? why another routing plugin? because i couldnt find one that handles both hash and pushstate 6 | created by 24hr // camilo.tapia 7 | author twitter: camilo.tapia 8 | 9 | Copyright 2011 camilo tapia // 24hr (email : camilo.tapia@gmail.com) 10 | 11 | This program is free software; you can redistribute it and/or modify 12 | it under the terms of the GNU General Public License, version 2, as 13 | published by the Free Software Foundation. 14 | 15 | This program is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | GNU General Public License for more details. 19 | 20 | You should have received a copy of the GNU General Public License 21 | along with this program; if not, write to the Free Software 22 | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 23 | 24 | */ 25 | 26 | 27 | (function($){ 28 | 29 | var hasPushState = (history && history.pushState); 30 | var hasHashState = !hasPushState && ("onhashchange" in window) && false; 31 | var router = {}; 32 | var routeList = []; 33 | var eventAdded = false; 34 | var currentUsedUrl = location.href; //used for ie to hold the current url 35 | var firstRoute = true; 36 | var errorCallback = function () {}; 37 | 38 | // hold the latest route that was activated 39 | router.currentId = ""; 40 | router.currentParameters = {}; 41 | 42 | // Create a default error handler 43 | router.errorCallback = errorCallback; 44 | 45 | router.capabilities = { 46 | hash: hasHashState, 47 | pushState: hasPushState, 48 | timer: !hasHashState && !hasPushState 49 | }; 50 | 51 | // reset all routes 52 | router.reset = function() 53 | { 54 | var router = {}; 55 | var routeList = []; 56 | router.currentId = ""; 57 | router.currentParameters = {}; 58 | } 59 | 60 | router.add = function(route, id, callback) 61 | { 62 | // if we only get a route and a callback, we switch the arguments 63 | if (typeof id == "function") 64 | { 65 | callback = id; 66 | delete id; 67 | } 68 | 69 | var isRegExp = typeof route == "object"; 70 | 71 | if (!isRegExp) 72 | { 73 | 74 | // remove the last slash to unifiy all routes 75 | if (route.lastIndexOf("/") == route.length - 1) 76 | { 77 | route = route.substring(0, route.length - 1); 78 | } 79 | 80 | // if the routes where created with an absolute url ,we have to remove the absolut part anyway, since we cant change that much 81 | route = route.replace(location.protocol + "//", "").replace(location.hostname, ""); 82 | } 83 | 84 | var routeItem = { 85 | route: route, 86 | callback: callback, 87 | type: isRegExp ? "regexp" : "string", 88 | id: id 89 | } 90 | 91 | routeList.push(routeItem); 92 | 93 | // we add the event listener after the first route is added so that we dont need to listen to events in vain 94 | if (!eventAdded) 95 | { 96 | bindStateEvents(); 97 | } 98 | }; 99 | 100 | router.addErrorHandler = function (callback) 101 | { 102 | this.errorCallback = callback; 103 | }; 104 | 105 | function bindStateEvents() 106 | { 107 | eventAdded = true; 108 | 109 | // default value telling router that we havent replaced the url from a hash. yet. 110 | router.fromHash = false; 111 | 112 | 113 | if (hasPushState) 114 | { 115 | // if we get a request with a qualified hash (ie it begins with #!) 116 | if (location.hash.indexOf("#!/") === 0) 117 | { 118 | // replace the state 119 | var url = location.pathname + location.hash.replace(/^#!\//gi, ""); 120 | history.replaceState({}, "", url); 121 | 122 | // this flag tells router that the url was converted from hash to popstate 123 | router.fromHash = true; 124 | } 125 | 126 | $(window).bind("popstate", handleRoutes); 127 | } 128 | else if (hasHashState) 129 | { 130 | $(window).bind("hashchange.router", handleRoutes); 131 | } 132 | else 133 | { 134 | // if no events are available we use a timer to check periodically for changes in the url 135 | setInterval( 136 | function() 137 | { 138 | if (location.href != currentUsedUrl) 139 | { 140 | handleRoutes(); 141 | currentUsedUrl = location.href; 142 | } 143 | }, 500 144 | ); 145 | } 146 | 147 | } 148 | 149 | bindStateEvents(); 150 | 151 | router.go = function(url, title) 152 | { 153 | if (hasPushState) 154 | { 155 | history.pushState({}, title, url); 156 | checkRoutes(); 157 | } 158 | else 159 | { 160 | // remove part of url that we dont use 161 | url = url.replace(location.protocol + "//", "").replace(location.hostname, ""); 162 | var hash = url.replace(location.pathname, ""); 163 | 164 | if (hash.indexOf("!") < 0) 165 | { 166 | hash = "!/" + hash; 167 | } 168 | location.hash = hash; 169 | } 170 | }; 171 | 172 | // do a check without affecting the history 173 | router.check = router.redo = function() 174 | { 175 | checkRoutes(true); 176 | }; 177 | 178 | // parse and wash the url to process 179 | function parseUrl(url) 180 | { 181 | var currentUrl = url ? url : location.pathname; 182 | 183 | currentUrl = decodeURI(currentUrl); 184 | 185 | // if no pushstate is availabe we have to use the hash 186 | if (!hasPushState) 187 | { 188 | if (location.hash.indexOf("#!/") === 0) 189 | { 190 | currentUrl += location.hash.substring(3); 191 | } 192 | else 193 | { 194 | return ''; 195 | } 196 | } 197 | 198 | // and if the last character is a slash, we just remove it 199 | currentUrl = currentUrl.replace(/\/$/, ""); 200 | 201 | return currentUrl; 202 | } 203 | 204 | // get the current parameters for either a specified url or the current one if parameters is ommited 205 | router.parameters = function(url) 206 | { 207 | // parse the url so that we handle a unified url 208 | var currentUrl = parseUrl(url); 209 | 210 | // get the list of actions for the current url 211 | var list = getParameters(currentUrl); 212 | 213 | // if the list is empty, return an empty object 214 | if (list.length == 0) 215 | { 216 | router.currentParameters = {}; 217 | } 218 | 219 | // if we got results, return the first one. at least for now 220 | else 221 | { 222 | router.currentParameters = list[0].data; 223 | } 224 | 225 | return router.currentParameters; 226 | } 227 | 228 | function getParameters(url) 229 | { 230 | 231 | var dataList = []; 232 | 233 | // console.log("ROUTES:"); 234 | 235 | for(var i = 0, ii = routeList.length; i < ii; i++) 236 | { 237 | var route = routeList[i]; 238 | 239 | // check for mathing reg exp 240 | if (route.type == "regexp") 241 | { 242 | var result = url.match(route.route); 243 | if (result) 244 | { 245 | var data = {}; 246 | data.matches = result; 247 | 248 | dataList.push( 249 | { 250 | route: route, 251 | data: data 252 | } 253 | ); 254 | 255 | // saves the current route id 256 | router.currentId = route.id; 257 | 258 | // break after first hit 259 | break; 260 | } 261 | } 262 | 263 | // check for mathing string routes 264 | else 265 | { 266 | var currentUrlParts = url.split("/"); 267 | var routeParts = route.route.split("/"); 268 | 269 | //console.log("matchCounter ", matchCounter, url, route.route) 270 | 271 | // first check so that they have the same amount of elements at least 272 | if (routeParts.length == currentUrlParts.length) 273 | { 274 | var data = {}; 275 | var matched = true; 276 | var matchCounter = 0; 277 | 278 | for(var j = 0, jj = routeParts.length; j < jj; j++) 279 | { 280 | var isParam = routeParts[j].indexOf(":") === 0; 281 | if (isParam) 282 | { 283 | data[routeParts[j].substring(1)] = decodeURI(currentUrlParts[j]); 284 | matchCounter++; 285 | } 286 | else 287 | { 288 | if (routeParts[j] == currentUrlParts[j]) 289 | { 290 | matchCounter++; 291 | } 292 | } 293 | } 294 | 295 | // break after first hit 296 | if (routeParts.length == matchCounter) 297 | { 298 | dataList.push( 299 | { 300 | route: route, 301 | data: data 302 | } 303 | ); 304 | 305 | // saved the current route id 306 | router.currentId = route.id; 307 | router.currentParameters = data; 308 | 309 | break; 310 | } 311 | 312 | } 313 | } 314 | 315 | } 316 | 317 | return dataList; 318 | } 319 | 320 | function checkRoutes() 321 | { 322 | var currentUrl = parseUrl(location.pathname); 323 | 324 | // check if something is catched 325 | var actionList = getParameters(currentUrl); 326 | 327 | // If no routes have been matched 328 | if (actionList.length == 0) { 329 | // Invoke error handler 330 | return router.errorCallback(currentUrl); 331 | } 332 | 333 | // ietrate trough result (but it will only kick in one) 334 | for(var i = 0, ii = actionList.length; i < ii; i++) 335 | { 336 | actionList[i].route.callback(actionList[i].data); 337 | } 338 | } 339 | 340 | 341 | function handleRoutes(e) 342 | { 343 | if (e != null && e.originalEvent && e.originalEvent.state !== undefined) 344 | { 345 | checkRoutes(); 346 | } 347 | else if (hasHashState) 348 | { 349 | checkRoutes(); 350 | } 351 | else if (!hasHashState && !hasPushState) 352 | { 353 | checkRoutes(); 354 | } 355 | } 356 | 357 | 358 | 359 | if (!$.router) 360 | { 361 | $.router = router; 362 | } 363 | else 364 | { 365 | if (window.console && window.console.warn) 366 | { 367 | console.warn("jQuery.status already defined. Something is using the same name."); 368 | } 369 | } 370 | 371 | })( jQuery ); 372 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # jquery router plugin 2 | This plugin handles routes for both hash and push state. 3 | 4 | Why? Why another routing plugin? Because i couldn't find one that handles both hash and pushstate. 5 | 6 | ### How to add routes 7 | 8 | To add route you simply call the add function, providing it with the actual route string, an optional id, and the callback. 9 | 10 | $.router.add(*route*, *[id]*, *callback*); 11 | 12 | Example: 13 | 14 | // Adds a route for /items/:item and calls the callback when matched 15 | $.router.add('/items/:item', function(data) { 16 | console.log(data.item); 17 | }); 18 | 19 | or 20 | 21 | // Adds a route for /items/:item and calls the callback when matched, but also has 22 | // a reference to the routes id in $.router.currentId 23 | $.router.add('/items/:item', 'foo', function(data) { 24 | console.log(data.item); 25 | }); 26 | 27 | ### How to change urls and trigger routes 28 | You can also change the current url to a route, and thereby triggering the route by calling *go*. 29 | 30 | $.router.go(url, title); 31 | 32 | Example: 33 | 34 | // This will change the url to http://www.foo.com/items/mycoolitem and set the title to 35 | // "My cool item" without reloading the page. If using hashes instead, it will use the url 36 | // http://www.foo.com/#!items/mycoolitem. 37 | // If a route has been set that matches it, it will be triggered. 38 | $.router.go('/items/mycoolitem', 'My cool item'); 39 | 40 | ### Reseting all routes 41 | 42 | If you need to remove all routes (which is good when testing) you just call: 43 | 44 | `$.router.reset();` 45 | 46 | ### Dealing with 404's 47 | 48 | If a url is entered which doesn't fire a route callback (a.k.a. 404 Not Found) you can add your error callback to take that case and make it beautiful. 49 | 50 | ```js 51 | $.router.addErrorHandler(function (url) { 52 | // url is the URL which the router couldn't find a callback for 53 | console.log(url); 54 | }); 55 | ``` 56 | 57 | ## License 58 | 59 | (The MIT License) 60 | 61 | Copyright (c) 2011 Camilo Tapia <camilo.tapia@gmail.com> 62 | 63 | Permission is hereby granted, free of charge, to any person obtaining 64 | a copy of this software and associated documentation files (the 65 | 'Software'), to deal in the Software without restriction, including 66 | without limitation the rights to use, copy, modify, merge, publish, 67 | distribute, sublicense, and/or sell copies of the Software, and to 68 | permit persons to whom the Software is furnished to do so, subject to 69 | the following conditions: 70 | 71 | The above copyright notice and this permission notice shall be 72 | included in all copies or substantial portions of the Software. 73 | 74 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 75 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 76 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 77 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 78 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 79 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 80 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 81 | --------------------------------------------------------------------------------