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