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