1 /*!! 2 * Hasher <http://github.com/millermedeiros/hasher> 3 * @author Miller Medeiros 4 * @version 1.2.0 (2013/11/11 03:18 PM) 5 * Released under the MIT License 6 */ 7 8 ;(function () { 9 var factory = function(signals){ 10 11 /*jshint white:false*/ 12 /*global signals:false, window:false*/ 13 14 /** 15 * Hasher 16 * @namespace History Manager for rich-media applications. 17 * @name hasher 18 */ 19 var hasher = (function(window){ 20 21 //-------------------------------------------------------------------------------------- 22 // Private Vars 23 //-------------------------------------------------------------------------------------- 24 25 var 26 27 // frequency that it will check hash value on IE 6-7 since it doesn't 28 // support the hashchange event 29 POOL_INTERVAL = 25, 30 31 // local storage for brevity and better compression -------------------------------- 32 33 document = window.document, 34 history = window.history, 35 Signal = signals.Signal, 36 37 // local vars ---------------------------------------------------------------------- 38 39 hasher, 40 _hash, 41 _checkInterval, 42 _isActive, 43 _frame, //iframe used for legacy IE (6-7) 44 _checkHistory, 45 _hashValRegexp = /#(.*)$/, 46 _baseUrlRegexp = /(\?.*)|(\#.*)/, 47 _hashRegexp = /^\#/, 48 49 // sniffing/feature detection ------------------------------------------------------- 50 51 //hack based on this: http://webreflection.blogspot.com/2009/01/32-bytes-to-know-if-your-browser-is-ie.html 52 _isIE = (!+"\v1"), 53 // hashchange is supported by FF3.6+, IE8+, Chrome 5+, Safari 5+ but 54 // feature detection fails on IE compatibility mode, so we need to 55 // check documentMode 56 _isHashChangeSupported = ('onhashchange' in window) && document.documentMode !== 7, 57 //check if is IE6-7 since hash change is only supported on IE8+ and 58 //changing hash value on IE6-7 doesn't generate history record. 59 _isLegacyIE = _isIE && !_isHashChangeSupported, 60 _isLocal = (location.protocol === 'file:'); 61 62 63 //-------------------------------------------------------------------------------------- 64 // Private Methods 65 //-------------------------------------------------------------------------------------- 66 67 function _escapeRegExp(str){ 68 return String(str || '').replace(/\W/g, "\\$&"); 69 } 70 71 function _trimHash(hash){ 72 if (!hash) return ''; 73 var regexp = new RegExp('^' + _escapeRegExp(hasher.prependHash) + '|' + _escapeRegExp(hasher.appendHash) + '$', 'g'); 74 return hash.replace(regexp, ''); 75 } 76 77 function _getWindowHash(){ 78 //parsed full URL instead of getting window.location.hash because Firefox decode hash value (and all the other browsers don't) 79 //also because of IE8 bug with hash query in local file [issue #6] 80 var result = _hashValRegexp.exec( hasher.getURL() ); 81 var path = (result && result[1]) || ''; 82 try { 83 return hasher.raw? path : decodeURIComponent(path); 84 } catch (e) { 85 // in case user did not set `hasher.raw` and decodeURIComponent 86 // throws an error (see #57) 87 return path; 88 } 89 } 90 91 function _getFrameHash(){ 92 return (_frame)? _frame.contentWindow.frameHash : null; 93 } 94 95 function _createFrame(){ 96 _frame = document.createElement('iframe'); 97 _frame.src = 'about:blank'; 98 _frame.style.display = 'none'; 99 document.body.appendChild(_frame); 100 } 101 102 function _updateFrame(){ 103 if(_frame && _hash !== _getFrameHash()){ 104 var frameDoc = _frame.contentWindow.document; 105 frameDoc.open(); 106 //update iframe content to force new history record. 107 //based on Really Simple History, SWFAddress and YUI.history. 108 frameDoc.write('<html><head><title>' + document.title + '</title><script type="text/javascript">var frameHash="' + _hash + '";</script></head><body> </body></html>'); 109 frameDoc.close(); 110 } 111 } 112 113 function _registerChange(newHash, isReplace){ 114 if(_hash !== newHash){ 115 var oldHash = _hash; 116 _hash = newHash; //should come before event dispatch to make sure user can get proper value inside event handler 117 if(_isLegacyIE){ 118 if(!isReplace){ 119 _updateFrame(); 120 } else { 121 _frame.contentWindow.frameHash = newHash; 122 } 123 } 124 hasher.changed.dispatch(_trimHash(newHash), _trimHash(oldHash)); 125 } 126 } 127 128 if (_isLegacyIE) { 129 /** 130 * @private 131 */ 132 _checkHistory = function(){ 133 var windowHash = _getWindowHash(), 134 frameHash = _getFrameHash(); 135 if(frameHash !== _hash && frameHash !== windowHash){ 136 //detect changes made pressing browser history buttons. 137 //Workaround since history.back() and history.forward() doesn't 138 //update hash value on IE6/7 but updates content of the iframe. 139 //needs to trim hash since value stored already have 140 //prependHash + appendHash for fast check. 141 hasher.setHash(_trimHash(frameHash)); 142 } else if (windowHash !== _hash){ 143 //detect if hash changed (manually or using setHash) 144 _registerChange(windowHash); 145 } 146 }; 147 } else { 148 /** 149 * @private 150 */ 151 _checkHistory = function(){ 152 var windowHash = _getWindowHash(); 153 if(windowHash !== _hash){ 154 _registerChange(windowHash); 155 } 156 }; 157 } 158 159 function _addListener(elm, eType, fn){ 160 if(elm.addEventListener){ 161 elm.addEventListener(eType, fn, false); 162 } else if (elm.attachEvent){ 163 elm.attachEvent('on' + eType, fn); 164 } 165 } 166 167 function _removeListener(elm, eType, fn){ 168 if(elm.removeEventListener){ 169 elm.removeEventListener(eType, fn, false); 170 } else if (elm.detachEvent){ 171 elm.detachEvent('on' + eType, fn); 172 } 173 } 174 175 function _makePath(paths){ 176 paths = Array.prototype.slice.call(arguments); 177 178 var path = paths.join(hasher.separator); 179 path = path? hasher.prependHash + path.replace(_hashRegexp, '') + hasher.appendHash : path; 180 return path; 181 } 182 183 function _encodePath(path){ 184 //used encodeURI instead of encodeURIComponent to preserve '?', '/', 185 //'#'. Fixes Safari bug [issue #8] 186 path = encodeURI(path); 187 if(_isIE && _isLocal){ 188 //fix IE8 local file bug [issue #6] 189 path = path.replace(/\?/, '%3F'); 190 } 191 return path; 192 } 193 194 //-------------------------------------------------------------------------------------- 195 // Public (API) 196 //-------------------------------------------------------------------------------------- 197 198 hasher = /** @lends hasher */ { 199 200 /** 201 * hasher Version Number 202 * @type string 203 * @constant 204 */ 205 VERSION : '1.2.0', 206 207 /** 208 * Boolean deciding if hasher encodes/decodes the hash or not. 209 * <ul> 210 * <li>default value: false;</li> 211 * </ul> 212 * @type boolean 213 */ 214 raw : false, 215 216 /** 217 * String that should always be added to the end of Hash value. 218 * <ul> 219 * <li>default value: '';</li> 220 * <li>will be automatically removed from `hasher.getHash()`</li> 221 * <li>avoid conflicts with elements that contain ID equal to hash value;</li> 222 * </ul> 223 * @type string 224 */ 225 appendHash : '', 226 227 /** 228 * String that should always be added to the beginning of Hash value. 229 * <ul> 230 * <li>default value: '/';</li> 231 * <li>will be automatically removed from `hasher.getHash()`</li> 232 * <li>avoid conflicts with elements that contain ID equal to hash value;</li> 233 * </ul> 234 * @type string 235 */ 236 prependHash : '/', 237 238 /** 239 * String used to split hash paths; used by `hasher.getHashAsArray()` to split paths. 240 * <ul> 241 * <li>default value: '/';</li> 242 * </ul> 243 * @type string 244 */ 245 separator : '/', 246 247 /** 248 * Signal dispatched when hash value changes. 249 * - pass current hash as 1st parameter to listeners and previous hash value as 2nd parameter. 250 * @type signals.Signal 251 */ 252 changed : new Signal(), 253 254 /** 255 * Signal dispatched when hasher is stopped. 256 * - pass current hash as first parameter to listeners 257 * @type signals.Signal 258 */ 259 stopped : new Signal(), 260 261 /** 262 * Signal dispatched when hasher is initialized. 263 * - pass current hash as first parameter to listeners. 264 * @type signals.Signal 265 */ 266 initialized : new Signal(), 267 268 /** 269 * Start listening/dispatching changes in the hash/history. 270 * <ul> 271 * <li>hasher won't dispatch CHANGE events by manually typing a new value or pressing the back/forward buttons before calling this method.</li> 272 * </ul> 273 */ 274 init : function(){ 275 if(_isActive) return; 276 277 _hash = _getWindowHash(); 278 279 //thought about branching/overloading hasher.init() to avoid checking multiple times but 280 //don't think worth doing it since it probably won't be called multiple times. 281 if(_isHashChangeSupported){ 282 _addListener(window, 'hashchange', _checkHistory); 283 }else { 284 if(_isLegacyIE){ 285 if(! _frame){ 286 _createFrame(); 287 } 288 _updateFrame(); 289 } 290 _checkInterval = setInterval(_checkHistory, POOL_INTERVAL); 291 } 292 293 _isActive = true; 294 hasher.initialized.dispatch(_trimHash(_hash)); 295 }, 296 297 /** 298 * Stop listening/dispatching changes in the hash/history. 299 * <ul> 300 * <li>hasher won't dispatch CHANGE events by manually typing a new value or pressing the back/forward buttons after calling this method, unless you call hasher.init() again.</li> 301 * <li>hasher will still dispatch changes made programatically by calling hasher.setHash();</li> 302 * </ul> 303 */ 304 stop : function(){ 305 if(! _isActive) return; 306 307 if(_isHashChangeSupported){ 308 _removeListener(window, 'hashchange', _checkHistory); 309 }else{ 310 clearInterval(_checkInterval); 311 _checkInterval = null; 312 } 313 314 _isActive = false; 315 hasher.stopped.dispatch(_trimHash(_hash)); 316 }, 317 318 /** 319 * @return {boolean} If hasher is listening to changes on the browser history and/or hash value. 320 */ 321 isActive : function(){ 322 return _isActive; 323 }, 324 325 /** 326 * @return {string} Full URL. 327 */ 328 getURL : function(){ 329 return window.location.href; 330 }, 331 332 /** 333 * @return {string} Retrieve URL without query string and hash. 334 */ 335 getBaseURL : function(){ 336 return hasher.getURL().replace(_baseUrlRegexp, ''); //removes everything after '?' and/or '#' 337 }, 338 339 /** 340 * Set Hash value, generating a new history record. 341 * @param {...string} path Hash value without '#'. Hasher will join 342 * path segments using `hasher.separator` and prepend/append hash value 343 * with `hasher.appendHash` and `hasher.prependHash` 344 * @example hasher.setHash('lorem', 'ipsum', 'dolor') -> '#/lorem/ipsum/dolor' 345 */ 346 setHash : function(path){ 347 path = _makePath.apply(null, arguments); 348 if(path !== _hash){ 349 // we should store raw value 350 _registerChange(path); 351 if (path === _hash) { 352 // we check if path is still === _hash to avoid error in 353 // case of multiple consecutive redirects [issue #39] 354 if (! hasher.raw) { 355 path = _encodePath(path); 356 } 357 window.location.hash = '#' + path; 358 } 359 } 360 }, 361 362 /** 363 * Set Hash value without keeping previous hash on the history record. 364 * Similar to calling `window.location.replace("#/hash")` but will also work on IE6-7. 365 * @param {...string} path Hash value without '#'. Hasher will join 366 * path segments using `hasher.separator` and prepend/append hash value 367 * with `hasher.appendHash` and `hasher.prependHash` 368 * @example hasher.replaceHash('lorem', 'ipsum', 'dolor') -> '#/lorem/ipsum/dolor' 369 */ 370 replaceHash : function(path){ 371 path = _makePath.apply(null, arguments); 372 if(path !== _hash){ 373 // we should store raw value 374 _registerChange(path, true); 375 if (path === _hash) { 376 // we check if path is still === _hash to avoid error in 377 // case of multiple consecutive redirects [issue #39] 378 if (! hasher.raw) { 379 path = _encodePath(path); 380 } 381 window.location.replace('#' + path); 382 } 383 } 384 }, 385 386 /** 387 * @return {string} Hash value without '#', `hasher.appendHash` and `hasher.prependHash`. 388 */ 389 getHash : function(){ 390 //didn't used actual value of the `window.location.hash` to avoid breaking the application in case `window.location.hash` isn't available and also because value should always be synched. 391 return _trimHash(_hash); 392 }, 393 394 /** 395 * @return {Array.<string>} Hash value split into an Array. 396 */ 397 getHashAsArray : function(){ 398 return hasher.getHash().split(hasher.separator); 399 }, 400 401 /** 402 * Removes all event listeners, stops hasher and destroy hasher object. 403 * - IMPORTANT: hasher won't work after calling this method, hasher Object will be deleted. 404 */ 405 dispose : function(){ 406 hasher.stop(); 407 hasher.initialized.dispose(); 408 hasher.stopped.dispose(); 409 hasher.changed.dispose(); 410 _frame = hasher = window.hasher = null; 411 }, 412 413 /** 414 * @return {string} A string representation of the object. 415 */ 416 toString : function(){ 417 return '[hasher version="'+ hasher.VERSION +'" hash="'+ hasher.getHash() +'"]'; 418 } 419 420 }; 421 422 hasher.initialized.memorize = true; //see #33 423 424 return hasher; 425 426 }(window)); 427 428 429 return hasher; 430 }; 431 432 if (typeof define === 'function' && define.amd) { 433 define(['signals'], factory); 434 } else if (typeof exports === 'object') { 435 module.exports = factory(require('signals')); 436 } else { 437 /*jshint sub:true */ 438 window['hasher'] = factory(window['signals']); 439 } 440 441 }()); 442