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