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