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