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