      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