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