| /*!! |
| * Hasher <http://github.com/millermedeiros/hasher> |
| * @author Miller Medeiros |
| * @version 1.2.0 (2013/11/11 03:18 PM) |
| * Released under the MIT License |
| */ |
| |
| ;(function () { |
| var factory = function(signals){ |
| |
| /*jshint white:false*/ |
| /*global signals:false, window:false*/ |
| |
| /** |
| * Hasher |
| * @namespace History Manager for rich-media applications. |
| * @name hasher |
| */ |
| var hasher = (function(window){ |
| |
| //-------------------------------------------------------------------------------------- |
| // Private Vars |
| //-------------------------------------------------------------------------------------- |
| |
| var |
| |
| // frequency that it will check hash value on IE 6-7 since it doesn't |
| // support the hashchange event |
| POOL_INTERVAL = 25, |
| |
| // local storage for brevity and better compression -------------------------------- |
| |
| document = window.document, |
| history = window.history, |
| Signal = signals.Signal, |
| |
| // local vars ---------------------------------------------------------------------- |
| |
| hasher, |
| _hash, |
| _checkInterval, |
| _isActive, |
| _frame, //iframe used for legacy IE (6-7) |
| _checkHistory, |
| _hashValRegexp = /#(.*)$/, |
| _baseUrlRegexp = /(\?.*)|(\#.*)/, |
| _hashRegexp = /^\#/, |
| |
| // sniffing/feature detection ------------------------------------------------------- |
| |
| //hack based on this: http://webreflection.blogspot.com/2009/01/32-bytes-to-know-if-your-browser-is-ie.html |
| _isIE = (!+"\v1"), |
| // hashchange is supported by FF3.6+, IE8+, Chrome 5+, Safari 5+ but |
| // feature detection fails on IE compatibility mode, so we need to |
| // check documentMode |
| _isHashChangeSupported = ('onhashchange' in window) && document.documentMode !== 7, |
| //check if is IE6-7 since hash change is only supported on IE8+ and |
| //changing hash value on IE6-7 doesn't generate history record. |
| _isLegacyIE = _isIE && !_isHashChangeSupported, |
| _isLocal = (location.protocol === 'file:'); |
| |
| |
| //-------------------------------------------------------------------------------------- |
| // Private Methods |
| //-------------------------------------------------------------------------------------- |
| |
| function _escapeRegExp(str){ |
| return String(str || '').replace(/\W/g, "\\$&"); |
| } |
| |
| function _trimHash(hash){ |
| if (!hash) return ''; |
| var regexp = new RegExp('^' + _escapeRegExp(hasher.prependHash) + '|' + _escapeRegExp(hasher.appendHash) + '$', 'g'); |
| return hash.replace(regexp, ''); |
| } |
| |
| function _getWindowHash(){ |
| //parsed full URL instead of getting window.location.hash because Firefox decode hash value (and all the other browsers don't) |
| //also because of IE8 bug with hash query in local file [issue #6] |
| var result = _hashValRegexp.exec( hasher.getURL() ); |
| var path = (result && result[1]) || ''; |
| try { |
| return hasher.raw? path : decodeURIComponent(path); |
| } catch (e) { |
| // in case user did not set `hasher.raw` and decodeURIComponent |
| // throws an error (see #57) |
| return path; |
| } |
| } |
| |
| function _getFrameHash(){ |
| return (_frame)? _frame.contentWindow.frameHash : null; |
| } |
| |
| function _createFrame(){ |
| _frame = document.createElement('iframe'); |
| _frame.src = 'about:blank'; |
| _frame.style.display = 'none'; |
| document.body.appendChild(_frame); |
| } |
| |
| function _updateFrame(){ |
| if(_frame && _hash !== _getFrameHash()){ |
| var frameDoc = _frame.contentWindow.document; |
| frameDoc.open(); |
| //update iframe content to force new history record. |
| //based on Really Simple History, SWFAddress and YUI.history. |
| frameDoc.write('<html><head><title>' + document.title + '</title><script type="text/javascript">var frameHash="' + _hash + '";</script></head><body> </body></html>'); |
| frameDoc.close(); |
| } |
| } |
| |
| function _registerChange(newHash, isReplace){ |
| if(_hash !== newHash){ |
| var oldHash = _hash; |
| _hash = newHash; //should come before event dispatch to make sure user can get proper value inside event handler |
| if(_isLegacyIE){ |
| if(!isReplace){ |
| _updateFrame(); |
| } else { |
| _frame.contentWindow.frameHash = newHash; |
| } |
| } |
| hasher.changed.dispatch(_trimHash(newHash), _trimHash(oldHash)); |
| } |
| } |
| |
| if (_isLegacyIE) { |
| /** |
| * @private |
| */ |
| _checkHistory = function(){ |
| var windowHash = _getWindowHash(), |
| frameHash = _getFrameHash(); |
| if(frameHash !== _hash && frameHash !== windowHash){ |
| //detect changes made pressing browser history buttons. |
| //Workaround since history.back() and history.forward() doesn't |
| //update hash value on IE6/7 but updates content of the iframe. |
| //needs to trim hash since value stored already have |
| //prependHash + appendHash for fast check. |
| hasher.setHash(_trimHash(frameHash)); |
| } else if (windowHash !== _hash){ |
| //detect if hash changed (manually or using setHash) |
| _registerChange(windowHash); |
| } |
| }; |
| } else { |
| /** |
| * @private |
| */ |
| _checkHistory = function(){ |
| var windowHash = _getWindowHash(); |
| if(windowHash !== _hash){ |
| _registerChange(windowHash); |
| } |
| }; |
| } |
| |
| function _addListener(elm, eType, fn){ |
| if(elm.addEventListener){ |
| elm.addEventListener(eType, fn, false); |
| } else if (elm.attachEvent){ |
| elm.attachEvent('on' + eType, fn); |
| } |
| } |
| |
| function _removeListener(elm, eType, fn){ |
| if(elm.removeEventListener){ |
| elm.removeEventListener(eType, fn, false); |
| } else if (elm.detachEvent){ |
| elm.detachEvent('on' + eType, fn); |
| } |
| } |
| |
| function _makePath(paths){ |
| paths = Array.prototype.slice.call(arguments); |
| |
| var path = paths.join(hasher.separator); |
| path = path? hasher.prependHash + path.replace(_hashRegexp, '') + hasher.appendHash : path; |
| return path; |
| } |
| |
| function _encodePath(path){ |
| //used encodeURI instead of encodeURIComponent to preserve '?', '/', |
| //'#'. Fixes Safari bug [issue #8] |
| path = encodeURI(path); |
| if(_isIE && _isLocal){ |
| //fix IE8 local file bug [issue #6] |
| path = path.replace(/\?/, '%3F'); |
| } |
| return path; |
| } |
| |
| //-------------------------------------------------------------------------------------- |
| // Public (API) |
| //-------------------------------------------------------------------------------------- |
| |
| hasher = /** @lends hasher */ { |
| |
| /** |
| * hasher Version Number |
| * @type string |
| * @constant |
| */ |
| VERSION : '1.2.0', |
| |
| /** |
| * Boolean deciding if hasher encodes/decodes the hash or not. |
| * <ul> |
| * <li>default value: false;</li> |
| * </ul> |
| * @type boolean |
| */ |
| raw : false, |
| |
| /** |
| * String that should always be added to the end of Hash value. |
| * <ul> |
| * <li>default value: '';</li> |
| * <li>will be automatically removed from `hasher.getHash()`</li> |
| * <li>avoid conflicts with elements that contain ID equal to hash value;</li> |
| * </ul> |
| * @type string |
| */ |
| appendHash : '', |
| |
| /** |
| * String that should always be added to the beginning of Hash value. |
| * <ul> |
| * <li>default value: '/';</li> |
| * <li>will be automatically removed from `hasher.getHash()`</li> |
| * <li>avoid conflicts with elements that contain ID equal to hash value;</li> |
| * </ul> |
| * @type string |
| */ |
| prependHash : '/', |
| |
| /** |
| * String used to split hash paths; used by `hasher.getHashAsArray()` to split paths. |
| * <ul> |
| * <li>default value: '/';</li> |
| * </ul> |
| * @type string |
| */ |
| separator : '/', |
| |
| /** |
| * Signal dispatched when hash value changes. |
| * - pass current hash as 1st parameter to listeners and previous hash value as 2nd parameter. |
| * @type signals.Signal |
| */ |
| changed : new Signal(), |
| |
| /** |
| * Signal dispatched when hasher is stopped. |
| * - pass current hash as first parameter to listeners |
| * @type signals.Signal |
| */ |
| stopped : new Signal(), |
| |
| /** |
| * Signal dispatched when hasher is initialized. |
| * - pass current hash as first parameter to listeners. |
| * @type signals.Signal |
| */ |
| initialized : new Signal(), |
| |
| /** |
| * Start listening/dispatching changes in the hash/history. |
| * <ul> |
| * <li>hasher won't dispatch CHANGE events by manually typing a new value or pressing the back/forward buttons before calling this method.</li> |
| * </ul> |
| */ |
| init : function(){ |
| if(_isActive) return; |
| |
| _hash = _getWindowHash(); |
| |
| //thought about branching/overloading hasher.init() to avoid checking multiple times but |
| //don't think worth doing it since it probably won't be called multiple times. |
| if(_isHashChangeSupported){ |
| _addListener(window, 'hashchange', _checkHistory); |
| }else { |
| if(_isLegacyIE){ |
| if(! _frame){ |
| _createFrame(); |
| } |
| _updateFrame(); |
| } |
| _checkInterval = setInterval(_checkHistory, POOL_INTERVAL); |
| } |
| |
| _isActive = true; |
| hasher.initialized.dispatch(_trimHash(_hash)); |
| }, |
| |
| /** |
| * Stop listening/dispatching changes in the hash/history. |
| * <ul> |
| * <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> |
| * <li>hasher will still dispatch changes made programatically by calling hasher.setHash();</li> |
| * </ul> |
| */ |
| stop : function(){ |
| if(! _isActive) return; |
| |
| if(_isHashChangeSupported){ |
| _removeListener(window, 'hashchange', _checkHistory); |
| }else{ |
| clearInterval(_checkInterval); |
| _checkInterval = null; |
| } |
| |
| _isActive = false; |
| hasher.stopped.dispatch(_trimHash(_hash)); |
| }, |
| |
| /** |
| * @return {boolean} If hasher is listening to changes on the browser history and/or hash value. |
| */ |
| isActive : function(){ |
| return _isActive; |
| }, |
| |
| /** |
| * @return {string} Full URL. |
| */ |
| getURL : function(){ |
| return window.location.href; |
| }, |
| |
| /** |
| * @return {string} Retrieve URL without query string and hash. |
| */ |
| getBaseURL : function(){ |
| return hasher.getURL().replace(_baseUrlRegexp, ''); //removes everything after '?' and/or '#' |
| }, |
| |
| /** |
| * Set Hash value, generating a new history record. |
| * @param {...string} path Hash value without '#'. Hasher will join |
| * path segments using `hasher.separator` and prepend/append hash value |
| * with `hasher.appendHash` and `hasher.prependHash` |
| * @example hasher.setHash('lorem', 'ipsum', 'dolor') -> '#/lorem/ipsum/dolor' |
| */ |
| setHash : function(path){ |
| path = _makePath.apply(null, arguments); |
| if(path !== _hash){ |
| // we should store raw value |
| _registerChange(path); |
| if (path === _hash) { |
| // we check if path is still === _hash to avoid error in |
| // case of multiple consecutive redirects [issue #39] |
| if (! hasher.raw) { |
| path = _encodePath(path); |
| } |
| window.location.hash = '#' + path; |
| } |
| } |
| }, |
| |
| /** |
| * Set Hash value without keeping previous hash on the history record. |
| * Similar to calling `window.location.replace("#/hash")` but will also work on IE6-7. |
| * @param {...string} path Hash value without '#'. Hasher will join |
| * path segments using `hasher.separator` and prepend/append hash value |
| * with `hasher.appendHash` and `hasher.prependHash` |
| * @example hasher.replaceHash('lorem', 'ipsum', 'dolor') -> '#/lorem/ipsum/dolor' |
| */ |
| replaceHash : function(path){ |
| path = _makePath.apply(null, arguments); |
| if(path !== _hash){ |
| // we should store raw value |
| _registerChange(path, true); |
| if (path === _hash) { |
| // we check if path is still === _hash to avoid error in |
| // case of multiple consecutive redirects [issue #39] |
| if (! hasher.raw) { |
| path = _encodePath(path); |
| } |
| window.location.replace('#' + path); |
| } |
| } |
| }, |
| |
| /** |
| * @return {string} Hash value without '#', `hasher.appendHash` and `hasher.prependHash`. |
| */ |
| getHash : function(){ |
| //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. |
| return _trimHash(_hash); |
| }, |
| |
| /** |
| * @return {Array.<string>} Hash value split into an Array. |
| */ |
| getHashAsArray : function(){ |
| return hasher.getHash().split(hasher.separator); |
| }, |
| |
| /** |
| * Removes all event listeners, stops hasher and destroy hasher object. |
| * - IMPORTANT: hasher won't work after calling this method, hasher Object will be deleted. |
| */ |
| dispose : function(){ |
| hasher.stop(); |
| hasher.initialized.dispose(); |
| hasher.stopped.dispose(); |
| hasher.changed.dispose(); |
| _frame = hasher = window.hasher = null; |
| }, |
| |
| /** |
| * @return {string} A string representation of the object. |
| */ |
| toString : function(){ |
| return '[hasher version="'+ hasher.VERSION +'" hash="'+ hasher.getHash() +'"]'; |
| } |
| |
| }; |
| |
| hasher.initialized.memorize = true; //see #33 |
| |
| return hasher; |
| |
| }(window)); |
| |
| |
| return hasher; |
| }; |
| |
| if (typeof define === 'function' && define.amd) { |
| define(['signals'], factory); |
| } else if (typeof exports === 'object') { |
| module.exports = factory(require('signals')); |
| } else { |
| /*jshint sub:true */ |
| window['hasher'] = factory(window['signals']); |
| } |
| |
| }()); |