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