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