2 * Originally based of this code... - refactored for Roo...
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/>
13 // ====================================================================
19 * How long should the interval be before hashchange checks
21 thishashChangeInterval : 100,
25 * How long should the interval be before safari poll checks
27 safariPollInterval : 500,
30 * History.options.doubleCheckInterval
31 * How long should the interval be before we perform a double check
33 doubleCheckInterval : 500,
36 * History.options.disableSuid
37 * Force this.not to append suid
42 * History.options.storeInterval
43 * How long should we wait between store calls
48 * History.options.busyDelay
49 * How long should we wait between busy events
54 * History.options.debug
55 * If true will enable debug messages to be logged
60 * History.options.initialTitle
61 * What is the title of the initial state
66 * History.options.html4Mode
67 * If true, will force HTMl4 mode (hashtags)
69 History.options.html4Mode = History.options.html4Mode || false;
72 * History.options.delayInit
73 * Want to override default options and call init manually.
75 History.options.delayInit = History.options.delayInit || false;
79 // ========================================================================
85 sessionStorage : false, // sessionStorage
90 init : function(options){
92 initialTitle : window.document.title,
94 Roo.apply(this,options)
96 // Check Load Status of Adapter
97 //if ( typeof this.Adapter === 'undefined' ) {
101 // Check Load Status of Core
102 if ( typeof this.initCore !== 'undefined' ) {
106 // Check Load Status of HTML4 Support
107 if ( typeof this.initHtml4 !== 'undefined' ) {
116 // ========================================================================
120 History.initCore = function(options){
124 this.sessionStorage = window.sessionStorage; // This will throw an exception in some browsers when cookies/localStorage are explicitly disabled (i.e. Chrome)
125 this.sessionStorage.setItem('TEST', '1');
126 this.sessionStorage.removeItem('TEST');
128 this.sessionStorage = false;
132 if ( typeof this.initCore.initialized !== 'undefined' ) {
137 this.initCore.initialized = true;
141 // ====================================================================
146 * Configurable options
148 this.options = History.options||{};
151 * History.options.hashChangeInterval
152 * How long should the interval be before hashchange checks
154 this.options.hashChangeInterval = this.options.hashChangeInterval || 100;
157 * History.options.safariPollInterval
158 * How long should the interval be before safari poll checks
160 this.options.safariPollInterval = this.options.safariPollInterval || 500;
163 * History.options.doubleCheckInterval
164 * How long should the interval be before we perform a double check
166 this.options.doubleCheckInterval = this.options.doubleCheckInterval || 500;
169 * History.options.disableSuid
170 * Force this.not to append suid
172 this.options.disableSuid = this.options.disableSuid || false;
175 * History.options.storeInterval
176 * How long should we wait between store calls
178 History.options.storeInterval = History.options.storeInterval || 1000;
181 * History.options.busyDelay
182 * How long should we wait between busy events
184 History.options.busyDelay = History.options.busyDelay || 250;
187 * History.options.debug
188 * If true will enable debug messages to be logged
190 History.options.debug = History.options.debug || false;
193 * History.options.initialTitle
194 * What is the title of the initial state
196 History.options.initialTitle = History.options.initialTitle || window.document.title;
199 * History.options.html4Mode
200 * If true, will force HTMl4 mode (hashtags)
202 History.options.html4Mode = History.options.html4Mode || false;
205 * History.options.delayInit
206 * Want to override default options and call init manually.
208 History.options.delayInit = History.options.delayInit || false;
211 // ====================================================================
215 * History.intervalList
216 * List of intervals set, to be cleared when document is unloaded.
218 History.intervalList = [];
221 * History.clearAllIntervals
222 * Clears all setInterval instances.
224 History.clearAllIntervals = function(){
225 var i, il = History.intervalList;
226 if (typeof il !== "undefined" && il !== null) {
227 for (i = 0; i < il.length; i++) {
228 clearInterval(il[i]);
230 History.intervalList = null;
235 // ====================================================================
239 * History.debug(message,...)
240 * Logs the passed arguments if debug enabled
242 History.debugLog = function(){
243 if ( (this.debug||false) ) {
244 Roo.log.apply(History,arguments);
250 // ====================================================================
254 * History.getInternetExplorerMajorVersion()
255 * Get's the major version of Internet Explorer
257 * @license Public Domain
258 * @author Benjamin Arthur Lupton <contact@balupton.com>
259 * @author James Padolsey <https://gist.github.com/527683>
261 History.getInternetExplorerMajorVersion = function(){
262 var result = History.getInternetExplorerMajorVersion.cached =
263 (typeof History.getInternetExplorerMajorVersion.cached !== 'undefined')
264 ? History.getInternetExplorerMajorVersion.cached
267 div = window.document.createElement('div'),
268 all = div.getElementsByTagName('i');
269 while ( (div.innerHTML = '<!--[if gt IE ' + (++v) + ']><i></i><![endif]-->') && all[0] ) {}
270 return (v > 4) ? v : false;
277 * History.isInternetExplorer()
278 * Are we using Internet Explorer?
280 * @license Public Domain
281 * @author Benjamin Arthur Lupton <contact@balupton.com>
283 History.isInternetExplorer = function(){
285 History.isInternetExplorer.cached =
286 (typeof History.isInternetExplorer.cached !== 'undefined')
287 ? History.isInternetExplorer.cached
288 : Boolean(History.getInternetExplorerMajorVersion())
295 * Which features require emulating?
298 if (History.options.html4Mode) {
309 window.history && window.history.pushState && window.history.replaceState
311 (/ 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) */
312 || (/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 */
316 !(('onhashchange' in window) || ('onhashchange' in window.document))
318 (History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 8)
325 * Is History enabled?
327 History.enabled = !History.emulated.pushState;
331 * Which bugs are present
335 * Safari 5 and Safari iOS 4 fail to return to the correct state once a hash is replaced by a `replaceState` call
336 * https://bugs.webkit.org/show_bug.cgi?id=56249
338 setHash: Boolean(!History.emulated.pushState && window.navigator.vendor === 'Apple Computer, Inc.' && /AppleWebKit\/5([0-2]|3[0-3])/.test(window.navigator.userAgent)),
341 * Safari 5 and Safari iOS 4 sometimes fail to apply the state change under busy conditions
342 * https://bugs.webkit.org/show_bug.cgi?id=42940
344 safariPoll: Boolean(!History.emulated.pushState && window.navigator.vendor === 'Apple Computer, Inc.' && /AppleWebKit\/5([0-2]|3[0-3])/.test(window.navigator.userAgent)),
347 * MSIE 6 and 7 sometimes do not apply a hash even it was told to (requiring a second call to the apply function)
349 ieDoubleCheck: Boolean(History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 8),
352 * MSIE 6 requires the entire hash to be encoded for the hashes to trigger the onHashChange event
354 hashEscape: Boolean(History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 7)
358 * History.isEmptyObject(obj)
359 * Checks to see if the Object is Empty
360 * @param {Object} obj
363 History.isEmptyObject = function(obj) {
364 for ( var name in obj ) {
365 if ( obj.hasOwnProperty(name) ) {
373 * History.cloneObject(obj)
374 * Clones a object and eliminate all references to the original contexts
375 * @param {Object} obj
378 History.cloneObject = function(obj) {
381 hash = JSON.stringify(obj);
382 newObj = JSON.parse(hash);
391 // ====================================================================
395 * History.getRootUrl()
396 * Turns "http://mysite.com/dir/page.html?asd" into "http://mysite.com"
397 * @return {String} rootUrl
399 History.getRootUrl = function(){
401 var rootUrl = window.document.location.protocol+'//'+(window.document.location.hostname||window.document.location.host);
402 if ( window.document.location.port||false ) {
403 rootUrl += ':'+window.document.location.port;
412 * History.getBaseHref()
413 * Fetches the `href` attribute of the `<base href="...">` element if it exists
414 * @return {String} baseHref
416 History.getBaseHref = function(){
419 baseElements = window.document.getElementsByTagName('base'),
423 // Test for Base Element
424 if ( baseElements.length === 1 ) {
425 // Prepare for Base Element
426 baseElement = baseElements[0];
427 baseHref = baseElement.href.replace(/[^\/]+$/,'');
430 // Adjust trailing slash
431 baseHref = baseHref.replace(/\/+$/,'');
432 if ( baseHref ) baseHref += '/';
439 * History.getBaseUrl()
440 * Fetches the baseHref or basePageUrl or rootUrl (whichever one exists first)
441 * @return {String} baseUrl
443 History.getBaseUrl = function(){
445 var baseUrl = History.getBaseHref()||History.getBasePageUrl()||History.getRootUrl();
452 * History.getPageUrl()
453 * Fetches the URL of the current page
454 * @return {String} pageUrl
456 History.getPageUrl = function(){
459 State = History.getState(false,false),
460 stateUrl = (State||{}).url||History.getLocationHref(),
464 pageUrl = stateUrl.replace(/\/+$/,'').replace(/[^\/]+$/,function(part,index,string){
465 return (/\./).test(part) ? part : part+'/';
473 * History.getBasePageUrl()
474 * Fetches the Url of the directory of the current page
475 * @return {String} basePageUrl
477 History.getBasePageUrl = function(){
479 var basePageUrl = (History.getLocationHref()).replace(/[#\?].*/,'').replace(/[^\/]+$/,function(part,index,string){
480 return (/[^\/]$/).test(part) ? '' : part;
481 }).replace(/\/+$/,'')+'/';
488 * History.getFullUrl(url)
489 * Ensures that we have an absolute URL and not a relative URL
490 * @param {string} url
491 * @param {Boolean} allowBaseHref
492 * @return {string} fullUrl
494 History.getFullUrl = function(url,allowBaseHref){
496 var fullUrl = url, firstChar = url.substring(0,1);
497 allowBaseHref = (typeof allowBaseHref === 'undefined') ? true : allowBaseHref;
500 if ( /[a-z]+\:\/\//.test(url) ) {
503 else if ( firstChar === '/' ) {
505 fullUrl = History.getRootUrl()+url.replace(/^\/+/,'');
507 else if ( firstChar === '#' ) {
509 fullUrl = History.getPageUrl().replace(/#.*/,'')+url;
511 else if ( firstChar === '?' ) {
513 fullUrl = History.getPageUrl().replace(/[\?#].*/,'')+url;
517 if ( allowBaseHref ) {
518 fullUrl = History.getBaseUrl()+url.replace(/^(\.\/)+/,'');
520 fullUrl = History.getBasePageUrl()+url.replace(/^(\.\/)+/,'');
522 // We have an if condition above as we do not want hashes
523 // which are relative to the baseHref in our URLs
524 // as if the baseHref changes, then all our bookmarks
525 // would now point to different locations
526 // whereas the basePageUrl will always stay the same
530 return fullUrl.replace(/\#$/,'');
534 * History.getShortUrl(url)
535 * Ensures that we have a relative URL and not a absolute URL
536 * @param {string} url
537 * @return {string} url
539 History.getShortUrl = function(url){
541 var shortUrl = url, baseUrl = History.getBaseUrl(), rootUrl = History.getRootUrl();
544 if ( History.emulated.pushState ) {
545 // We are in a if statement as when pushState is not emulated
546 // The actual url these short urls are relative to can change
547 // So within the same session, we the url may end up somewhere different
548 shortUrl = shortUrl.replace(baseUrl,'');
552 shortUrl = shortUrl.replace(rootUrl,'/');
554 // Ensure we can still detect it as a state
555 if ( History.isTraditionalAnchor(shortUrl) ) {
556 shortUrl = './'+shortUrl;
560 shortUrl = shortUrl.replace(/^(\.\/)+/g,'./').replace(/\#$/,'');
567 * History.getLocationHref(document)
568 * Returns a normalized version of document.location.href
569 * accounting for browser inconsistencies, etc.
571 * This URL will be URI-encoded and will include the hash
573 * @param {object} document
574 * @return {string} url
576 History.getLocationHref = function(doc) {
577 doc = doc || window.document;
579 // most of the time, this will be true
580 if (doc.URL === doc.location.href)
581 return doc.location.href;
583 // some versions of webkit URI-decode document.location.href
584 // but they leave document.URL in an encoded state
585 if (doc.location.href === decodeURIComponent(doc.URL))
588 // FF 3.6 only updates document.URL when a page is reloaded
589 // document.location.href is updated correctly
590 if (doc.location.hash && decodeURIComponent(doc.location.href.replace(/^[^#]+/, "")) === doc.location.hash)
591 return doc.location.href;
593 if (doc.URL.indexOf('#') == -1 && doc.location.href.indexOf('#') != -1)
594 return doc.location.href;
596 return doc.URL || doc.location.href;
600 // ====================================================================
605 * The store for all session specific data
611 * 1-1: State ID to State Object
613 History.idToState = History.idToState||{};
617 * 1-1: State String to State ID
619 History.stateToId = History.stateToId||{};
623 * 1-1: State URL to State ID
625 History.urlToId = History.urlToId||{};
628 * History.storedStates
629 * Store the states in an array
631 History.storedStates = History.storedStates||[];
634 * History.savedStates
635 * Saved the states in an array
637 History.savedStates = History.savedStates||[];
640 * History.noramlizeStore()
641 * Noramlize the store by adding necessary values
643 History.normalizeStore = function(){
644 History.store.idToState = History.store.idToState||{};
645 History.store.urlToId = History.store.urlToId||{};
646 History.store.stateToId = History.store.stateToId||{};
651 * Get an object containing the data, title and url of the current state
652 * @param {Boolean} friendly
653 * @param {Boolean} create
654 * @return {Object} State
656 History.getState = function(friendly,create){
658 if ( typeof friendly === 'undefined' ) { friendly = true; }
659 if ( typeof create === 'undefined' ) { create = true; }
662 var State = History.getLastSavedState();
665 if ( !State && create ) {
666 State = History.createStateObject();
671 State = History.cloneObject(State);
672 State.url = State.cleanUrl||State.url;
680 * History.getIdByState(State)
681 * Gets a ID for a State
682 * @param {State} newState
683 * @return {String} id
685 History.getIdByState = function(newState){
688 var id = History.extractId(newState.url),
692 // Find ID via State String
693 str = History.getStateString(newState);
694 if ( typeof History.stateToId[str] !== 'undefined' ) {
695 id = History.stateToId[str];
697 else if ( typeof History.store.stateToId[str] !== 'undefined' ) {
698 id = History.store.stateToId[str];
703 id = (new Date()).getTime() + String(Math.random()).replace(/\D/g,'');
704 if ( typeof History.idToState[id] === 'undefined' && typeof History.store.idToState[id] === 'undefined' ) {
709 // Apply the new State to the ID
710 History.stateToId[str] = id;
711 History.idToState[id] = newState;
720 * History.normalizeState(State)
721 * Expands a State Object
722 * @param {object} State
725 History.normalizeState = function(oldState){
727 var newState, dataNotEmpty;
730 if ( !oldState || (typeof oldState !== 'object') ) {
735 if ( typeof oldState.normalized !== 'undefined' ) {
740 if ( !oldState.data || (typeof oldState.data !== 'object') ) {
744 // ----------------------------------------------------------------
748 newState.normalized = true;
749 newState.title = oldState.title||'';
750 newState.url = History.getFullUrl(oldState.url?oldState.url:(History.getLocationHref()));
751 newState.hash = History.getShortUrl(newState.url);
752 newState.data = History.cloneObject(oldState.data);
755 newState.id = History.getIdByState(newState);
757 // ----------------------------------------------------------------
760 newState.cleanUrl = newState.url.replace(/\??\&_suid.*/,'');
761 newState.url = newState.cleanUrl;
763 // Check to see if we have more than just a url
764 dataNotEmpty = !History.isEmptyObject(newState.data);
767 if ( (newState.title || dataNotEmpty) && History.options.disableSuid !== true ) {
769 newState.hash = History.getShortUrl(newState.url).replace(/\??\&_suid.*/,'');
770 if ( !/\?/.test(newState.hash) ) {
771 newState.hash += '?';
773 newState.hash += '&_suid='+newState.id;
776 // Create the Hashed URL
777 newState.hashedUrl = History.getFullUrl(newState.hash);
779 // ----------------------------------------------------------------
781 // Update the URL if we have a duplicate
782 if ( (History.emulated.pushState || History.bugs.safariPoll) && History.hasUrlDuplicate(newState) ) {
783 newState.url = newState.hashedUrl;
786 // ----------------------------------------------------------------
793 * History.createStateObject(data,title,url)
794 * Creates a object based on the data, title and url state params
795 * @param {object} data
796 * @param {string} title
797 * @param {string} url
800 History.createStateObject = function(data,title,url){
809 State = History.normalizeState(State);
816 * History.getStateById(id)
817 * Get a state by it's UID
820 History.getStateById = function(id){
825 var State = History.idToState[id] || History.store.idToState[id] || undefined;
832 * Get a State's String
833 * @param {State} passedState
835 History.getStateString = function(passedState){
837 var State, cleanedState, str;
840 State = History.normalizeState(passedState);
845 title: passedState.title,
850 str = JSON.stringify(cleanedState);
858 * @param {State} passedState
859 * @return {String} id
861 History.getStateId = function(passedState){
866 State = History.normalizeState(passedState);
876 * History.getHashByState(State)
877 * Creates a Hash for the State Object
878 * @param {State} passedState
879 * @return {String} hash
881 History.getHashByState = function(passedState){
886 State = History.normalizeState(passedState);
896 * History.extractId(url_or_hash)
897 * Get a State ID by it's URL or Hash
898 * @param {string} url_or_hash
899 * @return {string} id
901 History.extractId = function ( url_or_hash ) {
903 var id,parts,url, tmp;
907 // If the URL has a #, use the id from before the #
908 if (url_or_hash.indexOf('#') != -1)
910 tmp = url_or_hash.split("#")[0];
917 parts = /(.*)\&_suid=([0-9]+)$/.exec(tmp);
918 url = parts ? (parts[1]||url_or_hash) : url_or_hash;
919 id = parts ? String(parts[2]||'') : '';
926 * History.isTraditionalAnchor
927 * Checks to see if the url is a traditional anchor or not
928 * @param {String} url_or_hash
931 History.isTraditionalAnchor = function(url_or_hash){
933 var isTraditional = !(/[\/\?\.]/.test(url_or_hash));
936 return isTraditional;
940 * History.extractState
941 * Get a State by it's URL or Hash
942 * @param {String} url_or_hash
943 * @return {State|null}
945 History.extractState = function(url_or_hash,create){
947 var State = null, id, url;
948 create = create||false;
951 id = History.extractId(url_or_hash);
953 State = History.getStateById(id);
956 // Fetch SUID returned no State
959 url = History.getFullUrl(url_or_hash);
962 id = History.getIdByUrl(url)||false;
964 State = History.getStateById(id);
968 if ( !State && create && !History.isTraditionalAnchor(url_or_hash) ) {
969 State = History.createStateObject(null,null,url);
978 * History.getIdByUrl()
979 * Get a State ID by a State URL
981 History.getIdByUrl = function(url){
983 var id = History.urlToId[url] || History.store.urlToId[url] || undefined;
990 * History.getLastSavedState()
991 * Get an object containing the data, title and url of the current state
992 * @return {Object} State
994 History.getLastSavedState = function(){
995 return History.savedStates[History.savedStates.length-1]||undefined;
999 * History.getLastStoredState()
1000 * Get an object containing the data, title and url of the current state
1001 * @return {Object} State
1003 History.getLastStoredState = function(){
1004 return History.storedStates[History.storedStates.length-1]||undefined;
1008 * History.hasUrlDuplicate
1009 * Checks if a Url will have a url conflict
1010 * @param {Object} newState
1011 * @return {Boolean} hasDuplicate
1013 History.hasUrlDuplicate = function(newState) {
1015 var hasDuplicate = false,
1019 oldState = History.extractState(newState.url);
1022 hasDuplicate = oldState && oldState.id !== newState.id;
1025 return hasDuplicate;
1029 * History.storeState
1031 * @param {Object} newState
1032 * @return {Object} newState
1034 History.storeState = function(newState){
1036 History.urlToId[newState.url] = newState.id;
1039 History.storedStates.push(History.cloneObject(newState));
1046 * History.isLastSavedState(newState)
1047 * Tests to see if the state is the last state
1048 * @param {Object} newState
1049 * @return {boolean} isLast
1051 History.isLastSavedState = function(newState){
1054 newId, oldState, oldId;
1057 if ( History.savedStates.length ) {
1058 newId = newState.id;
1059 oldState = History.getLastSavedState();
1060 oldId = oldState.id;
1063 isLast = (newId === oldId);
1073 * @param {Object} newState
1074 * @return {boolean} changed
1076 History.saveState = function(newState){
1078 if ( History.isLastSavedState(newState) ) {
1083 History.savedStates.push(History.cloneObject(newState));
1090 * History.getStateByIndex()
1091 * Gets a state by the index
1092 * @param {integer} index
1095 History.getStateByIndex = function(index){
1100 if ( typeof index === 'undefined' ) {
1101 // Get the last inserted
1102 State = History.savedStates[History.savedStates.length-1];
1104 else if ( index < 0 ) {
1106 State = History.savedStates[History.savedStates.length+index];
1109 // Get from the beginning
1110 State = History.savedStates[index];
1118 * History.getCurrentIndex()
1119 * Gets the current index
1122 History.getCurrentIndex = function(){
1127 if(History.savedStates.length < 1) {
1131 index = History.savedStates.length-1;
1136 // ====================================================================
1141 * @param {Location=} location
1142 * Gets the current document hash
1143 * Note: unlike location.hash, this is guaranteed to return the escaped hash in all browsers
1146 History.getHash = function(doc){
1147 var url = History.getLocationHref(doc),
1149 hash = History.getHashByUrl(url);
1154 * History.unescapeHash()
1155 * normalize and Unescape a Hash
1156 * @param {String} hash
1159 History.unescapeHash = function(hash){
1161 var result = History.normalizeHash(hash);
1164 result = decodeURIComponent(result);
1171 * History.normalizeHash()
1172 * normalize a hash across browsers
1175 History.normalizeHash = function(hash){
1177 var result = hash.replace(/[^#]*#/,'').replace(/#.*/, '');
1184 * History.setHash(hash)
1185 * Sets the document hash
1186 * @param {string} hash
1189 History.setHash = function(hash,queue){
1194 if ( queue !== false && History.busy() ) {
1195 // Wait + Push to Queue
1196 //History.debug('History.setHash: we must wait', arguments);
1199 callback: History.setHash,
1207 //History.debug('History.setHash: called',hash);
1209 // Make Busy + Continue
1212 // Check if hash is a state
1213 State = History.extractState(hash,true);
1214 if ( State && !History.emulated.pushState ) {
1215 // Hash is a state so skip the setHash
1216 //History.debug('History.setHash: Hash is a state so skipping the hash set with a direct pushState call',arguments);
1219 History.pushState(State.data,State.title,State.url,false);
1221 else if ( History.getHash() !== hash ) {
1222 // Hash is a proper hash, so apply it
1224 // Handle browser bugs
1225 if ( History.bugs.setHash ) {
1226 // Fix Safari Bug https://bugs.webkit.org/show_bug.cgi?id=56249
1228 // Fetch the base page
1229 pageUrl = History.getPageUrl();
1231 // Safari hash apply
1232 History.pushState(null,null,pageUrl+'#'+hash,false);
1235 // Normal hash apply
1236 window.document.location.hash = hash;
1246 * normalize and Escape a Hash
1249 History.escapeHash = function(hash){
1251 var result = History.normalizeHash(hash);
1254 result = window.encodeURIComponent(result);
1257 if ( !History.bugs.hashEscape ) {
1258 // Restore common parts
1260 .replace(/\%21/g,'!')
1261 .replace(/\%26/g,'&')
1262 .replace(/\%3D/g,'=')
1263 .replace(/\%3F/g,'?');
1271 * History.getHashByUrl(url)
1272 * Extracts the Hash from a URL
1273 * @param {string} url
1274 * @return {string} url
1276 History.getHashByUrl = function(url){
1278 var hash = String(url)
1279 .replace(/([^#]*)#?([^#]*)#?(.*)/, '$2')
1283 hash = History.unescapeHash(hash);
1290 * History.setTitle(title)
1291 * Applies the title to the document
1292 * @param {State} newState
1295 History.setTitle = function(newState){
1297 var title = newState.title,
1302 firstState = History.getStateByIndex(0);
1303 if ( firstState && firstState.url === newState.url ) {
1304 title = firstState.title||History.options.initialTitle;
1310 window.document.getElementsByTagName('title')[0].innerHTML = title.replace('<','<').replace('>','>').replace(' & ',' & ');
1312 catch ( Exception ) { }
1313 window.document.title = title;
1320 // ====================================================================
1325 * The list of queues to use
1326 * First In, First Out
1328 History.queues = [];
1331 * History.busy(value)
1332 * @param {boolean} value [optional]
1333 * @return {boolean} busy
1335 History.busy = function(value){
1337 if ( typeof value !== 'undefined' ) {
1338 //History.debug('History.busy: changing ['+(History.busy.flag||false)+'] to ['+(value||false)+']', History.queues.length);
1339 History.busy.flag = value;
1342 else if ( typeof History.busy.flag === 'undefined' ) {
1343 History.busy.flag = false;
1347 if ( !History.busy.flag ) {
1348 // Execute the next item in the queue
1349 window.clearTimeout(History.busy.timeout);
1350 var fireNext = function(){
1352 if ( History.busy.flag ) return;
1353 for ( i=History.queues.length-1; i >= 0; --i ) {
1354 queue = History.queues[i];
1355 if ( queue.length === 0 ) continue;
1356 item = queue.shift();
1357 History.fireQueueItem(item);
1358 History.busy.timeout = window.setTimeout(fireNext,History.options.busyDelay);
1361 History.busy.timeout = window.setTimeout(fireNext,History.options.busyDelay);
1365 return History.busy.flag;
1371 History.busy.flag = false;
1374 * History.fireQueueItem(item)
1376 * @param {Object} item
1377 * @return {Mixed} result
1379 History.fireQueueItem = function(item){
1380 return item.callback.apply(item.scope||History,item.args||[]);
1384 * History.pushQueue(callback,args)
1385 * Add an item to the queue
1386 * @param {Object} item [scope,callback,args,queue]
1388 History.pushQueue = function(item){
1389 // Prepare the queue
1390 History.queues[item.queue||0] = History.queues[item.queue||0]||[];
1393 History.queues[item.queue||0].push(item);
1400 * History.queue (item,queue), (func,queue), (func), (item)
1401 * Either firs the item now if not busy, or adds it to the queue
1403 History.queue = function(item,queue){
1405 if ( typeof item === 'function' ) {
1410 if ( typeof queue !== 'undefined' ) {
1415 if ( History.busy() ) {
1416 History.pushQueue(item);
1418 History.fireQueueItem(item);
1426 * History.clearQueue()
1429 History.clearQueue = function(){
1430 History.busy.flag = false;
1431 History.queues = [];
1436 // ====================================================================
1440 * History.stateChanged
1441 * States whether or not the state has changed since the last double check was initialised
1443 History.stateChanged = false;
1446 * History.doubleChecker
1447 * Contains the timeout used for the double checks
1449 History.doubleChecker = false;
1452 * History.doubleCheckComplete()
1453 * Complete a double check
1456 History.doubleCheckComplete = function(){
1458 History.stateChanged = true;
1461 History.doubleCheckClear();
1468 * History.doubleCheckClear()
1469 * Clear a double check
1472 History.doubleCheckClear = function(){
1474 if ( History.doubleChecker ) {
1475 window.clearTimeout(History.doubleChecker);
1476 History.doubleChecker = false;
1484 * History.doubleCheck()
1485 * Create a double check
1488 History.doubleCheck = function(tryAgain){
1490 History.stateChanged = false;
1491 History.doubleCheckClear();
1493 // Fix IE6,IE7 bug where calling history.back or history.forward does not actually change the hash (whereas doing it manually does)
1494 // Fix Safari 5 bug where sometimes the state does not change: https://bugs.webkit.org/show_bug.cgi?id=42940
1495 if ( History.bugs.ieDoubleCheck ) {
1497 History.doubleChecker = window.setTimeout(
1499 History.doubleCheckClear();
1500 if ( !History.stateChanged ) {
1501 //History.debug('History.doubleCheck: State has not yet changed, trying again', arguments);
1507 History.options.doubleCheckInterval
1516 // ====================================================================
1520 * History.safariStatePoll()
1521 * Poll the current state
1524 History.safariStatePoll = function(){
1527 // Get the Last State which has the new URL
1529 urlState = History.extractState(History.getLocationHref()),
1532 // Check for a difference
1533 if ( !History.isLastSavedState(urlState) ) {
1534 newState = urlState;
1540 // Check if we have a state with that url
1543 //History.debug('History.safariStatePoll: new');
1544 newState = History.createStateObject();
1547 // Apply the New State
1548 //History.debug('History.safariStatePoll: trigger');
1549 History.Adapter.trigger(window,'popstate');
1556 // ====================================================================
1560 * History.back(queue)
1561 * Send the browser history back one item
1562 * @param {Integer} queue [optional]
1564 History.back = function(queue){
1565 //History.debug('History.back: called', arguments);
1568 if ( queue !== false && History.busy() ) {
1569 // Wait + Push to Queue
1570 //History.debug('History.back: we must wait', arguments);
1573 callback: History.back,
1580 // Make Busy + Continue
1583 // Fix certain browser bugs that prevent the state from changing
1584 History.doubleCheck(function(){
1585 History.back(false);
1596 * History.forward(queue)
1597 * Send the browser history forward one item
1598 * @param {Integer} queue [optional]
1600 History.forward = function(queue){
1601 //History.debug('History.forward: called', arguments);
1604 if ( queue !== false && History.busy() ) {
1605 // Wait + Push to Queue
1606 //History.debug('History.forward: we must wait', arguments);
1609 callback: History.forward,
1616 // Make Busy + Continue
1619 // Fix certain browser bugs that prevent the state from changing
1620 History.doubleCheck(function(){
1621 History.forward(false);
1627 // End forward closure
1632 * History.go(index,queue)
1633 * Send the browser history back or forward index times
1634 * @param {Integer} queue [optional]
1636 History.go = function(index,queue){
1637 //History.debug('History.go: called', arguments);
1645 for ( i=1; i<=index; ++i ) {
1646 History.forward(queue);
1649 else if ( index < 0 ) {
1651 for ( i=-1; i>=index; --i ) {
1652 History.back(queue);
1656 throw new Error('History.go: History.go requires a positive or negative integer passed.');
1664 // ====================================================================
1665 // HTML5 State Support
1667 // Non-Native pushState Implementation
1668 if ( History.emulated.pushState ) {
1670 * Provide Skeleton for HTML4 Browsers
1674 var emptyFunction = function(){};
1675 History.pushState = History.pushState||emptyFunction;
1676 History.replaceState = History.replaceState||emptyFunction;
1677 } // History.emulated.pushState
1679 // Native pushState Implementation
1682 * Use native HTML5 History API Implementation
1686 * History.onPopState(event,extra)
1687 * Refresh the Current State
1689 History.onPopState = function(event,extra){
1691 var stateId = false, newState = false, currentHash, currentState;
1693 // Reset the double check
1694 History.doubleCheckComplete();
1696 // Check for a Hash, and handle apporiatly
1697 currentHash = History.getHash();
1698 if ( currentHash ) {
1700 currentState = History.extractState(currentHash||History.getLocationHref(),true);
1701 if ( currentState ) {
1702 // We were able to parse it, it must be a State!
1703 // Let's forward to replaceState
1704 //History.debug('History.onPopState: state anchor', currentHash, currentState);
1705 History.replaceState(currentState.data, currentState.title, currentState.url, false);
1708 // Traditional Anchor
1709 //History.debug('History.onPopState: traditional anchor', currentHash);
1710 History.Adapter.trigger(window,'anchorchange');
1711 History.busy(false);
1714 // We don't care for hashes
1715 History.expectedStateId = false;
1720 stateId = History.Adapter.extractEventData('state',event,extra) || false;
1724 // Vanilla: Back/forward button was used
1725 newState = History.getStateById(stateId);
1727 else if ( History.expectedStateId ) {
1728 // Vanilla: A new state was pushed, and popstate was called manually
1729 newState = History.getStateById(History.expectedStateId);
1733 newState = History.extractState(History.getLocationHref());
1736 // The State did not exist in our store
1738 // Regenerate the State
1739 newState = History.createStateObject(null,null,History.getLocationHref());
1743 History.expectedStateId = false;
1745 // Check if we are the same state
1746 if ( History.isLastSavedState(newState) ) {
1747 // There has been no change (just the page's hash has finally propagated)
1748 //History.debug('History.onPopState: no change', newState, History.savedStates);
1749 History.busy(false);
1754 History.storeState(newState);
1755 History.saveState(newState);
1757 // Force update of the title
1758 History.setTitle(newState);
1761 History.Adapter.trigger(window,'statechange');
1762 History.busy(false);
1767 History.Adapter.bind(window,'popstate',History.onPopState);
1770 * History.pushState(data,title,url)
1771 * Add a new State to the history object, become it, and trigger onpopstate
1772 * We have to trigger for HTML4 compatibility
1773 * @param {object} data
1774 * @param {string} title
1775 * @param {string} url
1778 History.pushState = function(data,title,url,queue){
1779 //History.debug('History.pushState: called', arguments);
1782 if ( History.getHashByUrl(url) && History.emulated.pushState ) {
1783 throw new Error('History.js does not support states with fragement-identifiers (hashes/anchors).');
1787 if ( queue !== false && History.busy() ) {
1788 // Wait + Push to Queue
1789 //History.debug('History.pushState: we must wait', arguments);
1792 callback: History.pushState,
1799 // Make Busy + Continue
1802 // Create the newState
1803 var newState = History.createStateObject(data,title,url);
1806 if ( History.isLastSavedState(newState) ) {
1807 // Won't be a change
1808 History.busy(false);
1811 // Store the newState
1812 History.storeState(newState);
1813 History.expectedStateId = newState.id;
1815 // Push the newState
1816 history.pushState(newState.id,newState.title,newState.url);
1819 History.Adapter.trigger(window,'popstate');
1822 // End pushState closure
1827 * History.replaceState(data,title,url)
1828 * Replace the State and trigger onpopstate
1829 * We have to trigger for HTML4 compatibility
1830 * @param {object} data
1831 * @param {string} title
1832 * @param {string} url
1835 History.replaceState = function(data,title,url,queue){
1836 //History.debug('History.replaceState: called', arguments);
1839 if ( History.getHashByUrl(url) && History.emulated.pushState ) {
1840 throw new Error('History.js does not support states with fragement-identifiers (hashes/anchors).');
1844 if ( queue !== false && History.busy() ) {
1845 // Wait + Push to Queue
1846 //History.debug('History.replaceState: we must wait', arguments);
1849 callback: History.replaceState,
1856 // Make Busy + Continue
1859 // Create the newState
1860 var newState = History.createStateObject(data,title,url);
1863 if ( History.isLastSavedState(newState) ) {
1864 // Won't be a change
1865 History.busy(false);
1868 // Store the newState
1869 History.storeState(newState);
1870 History.expectedStateId = newState.id;
1872 // Push the newState
1873 history.replaceState(newState.id,newState.title,newState.url);
1876 History.Adapter.trigger(window,'popstate');
1879 // End replaceState closure
1883 } // !History.emulated.pushState
1886 // ====================================================================
1892 if ( sessionStorage ) {
1895 History.store = JSON.parse(sessionStorage.getItem('History.store'))||{};
1902 History.normalizeStore();
1907 History.normalizeStore();
1911 * Clear Intervals on exit to prevent memory leaks
1913 History.Adapter.bind(window,"unload",History.clearAllIntervals);
1916 * Create the initial State
1918 History.saveState(History.storeState(History.extractState(History.getLocationHref(),true)));
1921 * Bind for Saving Store
1923 if ( sessionStorage ) {
1924 // When the page is closed
1925 History.onUnload = function(){
1927 var currentStore, item, currentStoreString;
1931 currentStore = JSON.parse(sessionStorage.getItem('History.store'))||{};
1938 currentStore.idToState = currentStore.idToState || {};
1939 currentStore.urlToId = currentStore.urlToId || {};
1940 currentStore.stateToId = currentStore.stateToId || {};
1943 for ( item in History.idToState ) {
1944 if ( !History.idToState.hasOwnProperty(item) ) {
1947 currentStore.idToState[item] = History.idToState[item];
1949 for ( item in History.urlToId ) {
1950 if ( !History.urlToId.hasOwnProperty(item) ) {
1953 currentStore.urlToId[item] = History.urlToId[item];
1955 for ( item in History.stateToId ) {
1956 if ( !History.stateToId.hasOwnProperty(item) ) {
1959 currentStore.stateToId[item] = History.stateToId[item];
1963 History.store = currentStore;
1964 History.normalizeStore();
1966 // In Safari, going into Private Browsing mode causes the
1967 // Session Storage object to still exist but if you try and use
1968 // or set any property/function of it it throws the exception
1969 // "QUOTA_EXCEEDED_ERR: DOM Exception 22: An attempt was made to
1970 // add something to storage that exceeded the quota." infinitely
1972 currentStoreString = JSON.stringify(currentStore);
1975 sessionStorage.setItem('History.store', currentStoreString);
1978 if (e.code === DOMException.QUOTA_EXCEEDED_ERR) {
1979 if (sessionStorage.length) {
1980 // Workaround for a bug seen on iPads. Sometimes the quota exceeded error comes up and simply
1981 // removing/resetting the storage can work.
1982 sessionStorage.removeItem('History.store');
1983 sessionStorage.setItem('History.store', currentStoreString);
1985 // Otherwise, we're probably private browsing in Safari, so we'll ignore the exception.
1993 // For Internet Explorer
1994 History.intervalList.push(setInterval(History.onUnload,History.options.storeInterval));
1996 // For Other Browsers
1997 History.Adapter.bind(window,'beforeunload',History.onUnload);
1998 History.Adapter.bind(window,'unload',History.onUnload);
2000 // Both are enabled for consistency
2003 // Non-Native pushState Implementation
2004 if ( !History.emulated.pushState ) {
2005 // Be aware, the following is only for native pushState implementations
2006 // If you are wanting to include something for all browsers
2007 // Then include it above this if block
2012 if ( History.bugs.safariPoll ) {
2013 History.intervalList.push(setInterval(History.safariStatePoll, History.options.safariPollInterval));
2017 * Ensure Cross Browser Compatibility
2019 if ( window.navigator.vendor === 'Apple Computer, Inc.' || (window.navigator.appCodeName||'') === 'Mozilla' ) {
2021 * Fix Safari HashChange Issue
2025 History.Adapter.bind(window,'hashchange',function(){
2026 History.Adapter.trigger(window,'popstate');
2030 if ( History.getHash() ) {
2031 History.Adapter.onDomLoad(function(){
2032 History.Adapter.trigger(window,'hashchange');
2037 } // !History.emulated.pushState
2040 }; // History.initCore
2042 // Try to Initialise History
2043 if (!History.options || !History.options.delayInit) {