Roo/History.js
[roojs1] / Roo / History.js
1 /**
2  * Originally based of this code... - refactored for Roo...
3  *
4  * History.js Core
5  * @author Benjamin Arthur Lupton <contact@balupton.com>
6  * @copyright 2010-2011 Benjamin Arthur Lupton <contact@balupton.com>
7  * @license New BSD License <http://creativecommons.org/licenses/BSD/>
8  */
9
10 Roo.History = {
11          
12      
13     // ====================================================================
14         // Options
15
16     
17     /**
18      * hashChangeInterval
19      * How long should the interval be before hashchange checks
20      */
21     thishashChangeInterval  : 100,
22     
23     /**
24      * safariPollInterval
25      * How long should the interval be before safari poll checks
26      */
27     safariPollInterval : 500,
28     
29     /**
30      * History.options.doubleCheckInterval
31      * How long should the interval be before we perform a double check
32      */
33     doubleCheckInterval : 500,
34     
35     /**
36      * History.options.disableSuid
37      * Force this.not to append suid
38      */
39     disableSuid : false,
40     
41     /**
42      * History.options.storeInterval
43      * How long should we wait between store calls
44      */
45     storeInterval : 1000,
46     
47     /**
48      * History.options.busyDelay
49      * How long should we wait between busy events
50      */
51     busyDelay : 250,
52     
53     /**
54      * History.options.debug
55      * If true will enable debug messages to be logged
56      */
57     debug : false,
58     
59     /**
60      * History.options.initialTitle
61      * What is the title of the initial state
62      */
63     initialTitle : '',
64     
65     /**
66      * History.options.html4Mode
67      * If true, will force HTMl4 mode (hashtags)
68      */
69     html4Mode : false,
70     
71     /**
72      * History.options.delayInit
73      * Want to override default options and call init manually.
74      */
75     delayInit : false,
76     
77     /**
78     * History.bugs
79     * Which bugs are present
80     */
81     bugs : {
82         /**
83         * Safari 5 and Safari iOS 4 fail to return to the correct state once a hash is replaced by a `replaceState` call
84         * https://bugs.webkit.org/show_bug.cgi?id=56249
85         */
86         setHash: false,
87
88        /**
89         * Safari 5 and Safari iOS 4 sometimes fail to apply the state change under busy conditions
90         * https://bugs.webkit.org/show_bug.cgi?id=42940
91         */
92        safariPoll: false,
93
94        /**
95         * MSIE 6 and 7 sometimes do not apply a hash even it was told to (requiring a second call to the apply function)
96         */
97        ieDoubleCheck: false,
98
99        /**
100         * MSIE 6 requires the entire hash to be encoded for the hashes to trigger the onHashChange event
101         */
102        hashEscape: false,
103     },
104      
105         // ========================================================================
106         // Initialise
107
108         // Localise Globals
109         
110                   
111         sessionStorage : false, // sessionStorage
112                  
113     intervalList : false, // array normally.
114
115         // Initialise History
116         init : function(options){
117         
118         initialTitle : window.document.title,
119         
120         Roo.apply(this,options)
121         
122                 // Check Load Status of Adapter
123                 //if ( typeof this.Adapter === 'undefined' ) {
124                 //      return false;
125                 //}
126
127                 // Check Load Status of Core
128                 if ( typeof this.initCore !== 'undefined' ) {
129                         this.initCore();
130                 }
131
132                 // Check Load Status of HTML4 Support
133                 if ( typeof this.initHtml4 !== 'undefined' ) {
134                         this.initHtml4();
135                 }
136
137                 // Return true
138                 return true;
139         },
140
141
142         // ========================================================================
143         // Initialise Core
144
145         // Initialise Core
146         initCore : function(options){
147                 // Initialise
148         this.intervalList = [];
149         
150         
151         try {
152             this.sessionStorage = window.sessionStorage; // This will throw an exception in some browsers when cookies/localStorage are explicitly disabled (i.e. Chrome)
153             this.sessionStorage.setItem('TEST', '1');
154             this.sessionStorage.removeItem('TEST');
155         } catch(e) {
156             this.sessionStorage = false;
157         }
158         
159         
160                 if ( typeof this.initCore.initialized !== 'undefined' ) {
161                         // Already Loaded
162                         return false;
163                 }
164                 else {
165                         this.initCore.initialized = true;
166                 }
167         
168
169     },
170                  
171
172     /**
173      * History.clearAllIntervals
174      * Clears all setInterval instances.
175      */
176     clearAllIntervals: function()
177     {
178         var i, il = this.intervalList;
179         if (typeof il !== "undefined" && il !== null) {
180             for (i = 0; i < il.length; i++) {
181                 clearInterval(il[i]);
182             }
183             this.intervalList = null;
184         }
185     },
186
187
188     // ====================================================================
189     // Debug
190
191     /**
192      * History.debugLog(message,...)
193      * Logs the passed arguments if debug enabled
194      */
195     debugLog : function()
196     {
197         if ( (this.debug||false) ) {
198             Roo.log.apply(History,arguments);
199         }
200     },
201
202                  
203
204     // ====================================================================
205     // Emulated Status
206
207     /**
208      * History.getInternetExplorerMajorVersion()
209      * Get's the major version of Internet Explorer
210      * @return {integer}
211      * @license Public Domain
212      * @author Benjamin Arthur Lupton <contact@balupton.com>
213      * @author James Padolsey <https://gist.github.com/527683>
214      */
215     getInternetExplorerMajorVersion = function(){
216         var result = this.getInternetExplorerMajorVersion.cached =
217                 (typeof this.getInternetExplorerMajorVersion.cached !== 'undefined')
218             ?   this.getInternetExplorerMajorVersion.cached
219             :   (function(){
220                     var v = 3,
221                             div = window.document.createElement('div'),
222                             all = div.getElementsByTagName('i');
223                     while ( (div.innerHTML = '<!--[if gt IE ' + (++v) + ']><i></i><![endif]-->') && all[0] ) {}
224                     return (v > 4) ? v : false;
225                 })()
226             ;
227         return result;
228     };
229
230     /**
231      * isInternetExplorer()
232      * Are we using Internet Explorer?
233      * @return {boolean}
234      * @license Public Domain
235      * @author Benjamin Arthur Lupton <contact@balupton.com>
236      */
237     isInternetExplorer = function(){
238         var result =
239             this.isInternetExplorer.cached =
240             (typeof this.isInternetExplorer.cached !== 'undefined')
241                 ?       this.isInternetExplorer.cached
242                 :       Boolean(this.getInternetExplorerMajorVersion())
243             ;
244         return result;
245     },
246     /**
247      * emulated
248      * Which features require emulating?
249      */
250
251     emulated : {
252         pushState : true,
253         hashChange: true
254     },
255     
256     initEmulated : function()
257     {
258     
259         
260                 if (this.html4Mode) {
261                         return;
262                 }
263
264                 
265
266         this.emulated.pushState = !Boolean(
267                                         window.history && window.history.pushState && window.history.replaceState
268                                         && !(
269                                                 (/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i).test(window.navigator.userAgent) /* disable for versions of iOS before version 4.3 (8F190) */
270                                                 || (/AppleWebKit\/5([0-2]|3[0-2])/i).test(window.navigator.userAgent) /* disable for the mercury iOS browser, or at least older versions of the webkit engine */
271                                         )
272                                 );
273         this.emulated.hashChange = Boolean(
274                                         !(('onhashchange' in window) || ('onhashchange' in window.document))
275                                         ||
276                                         (History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 8)
277                                 );
278                         
279         }
280
281                 /**
282                  * History.enabled
283                  * Is History enabled?
284                  */
285                 History.enabled = !History.emulated.pushState;
286
287                 
288
289                 /**
290                  * History.isEmptyObject(obj)
291                  * Checks to see if the Object is Empty
292                  * @param {Object} obj
293                  * @return {boolean}
294                  */
295                 History.isEmptyObject = function(obj) {
296                         for ( var name in obj ) {
297                                 if ( obj.hasOwnProperty(name) ) {
298                                         return false;
299                                 }
300                         }
301                         return true;
302                 };
303
304                 /**
305                  * History.cloneObject(obj)
306                  * Clones a object and eliminate all references to the original contexts
307                  * @param {Object} obj
308                  * @return {Object}
309                  */
310                 History.cloneObject = function(obj) {
311                         var hash,newObj;
312                         if ( obj ) {
313                                 hash = JSON.stringify(obj);
314                                 newObj = JSON.parse(hash);
315                         }
316                         else {
317                                 newObj = {};
318                         }
319                         return newObj;
320                 };
321
322
323                 // ====================================================================
324                 // URL Helpers
325
326                 /**
327                  * History.getRootUrl()
328                  * Turns "http://mysite.com/dir/page.html?asd" into "http://mysite.com"
329                  * @return {String} rootUrl
330                  */
331                 History.getRootUrl = function(){
332                         // Create
333                         var rootUrl = window.document.location.protocol+'//'+(window.document.location.hostname||window.document.location.host);
334                         if ( window.document.location.port||false ) {
335                                 rootUrl += ':'+window.document.location.port;
336                         }
337                         rootUrl += '/';
338
339                         // Return
340                         return rootUrl;
341                 };
342
343                 /**
344                  * History.getBaseHref()
345                  * Fetches the `href` attribute of the `<base href="...">` element if it exists
346                  * @return {String} baseHref
347                  */
348                 History.getBaseHref = function(){
349                         // Create
350                         var
351                                 baseElements = window.document.getElementsByTagName('base'),
352                                 baseElement = null,
353                                 baseHref = '';
354
355                         // Test for Base Element
356                         if ( baseElements.length === 1 ) {
357                                 // Prepare for Base Element
358                                 baseElement = baseElements[0];
359                                 baseHref = baseElement.href.replace(/[^\/]+$/,'');
360                         }
361
362                         // Adjust trailing slash
363                         baseHref = baseHref.replace(/\/+$/,'');
364                         if ( baseHref ) baseHref += '/';
365
366                         // Return
367                         return baseHref;
368                 };
369
370                 /**
371                  * History.getBaseUrl()
372                  * Fetches the baseHref or basePageUrl or rootUrl (whichever one exists first)
373                  * @return {String} baseUrl
374                  */
375                 History.getBaseUrl = function(){
376                         // Create
377                         var baseUrl = History.getBaseHref()||History.getBasePageUrl()||History.getRootUrl();
378
379                         // Return
380                         return baseUrl;
381                 };
382
383                 /**
384                  * History.getPageUrl()
385                  * Fetches the URL of the current page
386                  * @return {String} pageUrl
387                  */
388                 History.getPageUrl = function(){
389                         // Fetch
390                         var
391                                 State = History.getState(false,false),
392                                 stateUrl = (State||{}).url||History.getLocationHref(),
393                                 pageUrl;
394
395                         // Create
396                         pageUrl = stateUrl.replace(/\/+$/,'').replace(/[^\/]+$/,function(part,index,string){
397                                 return (/\./).test(part) ? part : part+'/';
398                         });
399
400                         // Return
401                         return pageUrl;
402                 };
403
404                 /**
405                  * History.getBasePageUrl()
406                  * Fetches the Url of the directory of the current page
407                  * @return {String} basePageUrl
408                  */
409                 History.getBasePageUrl = function(){
410                         // Create
411                         var basePageUrl = (History.getLocationHref()).replace(/[#\?].*/,'').replace(/[^\/]+$/,function(part,index,string){
412                                 return (/[^\/]$/).test(part) ? '' : part;
413                         }).replace(/\/+$/,'')+'/';
414
415                         // Return
416                         return basePageUrl;
417                 };
418
419                 /**
420                  * History.getFullUrl(url)
421                  * Ensures that we have an absolute URL and not a relative URL
422                  * @param {string} url
423                  * @param {Boolean} allowBaseHref
424                  * @return {string} fullUrl
425                  */
426                 History.getFullUrl = function(url,allowBaseHref){
427                         // Prepare
428                         var fullUrl = url, firstChar = url.substring(0,1);
429                         allowBaseHref = (typeof allowBaseHref === 'undefined') ? true : allowBaseHref;
430
431                         // Check
432                         if ( /[a-z]+\:\/\//.test(url) ) {
433                                 // Full URL
434                         }
435                         else if ( firstChar === '/' ) {
436                                 // Root URL
437                                 fullUrl = History.getRootUrl()+url.replace(/^\/+/,'');
438                         }
439                         else if ( firstChar === '#' ) {
440                                 // Anchor URL
441                                 fullUrl = History.getPageUrl().replace(/#.*/,'')+url;
442                         }
443                         else if ( firstChar === '?' ) {
444                                 // Query URL
445                                 fullUrl = History.getPageUrl().replace(/[\?#].*/,'')+url;
446                         }
447                         else {
448                                 // Relative URL
449                                 if ( allowBaseHref ) {
450                                         fullUrl = History.getBaseUrl()+url.replace(/^(\.\/)+/,'');
451                                 } else {
452                                         fullUrl = History.getBasePageUrl()+url.replace(/^(\.\/)+/,'');
453                                 }
454                                 // We have an if condition above as we do not want hashes
455                                 // which are relative to the baseHref in our URLs
456                                 // as if the baseHref changes, then all our bookmarks
457                                 // would now point to different locations
458                                 // whereas the basePageUrl will always stay the same
459                         }
460
461                         // Return
462                         return fullUrl.replace(/\#$/,'');
463                 };
464
465                 /**
466                  * History.getShortUrl(url)
467                  * Ensures that we have a relative URL and not a absolute URL
468                  * @param {string} url
469                  * @return {string} url
470                  */
471                 History.getShortUrl = function(url){
472                         // Prepare
473                         var shortUrl = url, baseUrl = History.getBaseUrl(), rootUrl = History.getRootUrl();
474
475                         // Trim baseUrl
476                         if ( History.emulated.pushState ) {
477                                 // We are in a if statement as when pushState is not emulated
478                                 // The actual url these short urls are relative to can change
479                                 // So within the same session, we the url may end up somewhere different
480                                 shortUrl = shortUrl.replace(baseUrl,'');
481                         }
482
483                         // Trim rootUrl
484                         shortUrl = shortUrl.replace(rootUrl,'/');
485
486                         // Ensure we can still detect it as a state
487                         if ( History.isTraditionalAnchor(shortUrl) ) {
488                                 shortUrl = './'+shortUrl;
489                         }
490
491                         // Clean It
492                         shortUrl = shortUrl.replace(/^(\.\/)+/g,'./').replace(/\#$/,'');
493
494                         // Return
495                         return shortUrl;
496                 };
497
498                 /**
499                  * History.getLocationHref(document)
500                  * Returns a normalized version of document.location.href
501                  * accounting for browser inconsistencies, etc.
502                  *
503                  * This URL will be URI-encoded and will include the hash
504                  *
505                  * @param {object} document
506                  * @return {string} url
507                  */
508                 History.getLocationHref = function(doc) {
509                         doc = doc || window.document;
510
511                         // most of the time, this will be true
512                         if (doc.URL === doc.location.href)
513                                 return doc.location.href;
514
515                         // some versions of webkit URI-decode document.location.href
516                         // but they leave document.URL in an encoded state
517                         if (doc.location.href === decodeURIComponent(doc.URL))
518                                 return doc.URL;
519
520                         // FF 3.6 only updates document.URL when a page is reloaded
521                         // document.location.href is updated correctly
522                         if (doc.location.hash && decodeURIComponent(doc.location.href.replace(/^[^#]+/, "")) === doc.location.hash)
523                                 return doc.location.href;
524
525                         if (doc.URL.indexOf('#') == -1 && doc.location.href.indexOf('#') != -1)
526                                 return doc.location.href;
527                         
528                         return doc.URL || doc.location.href;
529                 };
530
531
532                 // ====================================================================
533                 // State Storage
534
535                 /**
536                  * History.store
537                  * The store for all session specific data
538                  */
539                 History.store = {};
540
541                 /**
542                  * History.idToState
543                  * 1-1: State ID to State Object
544                  */
545                 History.idToState = History.idToState||{};
546
547                 /**
548                  * History.stateToId
549                  * 1-1: State String to State ID
550                  */
551                 History.stateToId = History.stateToId||{};
552
553                 /**
554                  * History.urlToId
555                  * 1-1: State URL to State ID
556                  */
557                 History.urlToId = History.urlToId||{};
558
559                 /**
560                  * History.storedStates
561                  * Store the states in an array
562                  */
563                 History.storedStates = History.storedStates||[];
564
565                 /**
566                  * History.savedStates
567                  * Saved the states in an array
568                  */
569                 History.savedStates = History.savedStates||[];
570
571                 /**
572                  * History.noramlizeStore()
573                  * Noramlize the store by adding necessary values
574                  */
575                 History.normalizeStore = function(){
576                         History.store.idToState = History.store.idToState||{};
577                         History.store.urlToId = History.store.urlToId||{};
578                         History.store.stateToId = History.store.stateToId||{};
579                 };
580
581                 /**
582                  * History.getState()
583                  * Get an object containing the data, title and url of the current state
584                  * @param {Boolean} friendly
585                  * @param {Boolean} create
586                  * @return {Object} State
587                  */
588                 History.getState = function(friendly,create){
589                         // Prepare
590                         if ( typeof friendly === 'undefined' ) { friendly = true; }
591                         if ( typeof create === 'undefined' ) { create = true; }
592
593                         // Fetch
594                         var State = History.getLastSavedState();
595
596                         // Create
597                         if ( !State && create ) {
598                                 State = History.createStateObject();
599                         }
600
601                         // Adjust
602                         if ( friendly ) {
603                                 State = History.cloneObject(State);
604                                 State.url = State.cleanUrl||State.url;
605                         }
606
607                         // Return
608                         return State;
609                 };
610
611                 /**
612                  * History.getIdByState(State)
613                  * Gets a ID for a State
614                  * @param {State} newState
615                  * @return {String} id
616                  */
617                 History.getIdByState = function(newState){
618
619                         // Fetch ID
620                         var id = History.extractId(newState.url),
621                                 str;
622
623                         if ( !id ) {
624                                 // Find ID via State String
625                                 str = History.getStateString(newState);
626                                 if ( typeof History.stateToId[str] !== 'undefined' ) {
627                                         id = History.stateToId[str];
628                                 }
629                                 else if ( typeof History.store.stateToId[str] !== 'undefined' ) {
630                                         id = History.store.stateToId[str];
631                                 }
632                                 else {
633                                         // Generate a new ID
634                                         while ( true ) {
635                                                 id = (new Date()).getTime() + String(Math.random()).replace(/\D/g,'');
636                                                 if ( typeof History.idToState[id] === 'undefined' && typeof History.store.idToState[id] === 'undefined' ) {
637                                                         break;
638                                                 }
639                                         }
640
641                                         // Apply the new State to the ID
642                                         History.stateToId[str] = id;
643                                         History.idToState[id] = newState;
644                                 }
645                         }
646
647                         // Return ID
648                         return id;
649                 };
650
651                 /**
652                  * History.normalizeState(State)
653                  * Expands a State Object
654                  * @param {object} State
655                  * @return {object}
656                  */
657                 History.normalizeState = function(oldState){
658                         // Variables
659                         var newState, dataNotEmpty;
660
661                         // Prepare
662                         if ( !oldState || (typeof oldState !== 'object') ) {
663                                 oldState = {};
664                         }
665
666                         // Check
667                         if ( typeof oldState.normalized !== 'undefined' ) {
668                                 return oldState;
669                         }
670
671                         // Adjust
672                         if ( !oldState.data || (typeof oldState.data !== 'object') ) {
673                                 oldState.data = {};
674                         }
675
676                         // ----------------------------------------------------------------
677
678                         // Create
679                         newState = {};
680                         newState.normalized = true;
681                         newState.title = oldState.title||'';
682                         newState.url = History.getFullUrl(oldState.url?oldState.url:(History.getLocationHref()));
683                         newState.hash = History.getShortUrl(newState.url);
684                         newState.data = History.cloneObject(oldState.data);
685
686                         // Fetch ID
687                         newState.id = History.getIdByState(newState);
688
689                         // ----------------------------------------------------------------
690
691                         // Clean the URL
692                         newState.cleanUrl = newState.url.replace(/\??\&_suid.*/,'');
693                         newState.url = newState.cleanUrl;
694
695                         // Check to see if we have more than just a url
696                         dataNotEmpty = !History.isEmptyObject(newState.data);
697
698                         // Apply
699                         if ( (newState.title || dataNotEmpty) && History.options.disableSuid !== true ) {
700                                 // Add ID to Hash
701                                 newState.hash = History.getShortUrl(newState.url).replace(/\??\&_suid.*/,'');
702                                 if ( !/\?/.test(newState.hash) ) {
703                                         newState.hash += '?';
704                                 }
705                                 newState.hash += '&_suid='+newState.id;
706                         }
707
708                         // Create the Hashed URL
709                         newState.hashedUrl = History.getFullUrl(newState.hash);
710
711                         // ----------------------------------------------------------------
712
713                         // Update the URL if we have a duplicate
714                         if ( (History.emulated.pushState || History.bugs.safariPoll) && History.hasUrlDuplicate(newState) ) {
715                                 newState.url = newState.hashedUrl;
716                         }
717
718                         // ----------------------------------------------------------------
719
720                         // Return
721                         return newState;
722                 };
723
724                 /**
725                  * History.createStateObject(data,title,url)
726                  * Creates a object based on the data, title and url state params
727                  * @param {object} data
728                  * @param {string} title
729                  * @param {string} url
730                  * @return {object}
731                  */
732                 History.createStateObject = function(data,title,url){
733                         // Hashify
734                         var State = {
735                                 'data': data,
736                                 'title': title,
737                                 'url': url
738                         };
739
740                         // Expand the State
741                         State = History.normalizeState(State);
742
743                         // Return object
744                         return State;
745                 };
746
747                 /**
748                  * History.getStateById(id)
749                  * Get a state by it's UID
750                  * @param {String} id
751                  */
752                 History.getStateById = function(id){
753                         // Prepare
754                         id = String(id);
755
756                         // Retrieve
757                         var State = History.idToState[id] || History.store.idToState[id] || undefined;
758
759                         // Return State
760                         return State;
761                 };
762
763                 /**
764                  * Get a State's String
765                  * @param {State} passedState
766                  */
767                 History.getStateString = function(passedState){
768                         // Prepare
769                         var State, cleanedState, str;
770
771                         // Fetch
772                         State = History.normalizeState(passedState);
773
774                         // Clean
775                         cleanedState = {
776                                 data: State.data,
777                                 title: passedState.title,
778                                 url: passedState.url
779                         };
780
781                         // Fetch
782                         str = JSON.stringify(cleanedState);
783
784                         // Return
785                         return str;
786                 };
787
788                 /**
789                  * Get a State's ID
790                  * @param {State} passedState
791                  * @return {String} id
792                  */
793                 History.getStateId = function(passedState){
794                         // Prepare
795                         var State, id;
796
797                         // Fetch
798                         State = History.normalizeState(passedState);
799
800                         // Fetch
801                         id = State.id;
802
803                         // Return
804                         return id;
805                 };
806
807                 /**
808                  * History.getHashByState(State)
809                  * Creates a Hash for the State Object
810                  * @param {State} passedState
811                  * @return {String} hash
812                  */
813                 History.getHashByState = function(passedState){
814                         // Prepare
815                         var State, hash;
816
817                         // Fetch
818                         State = History.normalizeState(passedState);
819
820                         // Hash
821                         hash = State.hash;
822
823                         // Return
824                         return hash;
825                 };
826
827                 /**
828                  * History.extractId(url_or_hash)
829                  * Get a State ID by it's URL or Hash
830                  * @param {string} url_or_hash
831                  * @return {string} id
832                  */
833                 History.extractId = function ( url_or_hash ) {
834                         // Prepare
835                         var id,parts,url, tmp;
836
837                         // Extract
838                         
839                         // If the URL has a #, use the id from before the #
840                         if (url_or_hash.indexOf('#') != -1)
841                         {
842                                 tmp = url_or_hash.split("#")[0];
843                         }
844                         else
845                         {
846                                 tmp = url_or_hash;
847                         }
848                         
849                         parts = /(.*)\&_suid=([0-9]+)$/.exec(tmp);
850                         url = parts ? (parts[1]||url_or_hash) : url_or_hash;
851                         id = parts ? String(parts[2]||'') : '';
852
853                         // Return
854                         return id||false;
855                 };
856
857                 /**
858                  * History.isTraditionalAnchor
859                  * Checks to see if the url is a traditional anchor or not
860                  * @param {String} url_or_hash
861                  * @return {Boolean}
862                  */
863                 History.isTraditionalAnchor = function(url_or_hash){
864                         // Check
865                         var isTraditional = !(/[\/\?\.]/.test(url_or_hash));
866
867                         // Return
868                         return isTraditional;
869                 };
870
871                 /**
872                  * History.extractState
873                  * Get a State by it's URL or Hash
874                  * @param {String} url_or_hash
875                  * @return {State|null}
876                  */
877                 History.extractState = function(url_or_hash,create){
878                         // Prepare
879                         var State = null, id, url;
880                         create = create||false;
881
882                         // Fetch SUID
883                         id = History.extractId(url_or_hash);
884                         if ( id ) {
885                                 State = History.getStateById(id);
886                         }
887
888                         // Fetch SUID returned no State
889                         if ( !State ) {
890                                 // Fetch URL
891                                 url = History.getFullUrl(url_or_hash);
892
893                                 // Check URL
894                                 id = History.getIdByUrl(url)||false;
895                                 if ( id ) {
896                                         State = History.getStateById(id);
897                                 }
898
899                                 // Create State
900                                 if ( !State && create && !History.isTraditionalAnchor(url_or_hash) ) {
901                                         State = History.createStateObject(null,null,url);
902                                 }
903                         }
904
905                         // Return
906                         return State;
907                 };
908
909                 /**
910                  * History.getIdByUrl()
911                  * Get a State ID by a State URL
912                  */
913                 History.getIdByUrl = function(url){
914                         // Fetch
915                         var id = History.urlToId[url] || History.store.urlToId[url] || undefined;
916
917                         // Return
918                         return id;
919                 };
920
921                 /**
922                  * History.getLastSavedState()
923                  * Get an object containing the data, title and url of the current state
924                  * @return {Object} State
925                  */
926                 History.getLastSavedState = function(){
927                         return History.savedStates[History.savedStates.length-1]||undefined;
928                 };
929
930                 /**
931                  * History.getLastStoredState()
932                  * Get an object containing the data, title and url of the current state
933                  * @return {Object} State
934                  */
935                 History.getLastStoredState = function(){
936                         return History.storedStates[History.storedStates.length-1]||undefined;
937                 };
938
939                 /**
940                  * History.hasUrlDuplicate
941                  * Checks if a Url will have a url conflict
942                  * @param {Object} newState
943                  * @return {Boolean} hasDuplicate
944                  */
945                 History.hasUrlDuplicate = function(newState) {
946                         // Prepare
947                         var hasDuplicate = false,
948                                 oldState;
949
950                         // Fetch
951                         oldState = History.extractState(newState.url);
952
953                         // Check
954                         hasDuplicate = oldState && oldState.id !== newState.id;
955
956                         // Return
957                         return hasDuplicate;
958                 };
959
960                 /**
961                  * History.storeState
962                  * Store a State
963                  * @param {Object} newState
964                  * @return {Object} newState
965                  */
966                 History.storeState = function(newState){
967                         // Store the State
968                         History.urlToId[newState.url] = newState.id;
969
970                         // Push the State
971                         History.storedStates.push(History.cloneObject(newState));
972
973                         // Return newState
974                         return newState;
975                 };
976
977                 /**
978                  * History.isLastSavedState(newState)
979                  * Tests to see if the state is the last state
980                  * @param {Object} newState
981                  * @return {boolean} isLast
982                  */
983                 History.isLastSavedState = function(newState){
984                         // Prepare
985                         var isLast = false,
986                                 newId, oldState, oldId;
987
988                         // Check
989                         if ( History.savedStates.length ) {
990                                 newId = newState.id;
991                                 oldState = History.getLastSavedState();
992                                 oldId = oldState.id;
993
994                                 // Check
995                                 isLast = (newId === oldId);
996                         }
997
998                         // Return
999                         return isLast;
1000                 };
1001
1002                 /**
1003                  * History.saveState
1004                  * Push a State
1005                  * @param {Object} newState
1006                  * @return {boolean} changed
1007                  */
1008                 History.saveState = function(newState){
1009                         // Check Hash
1010                         if ( History.isLastSavedState(newState) ) {
1011                                 return false;
1012                         }
1013
1014                         // Push the State
1015                         History.savedStates.push(History.cloneObject(newState));
1016
1017                         // Return true
1018                         return true;
1019                 };
1020
1021                 /**
1022                  * History.getStateByIndex()
1023                  * Gets a state by the index
1024                  * @param {integer} index
1025                  * @return {Object}
1026                  */
1027                 History.getStateByIndex = function(index){
1028                         // Prepare
1029                         var State = null;
1030
1031                         // Handle
1032                         if ( typeof index === 'undefined' ) {
1033                                 // Get the last inserted
1034                                 State = History.savedStates[History.savedStates.length-1];
1035                         }
1036                         else if ( index < 0 ) {
1037                                 // Get from the end
1038                                 State = History.savedStates[History.savedStates.length+index];
1039                         }
1040                         else {
1041                                 // Get from the beginning
1042                                 State = History.savedStates[index];
1043                         }
1044
1045                         // Return State
1046                         return State;
1047                 };
1048                 
1049                 /**
1050                  * History.getCurrentIndex()
1051                  * Gets the current index
1052                  * @return (integer)
1053                 */
1054                 History.getCurrentIndex = function(){
1055                         // Prepare
1056                         var index = null;
1057                         
1058                         // No states saved
1059                         if(History.savedStates.length < 1) {
1060                                 index = 0;
1061                         }
1062                         else {
1063                                 index = History.savedStates.length-1;
1064                         }
1065                         return index;
1066                 };
1067
1068                 // ====================================================================
1069                 // Hash Helpers
1070
1071                 /**
1072                  * History.getHash()
1073                  * @param {Location=} location
1074                  * Gets the current document hash
1075                  * Note: unlike location.hash, this is guaranteed to return the escaped hash in all browsers
1076                  * @return {string}
1077                  */
1078                 History.getHash = function(doc){
1079                         var url = History.getLocationHref(doc),
1080                                 hash;
1081                         hash = History.getHashByUrl(url);
1082                         return hash;
1083                 };
1084
1085                 /**
1086                  * History.unescapeHash()
1087                  * normalize and Unescape a Hash
1088                  * @param {String} hash
1089                  * @return {string}
1090                  */
1091                 History.unescapeHash = function(hash){
1092                         // Prepare
1093                         var result = History.normalizeHash(hash);
1094
1095                         // Unescape hash
1096                         result = decodeURIComponent(result);
1097
1098                         // Return result
1099                         return result;
1100                 };
1101
1102                 /**
1103                  * History.normalizeHash()
1104                  * normalize a hash across browsers
1105                  * @return {string}
1106                  */
1107                 History.normalizeHash = function(hash){
1108                         // Prepare
1109                         var result = hash.replace(/[^#]*#/,'').replace(/#.*/, '');
1110
1111                         // Return result
1112                         return result;
1113                 };
1114
1115                 /**
1116                  * History.setHash(hash)
1117                  * Sets the document hash
1118                  * @param {string} hash
1119                  * @return {History}
1120                  */
1121                 History.setHash = function(hash,queue){
1122                         // Prepare
1123                         var State, pageUrl;
1124
1125                         // Handle Queueing
1126                         if ( queue !== false && History.busy() ) {
1127                                 // Wait + Push to Queue
1128                                 //History.debug('History.setHash: we must wait', arguments);
1129                                 History.pushQueue({
1130                                         scope: History,
1131                                         callback: History.setHash,
1132                                         args: arguments,
1133                                         queue: queue
1134                                 });
1135                                 return false;
1136                         }
1137
1138                         // Log
1139                         //History.debug('History.setHash: called',hash);
1140
1141                         // Make Busy + Continue
1142                         History.busy(true);
1143
1144                         // Check if hash is a state
1145                         State = History.extractState(hash,true);
1146                         if ( State && !History.emulated.pushState ) {
1147                                 // Hash is a state so skip the setHash
1148                                 //History.debug('History.setHash: Hash is a state so skipping the hash set with a direct pushState call',arguments);
1149
1150                                 // PushState
1151                                 History.pushState(State.data,State.title,State.url,false);
1152                         }
1153                         else if ( History.getHash() !== hash ) {
1154                                 // Hash is a proper hash, so apply it
1155
1156                                 // Handle browser bugs
1157                                 if ( History.bugs.setHash ) {
1158                                         // Fix Safari Bug https://bugs.webkit.org/show_bug.cgi?id=56249
1159
1160                                         // Fetch the base page
1161                                         pageUrl = History.getPageUrl();
1162
1163                                         // Safari hash apply
1164                                         History.pushState(null,null,pageUrl+'#'+hash,false);
1165                                 }
1166                                 else {
1167                                         // Normal hash apply
1168                                         window.document.location.hash = hash;
1169                                 }
1170                         }
1171
1172                         // Chain
1173                         return History;
1174                 };
1175
1176                 /**
1177                  * History.escape()
1178                  * normalize and Escape a Hash
1179                  * @return {string}
1180                  */
1181                 History.escapeHash = function(hash){
1182                         // Prepare
1183                         var result = History.normalizeHash(hash);
1184
1185                         // Escape hash
1186                         result = window.encodeURIComponent(result);
1187
1188                         // IE6 Escape Bug
1189                         if ( !History.bugs.hashEscape ) {
1190                                 // Restore common parts
1191                                 result = result
1192                                         .replace(/\%21/g,'!')
1193                                         .replace(/\%26/g,'&')
1194                                         .replace(/\%3D/g,'=')
1195                                         .replace(/\%3F/g,'?');
1196                         }
1197
1198                         // Return result
1199                         return result;
1200                 };
1201
1202                 /**
1203                  * History.getHashByUrl(url)
1204                  * Extracts the Hash from a URL
1205                  * @param {string} url
1206                  * @return {string} url
1207                  */
1208                 History.getHashByUrl = function(url){
1209                         // Extract the hash
1210                         var hash = String(url)
1211                                 .replace(/([^#]*)#?([^#]*)#?(.*)/, '$2')
1212                                 ;
1213
1214                         // Unescape hash
1215                         hash = History.unescapeHash(hash);
1216
1217                         // Return hash
1218                         return hash;
1219                 };
1220
1221                 /**
1222                  * History.setTitle(title)
1223                  * Applies the title to the document
1224                  * @param {State} newState
1225                  * @return {Boolean}
1226                  */
1227                 History.setTitle = function(newState){
1228                         // Prepare
1229                         var title = newState.title,
1230                                 firstState;
1231
1232                         // Initial
1233                         if ( !title ) {
1234                                 firstState = History.getStateByIndex(0);
1235                                 if ( firstState && firstState.url === newState.url ) {
1236                                         title = firstState.title||History.options.initialTitle;
1237                                 }
1238                         }
1239
1240                         // Apply
1241                         try {
1242                                 window.document.getElementsByTagName('title')[0].innerHTML = title.replace('<','&lt;').replace('>','&gt;').replace(' & ',' &amp; ');
1243                         }
1244                         catch ( Exception ) { }
1245                         window.document.title = title;
1246
1247                         // Chain
1248                         return History;
1249                 };
1250
1251
1252                 // ====================================================================
1253                 // Queueing
1254
1255                 /**
1256                  * History.queues
1257                  * The list of queues to use
1258                  * First In, First Out
1259                  */
1260                 History.queues = [];
1261
1262                 /**
1263                  * History.busy(value)
1264                  * @param {boolean} value [optional]
1265                  * @return {boolean} busy
1266                  */
1267                 History.busy = function(value){
1268                         // Apply
1269                         if ( typeof value !== 'undefined' ) {
1270                                 //History.debug('History.busy: changing ['+(History.busy.flag||false)+'] to ['+(value||false)+']', History.queues.length);
1271                                 History.busy.flag = value;
1272                         }
1273                         // Default
1274                         else if ( typeof History.busy.flag === 'undefined' ) {
1275                                 History.busy.flag = false;
1276                         }
1277
1278                         // Queue
1279                         if ( !History.busy.flag ) {
1280                                 // Execute the next item in the queue
1281                                 window.clearTimeout(History.busy.timeout);
1282                                 var fireNext = function(){
1283                                         var i, queue, item;
1284                                         if ( History.busy.flag ) return;
1285                                         for ( i=History.queues.length-1; i >= 0; --i ) {
1286                                                 queue = History.queues[i];
1287                                                 if ( queue.length === 0 ) continue;
1288                                                 item = queue.shift();
1289                                                 History.fireQueueItem(item);
1290                                                 History.busy.timeout = window.setTimeout(fireNext,History.options.busyDelay);
1291                                         }
1292                                 };
1293                                 History.busy.timeout = window.setTimeout(fireNext,History.options.busyDelay);
1294                         }
1295
1296                         // Return
1297                         return History.busy.flag;
1298                 };
1299
1300                 /**
1301                  * History.busy.flag
1302                  */
1303                 History.busy.flag = false;
1304
1305                 /**
1306                  * History.fireQueueItem(item)
1307                  * Fire a Queue Item
1308                  * @param {Object} item
1309                  * @return {Mixed} result
1310                  */
1311                 History.fireQueueItem = function(item){
1312                         return item.callback.apply(item.scope||History,item.args||[]);
1313                 };
1314
1315                 /**
1316                  * History.pushQueue(callback,args)
1317                  * Add an item to the queue
1318                  * @param {Object} item [scope,callback,args,queue]
1319                  */
1320                 History.pushQueue = function(item){
1321                         // Prepare the queue
1322                         History.queues[item.queue||0] = History.queues[item.queue||0]||[];
1323
1324                         // Add to the queue
1325                         History.queues[item.queue||0].push(item);
1326
1327                         // Chain
1328                         return History;
1329                 };
1330
1331                 /**
1332                  * History.queue (item,queue), (func,queue), (func), (item)
1333                  * Either firs the item now if not busy, or adds it to the queue
1334                  */
1335                 History.queue = function(item,queue){
1336                         // Prepare
1337                         if ( typeof item === 'function' ) {
1338                                 item = {
1339                                         callback: item
1340                                 };
1341                         }
1342                         if ( typeof queue !== 'undefined' ) {
1343                                 item.queue = queue;
1344                         }
1345
1346                         // Handle
1347                         if ( History.busy() ) {
1348                                 History.pushQueue(item);
1349                         } else {
1350                                 History.fireQueueItem(item);
1351                         }
1352
1353                         // Chain
1354                         return History;
1355                 };
1356
1357                 /**
1358                  * History.clearQueue()
1359                  * Clears the Queue
1360                  */
1361                 History.clearQueue = function(){
1362                         History.busy.flag = false;
1363                         History.queues = [];
1364                         return History;
1365                 };
1366
1367
1368                 // ====================================================================
1369                 // IE Bug Fix
1370
1371                 /**
1372                  * History.stateChanged
1373                  * States whether or not the state has changed since the last double check was initialised
1374                  */
1375                 History.stateChanged = false;
1376
1377                 /**
1378                  * History.doubleChecker
1379                  * Contains the timeout used for the double checks
1380                  */
1381                 History.doubleChecker = false;
1382
1383                 /**
1384                  * History.doubleCheckComplete()
1385                  * Complete a double check
1386                  * @return {History}
1387                  */
1388                 History.doubleCheckComplete = function(){
1389                         // Update
1390                         History.stateChanged = true;
1391
1392                         // Clear
1393                         History.doubleCheckClear();
1394
1395                         // Chain
1396                         return History;
1397                 };
1398
1399                 /**
1400                  * History.doubleCheckClear()
1401                  * Clear a double check
1402                  * @return {History}
1403                  */
1404                 History.doubleCheckClear = function(){
1405                         // Clear
1406                         if ( History.doubleChecker ) {
1407                                 window.clearTimeout(History.doubleChecker);
1408                                 History.doubleChecker = false;
1409                         }
1410
1411                         // Chain
1412                         return History;
1413                 };
1414
1415                 /**
1416                  * History.doubleCheck()
1417                  * Create a double check
1418                  * @return {History}
1419                  */
1420                 History.doubleCheck = function(tryAgain){
1421                         // Reset
1422                         History.stateChanged = false;
1423                         History.doubleCheckClear();
1424
1425                         // Fix IE6,IE7 bug where calling history.back or history.forward does not actually change the hash (whereas doing it manually does)
1426                         // Fix Safari 5 bug where sometimes the state does not change: https://bugs.webkit.org/show_bug.cgi?id=42940
1427                         if ( History.bugs.ieDoubleCheck ) {
1428                                 // Apply Check
1429                                 History.doubleChecker = window.setTimeout(
1430                                         function(){
1431                                                 History.doubleCheckClear();
1432                                                 if ( !History.stateChanged ) {
1433                                                         //History.debug('History.doubleCheck: State has not yet changed, trying again', arguments);
1434                                                         // Re-Attempt
1435                                                         tryAgain();
1436                                                 }
1437                                                 return true;
1438                                         },
1439                                         History.options.doubleCheckInterval
1440                                 );
1441                         }
1442
1443                         // Chain
1444                         return History;
1445                 };
1446
1447
1448                 // ====================================================================
1449                 // Safari Bug Fix
1450
1451                 /**
1452                  * History.safariStatePoll()
1453                  * Poll the current state
1454                  * @return {History}
1455                  */
1456                 History.safariStatePoll = function(){
1457                         // Poll the URL
1458
1459                         // Get the Last State which has the new URL
1460                         var
1461                                 urlState = History.extractState(History.getLocationHref()),
1462                                 newState;
1463
1464                         // Check for a difference
1465                         if ( !History.isLastSavedState(urlState) ) {
1466                                 newState = urlState;
1467                         }
1468                         else {
1469                                 return;
1470                         }
1471
1472                         // Check if we have a state with that url
1473                         // If not create it
1474                         if ( !newState ) {
1475                                 //History.debug('History.safariStatePoll: new');
1476                                 newState = History.createStateObject();
1477                         }
1478
1479                         // Apply the New State
1480                         //History.debug('History.safariStatePoll: trigger');
1481                         History.Adapter.trigger(window,'popstate');
1482
1483                         // Chain
1484                         return History;
1485                 };
1486
1487
1488                 // ====================================================================
1489                 // State Aliases
1490
1491                 /**
1492                  * History.back(queue)
1493                  * Send the browser history back one item
1494                  * @param {Integer} queue [optional]
1495                  */
1496                 History.back = function(queue){
1497                         //History.debug('History.back: called', arguments);
1498
1499                         // Handle Queueing
1500                         if ( queue !== false && History.busy() ) {
1501                                 // Wait + Push to Queue
1502                                 //History.debug('History.back: we must wait', arguments);
1503                                 History.pushQueue({
1504                                         scope: History,
1505                                         callback: History.back,
1506                                         args: arguments,
1507                                         queue: queue
1508                                 });
1509                                 return false;
1510                         }
1511
1512                         // Make Busy + Continue
1513                         History.busy(true);
1514
1515                         // Fix certain browser bugs that prevent the state from changing
1516                         History.doubleCheck(function(){
1517                                 History.back(false);
1518                         });
1519
1520                         // Go back
1521                         history.go(-1);
1522
1523                         // End back closure
1524                         return true;
1525                 };
1526
1527                 /**
1528                  * History.forward(queue)
1529                  * Send the browser history forward one item
1530                  * @param {Integer} queue [optional]
1531                  */
1532                 History.forward = function(queue){
1533                         //History.debug('History.forward: called', arguments);
1534
1535                         // Handle Queueing
1536                         if ( queue !== false && History.busy() ) {
1537                                 // Wait + Push to Queue
1538                                 //History.debug('History.forward: we must wait', arguments);
1539                                 History.pushQueue({
1540                                         scope: History,
1541                                         callback: History.forward,
1542                                         args: arguments,
1543                                         queue: queue
1544                                 });
1545                                 return false;
1546                         }
1547
1548                         // Make Busy + Continue
1549                         History.busy(true);
1550
1551                         // Fix certain browser bugs that prevent the state from changing
1552                         History.doubleCheck(function(){
1553                                 History.forward(false);
1554                         });
1555
1556                         // Go forward
1557                         history.go(1);
1558
1559                         // End forward closure
1560                         return true;
1561                 };
1562
1563                 /**
1564                  * History.go(index,queue)
1565                  * Send the browser history back or forward index times
1566                  * @param {Integer} queue [optional]
1567                  */
1568                 History.go = function(index,queue){
1569                         //History.debug('History.go: called', arguments);
1570
1571                         // Prepare
1572                         var i;
1573
1574                         // Handle
1575                         if ( index > 0 ) {
1576                                 // Forward
1577                                 for ( i=1; i<=index; ++i ) {
1578                                         History.forward(queue);
1579                                 }
1580                         }
1581                         else if ( index < 0 ) {
1582                                 // Backward
1583                                 for ( i=-1; i>=index; --i ) {
1584                                         History.back(queue);
1585                                 }
1586                         }
1587                         else {
1588                                 throw new Error('History.go: History.go requires a positive or negative integer passed.');
1589                         }
1590
1591                         // Chain
1592                         return History;
1593                 };
1594
1595
1596                 // ====================================================================
1597                 // HTML5 State Support
1598
1599                 // Non-Native pushState Implementation
1600                 if ( History.emulated.pushState ) {
1601                         /*
1602                          * Provide Skeleton for HTML4 Browsers
1603                          */
1604
1605                         // Prepare
1606                         var emptyFunction = function(){};
1607                         History.pushState = History.pushState||emptyFunction;
1608                         History.replaceState = History.replaceState||emptyFunction;
1609                 } // History.emulated.pushState
1610
1611                 // Native pushState Implementation
1612                 else {
1613                         /*
1614                          * Use native HTML5 History API Implementation
1615                          */
1616
1617                         /**
1618                          * History.onPopState(event,extra)
1619                          * Refresh the Current State
1620                          */
1621                         History.onPopState = function(event,extra){
1622                                 // Prepare
1623                                 var stateId = false, newState = false, currentHash, currentState;
1624
1625                                 // Reset the double check
1626                                 History.doubleCheckComplete();
1627
1628                                 // Check for a Hash, and handle apporiatly
1629                                 currentHash = History.getHash();
1630                                 if ( currentHash ) {
1631                                         // Expand Hash
1632                                         currentState = History.extractState(currentHash||History.getLocationHref(),true);
1633                                         if ( currentState ) {
1634                                                 // We were able to parse it, it must be a State!
1635                                                 // Let's forward to replaceState
1636                                                 //History.debug('History.onPopState: state anchor', currentHash, currentState);
1637                                                 History.replaceState(currentState.data, currentState.title, currentState.url, false);
1638                                         }
1639                                         else {
1640                                                 // Traditional Anchor
1641                                                 //History.debug('History.onPopState: traditional anchor', currentHash);
1642                                                 History.Adapter.trigger(window,'anchorchange');
1643                                                 History.busy(false);
1644                                         }
1645
1646                                         // We don't care for hashes
1647                                         History.expectedStateId = false;
1648                                         return false;
1649                                 }
1650
1651                                 // Ensure
1652                                 stateId = History.Adapter.extractEventData('state',event,extra) || false;
1653
1654                                 // Fetch State
1655                                 if ( stateId ) {
1656                                         // Vanilla: Back/forward button was used
1657                                         newState = History.getStateById(stateId);
1658                                 }
1659                                 else if ( History.expectedStateId ) {
1660                                         // Vanilla: A new state was pushed, and popstate was called manually
1661                                         newState = History.getStateById(History.expectedStateId);
1662                                 }
1663                                 else {
1664                                         // Initial State
1665                                         newState = History.extractState(History.getLocationHref());
1666                                 }
1667
1668                                 // The State did not exist in our store
1669                                 if ( !newState ) {
1670                                         // Regenerate the State
1671                                         newState = History.createStateObject(null,null,History.getLocationHref());
1672                                 }
1673
1674                                 // Clean
1675                                 History.expectedStateId = false;
1676
1677                                 // Check if we are the same state
1678                                 if ( History.isLastSavedState(newState) ) {
1679                                         // There has been no change (just the page's hash has finally propagated)
1680                                         //History.debug('History.onPopState: no change', newState, History.savedStates);
1681                                         History.busy(false);
1682                                         return false;
1683                                 }
1684
1685                                 // Store the State
1686                                 History.storeState(newState);
1687                                 History.saveState(newState);
1688
1689                                 // Force update of the title
1690                                 History.setTitle(newState);
1691
1692                                 // Fire Our Event
1693                                 History.Adapter.trigger(window,'statechange');
1694                                 History.busy(false);
1695
1696                                 // Return true
1697                                 return true;
1698                         };
1699                         History.Adapter.bind(window,'popstate',History.onPopState);
1700
1701                         /**
1702                          * History.pushState(data,title,url)
1703                          * Add a new State to the history object, become it, and trigger onpopstate
1704                          * We have to trigger for HTML4 compatibility
1705                          * @param {object} data
1706                          * @param {string} title
1707                          * @param {string} url
1708                          * @return {true}
1709                          */
1710                         History.pushState = function(data,title,url,queue){
1711                                 //History.debug('History.pushState: called', arguments);
1712
1713                                 // Check the State
1714                                 if ( History.getHashByUrl(url) && History.emulated.pushState ) {
1715                                         throw new Error('History.js does not support states with fragement-identifiers (hashes/anchors).');
1716                                 }
1717
1718                                 // Handle Queueing
1719                                 if ( queue !== false && History.busy() ) {
1720                                         // Wait + Push to Queue
1721                                         //History.debug('History.pushState: we must wait', arguments);
1722                                         History.pushQueue({
1723                                                 scope: History,
1724                                                 callback: History.pushState,
1725                                                 args: arguments,
1726                                                 queue: queue
1727                                         });
1728                                         return false;
1729                                 }
1730
1731                                 // Make Busy + Continue
1732                                 History.busy(true);
1733
1734                                 // Create the newState
1735                                 var newState = History.createStateObject(data,title,url);
1736
1737                                 // Check it
1738                                 if ( History.isLastSavedState(newState) ) {
1739                                         // Won't be a change
1740                                         History.busy(false);
1741                                 }
1742                                 else {
1743                                         // Store the newState
1744                                         History.storeState(newState);
1745                                         History.expectedStateId = newState.id;
1746
1747                                         // Push the newState
1748                                         history.pushState(newState.id,newState.title,newState.url);
1749
1750                                         // Fire HTML5 Event
1751                                         History.Adapter.trigger(window,'popstate');
1752                                 }
1753
1754                                 // End pushState closure
1755                                 return true;
1756                         };
1757
1758                         /**
1759                          * History.replaceState(data,title,url)
1760                          * Replace the State and trigger onpopstate
1761                          * We have to trigger for HTML4 compatibility
1762                          * @param {object} data
1763                          * @param {string} title
1764                          * @param {string} url
1765                          * @return {true}
1766                          */
1767                         History.replaceState = function(data,title,url,queue){
1768                                 //History.debug('History.replaceState: called', arguments);
1769
1770                                 // Check the State
1771                                 if ( History.getHashByUrl(url) && History.emulated.pushState ) {
1772                                         throw new Error('History.js does not support states with fragement-identifiers (hashes/anchors).');
1773                                 }
1774
1775                                 // Handle Queueing
1776                                 if ( queue !== false && History.busy() ) {
1777                                         // Wait + Push to Queue
1778                                         //History.debug('History.replaceState: we must wait', arguments);
1779                                         History.pushQueue({
1780                                                 scope: History,
1781                                                 callback: History.replaceState,
1782                                                 args: arguments,
1783                                                 queue: queue
1784                                         });
1785                                         return false;
1786                                 }
1787
1788                                 // Make Busy + Continue
1789                                 History.busy(true);
1790
1791                                 // Create the newState
1792                                 var newState = History.createStateObject(data,title,url);
1793
1794                                 // Check it
1795                                 if ( History.isLastSavedState(newState) ) {
1796                                         // Won't be a change
1797                                         History.busy(false);
1798                                 }
1799                                 else {
1800                                         // Store the newState
1801                                         History.storeState(newState);
1802                                         History.expectedStateId = newState.id;
1803
1804                                         // Push the newState
1805                                         history.replaceState(newState.id,newState.title,newState.url);
1806
1807                                         // Fire HTML5 Event
1808                                         History.Adapter.trigger(window,'popstate');
1809                                 }
1810
1811                                 // End replaceState closure
1812                                 return true;
1813                         };
1814
1815                 } // !History.emulated.pushState
1816
1817
1818                 // ====================================================================
1819                 // Initialise
1820
1821                 /**
1822                  * Load the Store
1823                  */
1824                 if ( sessionStorage ) {
1825                         // Fetch
1826                         try {
1827                                 History.store = JSON.parse(sessionStorage.getItem('History.store'))||{};
1828                         }
1829                         catch ( err ) {
1830                                 History.store = {};
1831                         }
1832
1833                         // Normalize
1834                         History.normalizeStore();
1835                 }
1836                 else {
1837                         // Default Load
1838                         History.store = {};
1839                         History.normalizeStore();
1840                 }
1841
1842                 /**
1843                  * Clear Intervals on exit to prevent memory leaks
1844                  */
1845                 History.Adapter.bind(window,"unload",History.clearAllIntervals);
1846
1847                 /**
1848                  * Create the initial State
1849                  */
1850                 History.saveState(History.storeState(History.extractState(History.getLocationHref(),true)));
1851
1852                 /**
1853                  * Bind for Saving Store
1854                  */
1855                 if ( sessionStorage ) {
1856                         // When the page is closed
1857                         History.onUnload = function(){
1858                                 // Prepare
1859                                 var     currentStore, item, currentStoreString;
1860
1861                                 // Fetch
1862                                 try {
1863                                         currentStore = JSON.parse(sessionStorage.getItem('History.store'))||{};
1864                                 }
1865                                 catch ( err ) {
1866                                         currentStore = {};
1867                                 }
1868
1869                                 // Ensure
1870                                 currentStore.idToState = currentStore.idToState || {};
1871                                 currentStore.urlToId = currentStore.urlToId || {};
1872                                 currentStore.stateToId = currentStore.stateToId || {};
1873
1874                                 // Sync
1875                                 for ( item in History.idToState ) {
1876                                         if ( !History.idToState.hasOwnProperty(item) ) {
1877                                                 continue;
1878                                         }
1879                                         currentStore.idToState[item] = History.idToState[item];
1880                                 }
1881                                 for ( item in History.urlToId ) {
1882                                         if ( !History.urlToId.hasOwnProperty(item) ) {
1883                                                 continue;
1884                                         }
1885                                         currentStore.urlToId[item] = History.urlToId[item];
1886                                 }
1887                                 for ( item in History.stateToId ) {
1888                                         if ( !History.stateToId.hasOwnProperty(item) ) {
1889                                                 continue;
1890                                         }
1891                                         currentStore.stateToId[item] = History.stateToId[item];
1892                                 }
1893
1894                                 // Update
1895                                 History.store = currentStore;
1896                                 History.normalizeStore();
1897
1898                                 // In Safari, going into Private Browsing mode causes the
1899                                 // Session Storage object to still exist but if you try and use
1900                                 // or set any property/function of it it throws the exception
1901                                 // "QUOTA_EXCEEDED_ERR: DOM Exception 22: An attempt was made to
1902                                 // add something to storage that exceeded the quota." infinitely
1903                                 // every second.
1904                                 currentStoreString = JSON.stringify(currentStore);
1905                                 try {
1906                                         // Store
1907                                         sessionStorage.setItem('History.store', currentStoreString);
1908                                 }
1909                                 catch (e) {
1910                                         if (e.code === DOMException.QUOTA_EXCEEDED_ERR) {
1911                                                 if (sessionStorage.length) {
1912                                                         // Workaround for a bug seen on iPads. Sometimes the quota exceeded error comes up and simply
1913                                                         // removing/resetting the storage can work.
1914                                                         sessionStorage.removeItem('History.store');
1915                                                         sessionStorage.setItem('History.store', currentStoreString);
1916                                                 } else {
1917                                                         // Otherwise, we're probably private browsing in Safari, so we'll ignore the exception.
1918                                                 }
1919                                         } else {
1920                                                 throw e;
1921                                         }
1922                                 }
1923                         };
1924
1925                         // For Internet Explorer
1926                         History.intervalList.push(setInterval(History.onUnload,History.options.storeInterval));
1927
1928                         // For Other Browsers
1929                         History.Adapter.bind(window,'beforeunload',History.onUnload);
1930                         History.Adapter.bind(window,'unload',History.onUnload);
1931
1932                         // Both are enabled for consistency
1933                 }
1934
1935                 // Non-Native pushState Implementation
1936                 if ( !History.emulated.pushState ) {
1937                         // Be aware, the following is only for native pushState implementations
1938                         // If you are wanting to include something for all browsers
1939                         // Then include it above this if block
1940
1941                         /**
1942                          * Setup Safari Fix
1943                          */
1944                         if ( History.bugs.safariPoll ) {
1945                                 History.intervalList.push(setInterval(History.safariStatePoll, History.options.safariPollInterval));
1946                         }
1947
1948                         /**
1949                          * Ensure Cross Browser Compatibility
1950                          */
1951                         if ( window.navigator.vendor === 'Apple Computer, Inc.' || (window.navigator.appCodeName||'') === 'Mozilla' ) {
1952                                 /**
1953                                  * Fix Safari HashChange Issue
1954                                  */
1955
1956                                 // Setup Alias
1957                                 History.Adapter.bind(window,'hashchange',function(){
1958                                         History.Adapter.trigger(window,'popstate');
1959                                 });
1960
1961                                 // Initialise Alias
1962                                 if ( History.getHash() ) {
1963                                         History.Adapter.onDomLoad(function(){
1964                                                 History.Adapter.trigger(window,'hashchange');
1965                                         });
1966                                 }
1967                         }
1968
1969                 } // !History.emulated.pushState
1970
1971
1972         }; // History.initCore
1973
1974         // Try to Initialise History
1975         if (!History.options || !History.options.delayInit) {
1976                 History.init();
1977         }
1978
1979 })(window);