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