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