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