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