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