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)
72 * History.options.delayInit
73 * Want to override default options and call init manually.
79 // ========================================================================
85 sessionStorage : false, // sessionStorage
87 intervalList : false, // array normally.
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 initCore : function(options){
122 this.intervalList = [];
126 this.sessionStorage = window.sessionStorage; // This will throw an exception in some browsers when cookies/localStorage are explicitly disabled (i.e. Chrome)
127 this.sessionStorage.setItem('TEST', '1');
128 this.sessionStorage.removeItem('TEST');
130 this.sessionStorage = false;
134 if ( typeof this.initCore.initialized !== 'undefined' ) {
139 this.initCore.initialized = true;
147 * History.clearAllIntervals
148 * Clears all setInterval instances.
150 clearAllIntervals: function()
152 var i, il = this.intervalList;
153 if (typeof il !== "undefined" && il !== null) {
154 for (i = 0; i < il.length; i++) {
155 clearInterval(il[i]);
157 this.intervalList = null;
162 // ====================================================================
166 * History.debugLog(message,...)
167 * Logs the passed arguments if debug enabled
169 debugLog : function()
171 if ( (this.debug||false) ) {
172 Roo.log.apply(History,arguments);
178 // ====================================================================
182 * History.getInternetExplorerMajorVersion()
183 * Get's the major version of Internet Explorer
185 * @license Public Domain
186 * @author Benjamin Arthur Lupton <contact@balupton.com>
187 * @author James Padolsey <https://gist.github.com/527683>
189 History.getInternetExplorerMajorVersion = function(){
190 var result = History.getInternetExplorerMajorVersion.cached =
191 (typeof History.getInternetExplorerMajorVersion.cached !== 'undefined')
192 ? History.getInternetExplorerMajorVersion.cached
195 div = window.document.createElement('div'),
196 all = div.getElementsByTagName('i');
197 while ( (div.innerHTML = '<!--[if gt IE ' + (++v) + ']><i></i><![endif]-->') && all[0] ) {}
198 return (v > 4) ? v : false;
205 * History.isInternetExplorer()
206 * Are we using Internet Explorer?
208 * @license Public Domain
209 * @author Benjamin Arthur Lupton <contact@balupton.com>
211 History.isInternetExplorer = function(){
213 History.isInternetExplorer.cached =
214 (typeof History.isInternetExplorer.cached !== 'undefined')
215 ? History.isInternetExplorer.cached
216 : Boolean(History.getInternetExplorerMajorVersion())
223 * Which features require emulating?
226 if (History.options.html4Mode) {
237 window.history && window.history.pushState && window.history.replaceState
239 (/ 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) */
240 || (/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 */
244 !(('onhashchange' in window) || ('onhashchange' in window.document))
246 (History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 8)
253 * Is History enabled?
255 History.enabled = !History.emulated.pushState;
259 * Which bugs are present
263 * Safari 5 and Safari iOS 4 fail to return to the correct state once a hash is replaced by a `replaceState` call
264 * https://bugs.webkit.org/show_bug.cgi?id=56249
266 setHash: Boolean(!History.emulated.pushState && window.navigator.vendor === 'Apple Computer, Inc.' && /AppleWebKit\/5([0-2]|3[0-3])/.test(window.navigator.userAgent)),
269 * Safari 5 and Safari iOS 4 sometimes fail to apply the state change under busy conditions
270 * https://bugs.webkit.org/show_bug.cgi?id=42940
272 safariPoll: Boolean(!History.emulated.pushState && window.navigator.vendor === 'Apple Computer, Inc.' && /AppleWebKit\/5([0-2]|3[0-3])/.test(window.navigator.userAgent)),
275 * MSIE 6 and 7 sometimes do not apply a hash even it was told to (requiring a second call to the apply function)
277 ieDoubleCheck: Boolean(History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 8),
280 * MSIE 6 requires the entire hash to be encoded for the hashes to trigger the onHashChange event
282 hashEscape: Boolean(History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 7)
286 * History.isEmptyObject(obj)
287 * Checks to see if the Object is Empty
288 * @param {Object} obj
291 History.isEmptyObject = function(obj) {
292 for ( var name in obj ) {
293 if ( obj.hasOwnProperty(name) ) {
301 * History.cloneObject(obj)
302 * Clones a object and eliminate all references to the original contexts
303 * @param {Object} obj
306 History.cloneObject = function(obj) {
309 hash = JSON.stringify(obj);
310 newObj = JSON.parse(hash);
319 // ====================================================================
323 * History.getRootUrl()
324 * Turns "http://mysite.com/dir/page.html?asd" into "http://mysite.com"
325 * @return {String} rootUrl
327 History.getRootUrl = function(){
329 var rootUrl = window.document.location.protocol+'//'+(window.document.location.hostname||window.document.location.host);
330 if ( window.document.location.port||false ) {
331 rootUrl += ':'+window.document.location.port;
340 * History.getBaseHref()
341 * Fetches the `href` attribute of the `<base href="...">` element if it exists
342 * @return {String} baseHref
344 History.getBaseHref = function(){
347 baseElements = window.document.getElementsByTagName('base'),
351 // Test for Base Element
352 if ( baseElements.length === 1 ) {
353 // Prepare for Base Element
354 baseElement = baseElements[0];
355 baseHref = baseElement.href.replace(/[^\/]+$/,'');
358 // Adjust trailing slash
359 baseHref = baseHref.replace(/\/+$/,'');
360 if ( baseHref ) baseHref += '/';
367 * History.getBaseUrl()
368 * Fetches the baseHref or basePageUrl or rootUrl (whichever one exists first)
369 * @return {String} baseUrl
371 History.getBaseUrl = function(){
373 var baseUrl = History.getBaseHref()||History.getBasePageUrl()||History.getRootUrl();
380 * History.getPageUrl()
381 * Fetches the URL of the current page
382 * @return {String} pageUrl
384 History.getPageUrl = function(){
387 State = History.getState(false,false),
388 stateUrl = (State||{}).url||History.getLocationHref(),
392 pageUrl = stateUrl.replace(/\/+$/,'').replace(/[^\/]+$/,function(part,index,string){
393 return (/\./).test(part) ? part : part+'/';
401 * History.getBasePageUrl()
402 * Fetches the Url of the directory of the current page
403 * @return {String} basePageUrl
405 History.getBasePageUrl = function(){
407 var basePageUrl = (History.getLocationHref()).replace(/[#\?].*/,'').replace(/[^\/]+$/,function(part,index,string){
408 return (/[^\/]$/).test(part) ? '' : part;
409 }).replace(/\/+$/,'')+'/';
416 * History.getFullUrl(url)
417 * Ensures that we have an absolute URL and not a relative URL
418 * @param {string} url
419 * @param {Boolean} allowBaseHref
420 * @return {string} fullUrl
422 History.getFullUrl = function(url,allowBaseHref){
424 var fullUrl = url, firstChar = url.substring(0,1);
425 allowBaseHref = (typeof allowBaseHref === 'undefined') ? true : allowBaseHref;
428 if ( /[a-z]+\:\/\//.test(url) ) {
431 else if ( firstChar === '/' ) {
433 fullUrl = History.getRootUrl()+url.replace(/^\/+/,'');
435 else if ( firstChar === '#' ) {
437 fullUrl = History.getPageUrl().replace(/#.*/,'')+url;
439 else if ( firstChar === '?' ) {
441 fullUrl = History.getPageUrl().replace(/[\?#].*/,'')+url;
445 if ( allowBaseHref ) {
446 fullUrl = History.getBaseUrl()+url.replace(/^(\.\/)+/,'');
448 fullUrl = History.getBasePageUrl()+url.replace(/^(\.\/)+/,'');
450 // We have an if condition above as we do not want hashes
451 // which are relative to the baseHref in our URLs
452 // as if the baseHref changes, then all our bookmarks
453 // would now point to different locations
454 // whereas the basePageUrl will always stay the same
458 return fullUrl.replace(/\#$/,'');
462 * History.getShortUrl(url)
463 * Ensures that we have a relative URL and not a absolute URL
464 * @param {string} url
465 * @return {string} url
467 History.getShortUrl = function(url){
469 var shortUrl = url, baseUrl = History.getBaseUrl(), rootUrl = History.getRootUrl();
472 if ( History.emulated.pushState ) {
473 // We are in a if statement as when pushState is not emulated
474 // The actual url these short urls are relative to can change
475 // So within the same session, we the url may end up somewhere different
476 shortUrl = shortUrl.replace(baseUrl,'');
480 shortUrl = shortUrl.replace(rootUrl,'/');
482 // Ensure we can still detect it as a state
483 if ( History.isTraditionalAnchor(shortUrl) ) {
484 shortUrl = './'+shortUrl;
488 shortUrl = shortUrl.replace(/^(\.\/)+/g,'./').replace(/\#$/,'');
495 * History.getLocationHref(document)
496 * Returns a normalized version of document.location.href
497 * accounting for browser inconsistencies, etc.
499 * This URL will be URI-encoded and will include the hash
501 * @param {object} document
502 * @return {string} url
504 History.getLocationHref = function(doc) {
505 doc = doc || window.document;
507 // most of the time, this will be true
508 if (doc.URL === doc.location.href)
509 return doc.location.href;
511 // some versions of webkit URI-decode document.location.href
512 // but they leave document.URL in an encoded state
513 if (doc.location.href === decodeURIComponent(doc.URL))
516 // FF 3.6 only updates document.URL when a page is reloaded
517 // document.location.href is updated correctly
518 if (doc.location.hash && decodeURIComponent(doc.location.href.replace(/^[^#]+/, "")) === doc.location.hash)
519 return doc.location.href;
521 if (doc.URL.indexOf('#') == -1 && doc.location.href.indexOf('#') != -1)
522 return doc.location.href;
524 return doc.URL || doc.location.href;
528 // ====================================================================
533 * The store for all session specific data
539 * 1-1: State ID to State Object
541 History.idToState = History.idToState||{};
545 * 1-1: State String to State ID
547 History.stateToId = History.stateToId||{};
551 * 1-1: State URL to State ID
553 History.urlToId = History.urlToId||{};
556 * History.storedStates
557 * Store the states in an array
559 History.storedStates = History.storedStates||[];
562 * History.savedStates
563 * Saved the states in an array
565 History.savedStates = History.savedStates||[];
568 * History.noramlizeStore()
569 * Noramlize the store by adding necessary values
571 History.normalizeStore = function(){
572 History.store.idToState = History.store.idToState||{};
573 History.store.urlToId = History.store.urlToId||{};
574 History.store.stateToId = History.store.stateToId||{};
579 * Get an object containing the data, title and url of the current state
580 * @param {Boolean} friendly
581 * @param {Boolean} create
582 * @return {Object} State
584 History.getState = function(friendly,create){
586 if ( typeof friendly === 'undefined' ) { friendly = true; }
587 if ( typeof create === 'undefined' ) { create = true; }
590 var State = History.getLastSavedState();
593 if ( !State && create ) {
594 State = History.createStateObject();
599 State = History.cloneObject(State);
600 State.url = State.cleanUrl||State.url;
608 * History.getIdByState(State)
609 * Gets a ID for a State
610 * @param {State} newState
611 * @return {String} id
613 History.getIdByState = function(newState){
616 var id = History.extractId(newState.url),
620 // Find ID via State String
621 str = History.getStateString(newState);
622 if ( typeof History.stateToId[str] !== 'undefined' ) {
623 id = History.stateToId[str];
625 else if ( typeof History.store.stateToId[str] !== 'undefined' ) {
626 id = History.store.stateToId[str];
631 id = (new Date()).getTime() + String(Math.random()).replace(/\D/g,'');
632 if ( typeof History.idToState[id] === 'undefined' && typeof History.store.idToState[id] === 'undefined' ) {
637 // Apply the new State to the ID
638 History.stateToId[str] = id;
639 History.idToState[id] = newState;
648 * History.normalizeState(State)
649 * Expands a State Object
650 * @param {object} State
653 History.normalizeState = function(oldState){
655 var newState, dataNotEmpty;
658 if ( !oldState || (typeof oldState !== 'object') ) {
663 if ( typeof oldState.normalized !== 'undefined' ) {
668 if ( !oldState.data || (typeof oldState.data !== 'object') ) {
672 // ----------------------------------------------------------------
676 newState.normalized = true;
677 newState.title = oldState.title||'';
678 newState.url = History.getFullUrl(oldState.url?oldState.url:(History.getLocationHref()));
679 newState.hash = History.getShortUrl(newState.url);
680 newState.data = History.cloneObject(oldState.data);
683 newState.id = History.getIdByState(newState);
685 // ----------------------------------------------------------------
688 newState.cleanUrl = newState.url.replace(/\??\&_suid.*/,'');
689 newState.url = newState.cleanUrl;
691 // Check to see if we have more than just a url
692 dataNotEmpty = !History.isEmptyObject(newState.data);
695 if ( (newState.title || dataNotEmpty) && History.options.disableSuid !== true ) {
697 newState.hash = History.getShortUrl(newState.url).replace(/\??\&_suid.*/,'');
698 if ( !/\?/.test(newState.hash) ) {
699 newState.hash += '?';
701 newState.hash += '&_suid='+newState.id;
704 // Create the Hashed URL
705 newState.hashedUrl = History.getFullUrl(newState.hash);
707 // ----------------------------------------------------------------
709 // Update the URL if we have a duplicate
710 if ( (History.emulated.pushState || History.bugs.safariPoll) && History.hasUrlDuplicate(newState) ) {
711 newState.url = newState.hashedUrl;
714 // ----------------------------------------------------------------
721 * History.createStateObject(data,title,url)
722 * Creates a object based on the data, title and url state params
723 * @param {object} data
724 * @param {string} title
725 * @param {string} url
728 History.createStateObject = function(data,title,url){
737 State = History.normalizeState(State);
744 * History.getStateById(id)
745 * Get a state by it's UID
748 History.getStateById = function(id){
753 var State = History.idToState[id] || History.store.idToState[id] || undefined;
760 * Get a State's String
761 * @param {State} passedState
763 History.getStateString = function(passedState){
765 var State, cleanedState, str;
768 State = History.normalizeState(passedState);
773 title: passedState.title,
778 str = JSON.stringify(cleanedState);
786 * @param {State} passedState
787 * @return {String} id
789 History.getStateId = function(passedState){
794 State = History.normalizeState(passedState);
804 * History.getHashByState(State)
805 * Creates a Hash for the State Object
806 * @param {State} passedState
807 * @return {String} hash
809 History.getHashByState = function(passedState){
814 State = History.normalizeState(passedState);
824 * History.extractId(url_or_hash)
825 * Get a State ID by it's URL or Hash
826 * @param {string} url_or_hash
827 * @return {string} id
829 History.extractId = function ( url_or_hash ) {
831 var id,parts,url, tmp;
835 // If the URL has a #, use the id from before the #
836 if (url_or_hash.indexOf('#') != -1)
838 tmp = url_or_hash.split("#")[0];
845 parts = /(.*)\&_suid=([0-9]+)$/.exec(tmp);
846 url = parts ? (parts[1]||url_or_hash) : url_or_hash;
847 id = parts ? String(parts[2]||'') : '';
854 * History.isTraditionalAnchor
855 * Checks to see if the url is a traditional anchor or not
856 * @param {String} url_or_hash
859 History.isTraditionalAnchor = function(url_or_hash){
861 var isTraditional = !(/[\/\?\.]/.test(url_or_hash));
864 return isTraditional;
868 * History.extractState
869 * Get a State by it's URL or Hash
870 * @param {String} url_or_hash
871 * @return {State|null}
873 History.extractState = function(url_or_hash,create){
875 var State = null, id, url;
876 create = create||false;
879 id = History.extractId(url_or_hash);
881 State = History.getStateById(id);
884 // Fetch SUID returned no State
887 url = History.getFullUrl(url_or_hash);
890 id = History.getIdByUrl(url)||false;
892 State = History.getStateById(id);
896 if ( !State && create && !History.isTraditionalAnchor(url_or_hash) ) {
897 State = History.createStateObject(null,null,url);
906 * History.getIdByUrl()
907 * Get a State ID by a State URL
909 History.getIdByUrl = function(url){
911 var id = History.urlToId[url] || History.store.urlToId[url] || undefined;
918 * History.getLastSavedState()
919 * Get an object containing the data, title and url of the current state
920 * @return {Object} State
922 History.getLastSavedState = function(){
923 return History.savedStates[History.savedStates.length-1]||undefined;
927 * History.getLastStoredState()
928 * Get an object containing the data, title and url of the current state
929 * @return {Object} State
931 History.getLastStoredState = function(){
932 return History.storedStates[History.storedStates.length-1]||undefined;
936 * History.hasUrlDuplicate
937 * Checks if a Url will have a url conflict
938 * @param {Object} newState
939 * @return {Boolean} hasDuplicate
941 History.hasUrlDuplicate = function(newState) {
943 var hasDuplicate = false,
947 oldState = History.extractState(newState.url);
950 hasDuplicate = oldState && oldState.id !== newState.id;
959 * @param {Object} newState
960 * @return {Object} newState
962 History.storeState = function(newState){
964 History.urlToId[newState.url] = newState.id;
967 History.storedStates.push(History.cloneObject(newState));
974 * History.isLastSavedState(newState)
975 * Tests to see if the state is the last state
976 * @param {Object} newState
977 * @return {boolean} isLast
979 History.isLastSavedState = function(newState){
982 newId, oldState, oldId;
985 if ( History.savedStates.length ) {
987 oldState = History.getLastSavedState();
991 isLast = (newId === oldId);
1001 * @param {Object} newState
1002 * @return {boolean} changed
1004 History.saveState = function(newState){
1006 if ( History.isLastSavedState(newState) ) {
1011 History.savedStates.push(History.cloneObject(newState));
1018 * History.getStateByIndex()
1019 * Gets a state by the index
1020 * @param {integer} index
1023 History.getStateByIndex = function(index){
1028 if ( typeof index === 'undefined' ) {
1029 // Get the last inserted
1030 State = History.savedStates[History.savedStates.length-1];
1032 else if ( index < 0 ) {
1034 State = History.savedStates[History.savedStates.length+index];
1037 // Get from the beginning
1038 State = History.savedStates[index];
1046 * History.getCurrentIndex()
1047 * Gets the current index
1050 History.getCurrentIndex = function(){
1055 if(History.savedStates.length < 1) {
1059 index = History.savedStates.length-1;
1064 // ====================================================================
1069 * @param {Location=} location
1070 * Gets the current document hash
1071 * Note: unlike location.hash, this is guaranteed to return the escaped hash in all browsers
1074 History.getHash = function(doc){
1075 var url = History.getLocationHref(doc),
1077 hash = History.getHashByUrl(url);
1082 * History.unescapeHash()
1083 * normalize and Unescape a Hash
1084 * @param {String} hash
1087 History.unescapeHash = function(hash){
1089 var result = History.normalizeHash(hash);
1092 result = decodeURIComponent(result);
1099 * History.normalizeHash()
1100 * normalize a hash across browsers
1103 History.normalizeHash = function(hash){
1105 var result = hash.replace(/[^#]*#/,'').replace(/#.*/, '');
1112 * History.setHash(hash)
1113 * Sets the document hash
1114 * @param {string} hash
1117 History.setHash = function(hash,queue){
1122 if ( queue !== false && History.busy() ) {
1123 // Wait + Push to Queue
1124 //History.debug('History.setHash: we must wait', arguments);
1127 callback: History.setHash,
1135 //History.debug('History.setHash: called',hash);
1137 // Make Busy + Continue
1140 // Check if hash is a state
1141 State = History.extractState(hash,true);
1142 if ( State && !History.emulated.pushState ) {
1143 // Hash is a state so skip the setHash
1144 //History.debug('History.setHash: Hash is a state so skipping the hash set with a direct pushState call',arguments);
1147 History.pushState(State.data,State.title,State.url,false);
1149 else if ( History.getHash() !== hash ) {
1150 // Hash is a proper hash, so apply it
1152 // Handle browser bugs
1153 if ( History.bugs.setHash ) {
1154 // Fix Safari Bug https://bugs.webkit.org/show_bug.cgi?id=56249
1156 // Fetch the base page
1157 pageUrl = History.getPageUrl();
1159 // Safari hash apply
1160 History.pushState(null,null,pageUrl+'#'+hash,false);
1163 // Normal hash apply
1164 window.document.location.hash = hash;
1174 * normalize and Escape a Hash
1177 History.escapeHash = function(hash){
1179 var result = History.normalizeHash(hash);
1182 result = window.encodeURIComponent(result);
1185 if ( !History.bugs.hashEscape ) {
1186 // Restore common parts
1188 .replace(/\%21/g,'!')
1189 .replace(/\%26/g,'&')
1190 .replace(/\%3D/g,'=')
1191 .replace(/\%3F/g,'?');
1199 * History.getHashByUrl(url)
1200 * Extracts the Hash from a URL
1201 * @param {string} url
1202 * @return {string} url
1204 History.getHashByUrl = function(url){
1206 var hash = String(url)
1207 .replace(/([^#]*)#?([^#]*)#?(.*)/, '$2')
1211 hash = History.unescapeHash(hash);
1218 * History.setTitle(title)
1219 * Applies the title to the document
1220 * @param {State} newState
1223 History.setTitle = function(newState){
1225 var title = newState.title,
1230 firstState = History.getStateByIndex(0);
1231 if ( firstState && firstState.url === newState.url ) {
1232 title = firstState.title||History.options.initialTitle;
1238 window.document.getElementsByTagName('title')[0].innerHTML = title.replace('<','<').replace('>','>').replace(' & ',' & ');
1240 catch ( Exception ) { }
1241 window.document.title = title;
1248 // ====================================================================
1253 * The list of queues to use
1254 * First In, First Out
1256 History.queues = [];
1259 * History.busy(value)
1260 * @param {boolean} value [optional]
1261 * @return {boolean} busy
1263 History.busy = function(value){
1265 if ( typeof value !== 'undefined' ) {
1266 //History.debug('History.busy: changing ['+(History.busy.flag||false)+'] to ['+(value||false)+']', History.queues.length);
1267 History.busy.flag = value;
1270 else if ( typeof History.busy.flag === 'undefined' ) {
1271 History.busy.flag = false;
1275 if ( !History.busy.flag ) {
1276 // Execute the next item in the queue
1277 window.clearTimeout(History.busy.timeout);
1278 var fireNext = function(){
1280 if ( History.busy.flag ) return;
1281 for ( i=History.queues.length-1; i >= 0; --i ) {
1282 queue = History.queues[i];
1283 if ( queue.length === 0 ) continue;
1284 item = queue.shift();
1285 History.fireQueueItem(item);
1286 History.busy.timeout = window.setTimeout(fireNext,History.options.busyDelay);
1289 History.busy.timeout = window.setTimeout(fireNext,History.options.busyDelay);
1293 return History.busy.flag;
1299 History.busy.flag = false;
1302 * History.fireQueueItem(item)
1304 * @param {Object} item
1305 * @return {Mixed} result
1307 History.fireQueueItem = function(item){
1308 return item.callback.apply(item.scope||History,item.args||[]);
1312 * History.pushQueue(callback,args)
1313 * Add an item to the queue
1314 * @param {Object} item [scope,callback,args,queue]
1316 History.pushQueue = function(item){
1317 // Prepare the queue
1318 History.queues[item.queue||0] = History.queues[item.queue||0]||[];
1321 History.queues[item.queue||0].push(item);
1328 * History.queue (item,queue), (func,queue), (func), (item)
1329 * Either firs the item now if not busy, or adds it to the queue
1331 History.queue = function(item,queue){
1333 if ( typeof item === 'function' ) {
1338 if ( typeof queue !== 'undefined' ) {
1343 if ( History.busy() ) {
1344 History.pushQueue(item);
1346 History.fireQueueItem(item);
1354 * History.clearQueue()
1357 History.clearQueue = function(){
1358 History.busy.flag = false;
1359 History.queues = [];
1364 // ====================================================================
1368 * History.stateChanged
1369 * States whether or not the state has changed since the last double check was initialised
1371 History.stateChanged = false;
1374 * History.doubleChecker
1375 * Contains the timeout used for the double checks
1377 History.doubleChecker = false;
1380 * History.doubleCheckComplete()
1381 * Complete a double check
1384 History.doubleCheckComplete = function(){
1386 History.stateChanged = true;
1389 History.doubleCheckClear();
1396 * History.doubleCheckClear()
1397 * Clear a double check
1400 History.doubleCheckClear = function(){
1402 if ( History.doubleChecker ) {
1403 window.clearTimeout(History.doubleChecker);
1404 History.doubleChecker = false;
1412 * History.doubleCheck()
1413 * Create a double check
1416 History.doubleCheck = function(tryAgain){
1418 History.stateChanged = false;
1419 History.doubleCheckClear();
1421 // Fix IE6,IE7 bug where calling history.back or history.forward does not actually change the hash (whereas doing it manually does)
1422 // Fix Safari 5 bug where sometimes the state does not change: https://bugs.webkit.org/show_bug.cgi?id=42940
1423 if ( History.bugs.ieDoubleCheck ) {
1425 History.doubleChecker = window.setTimeout(
1427 History.doubleCheckClear();
1428 if ( !History.stateChanged ) {
1429 //History.debug('History.doubleCheck: State has not yet changed, trying again', arguments);
1435 History.options.doubleCheckInterval
1444 // ====================================================================
1448 * History.safariStatePoll()
1449 * Poll the current state
1452 History.safariStatePoll = function(){
1455 // Get the Last State which has the new URL
1457 urlState = History.extractState(History.getLocationHref()),
1460 // Check for a difference
1461 if ( !History.isLastSavedState(urlState) ) {
1462 newState = urlState;
1468 // Check if we have a state with that url
1471 //History.debug('History.safariStatePoll: new');
1472 newState = History.createStateObject();
1475 // Apply the New State
1476 //History.debug('History.safariStatePoll: trigger');
1477 History.Adapter.trigger(window,'popstate');
1484 // ====================================================================
1488 * History.back(queue)
1489 * Send the browser history back one item
1490 * @param {Integer} queue [optional]
1492 History.back = function(queue){
1493 //History.debug('History.back: called', arguments);
1496 if ( queue !== false && History.busy() ) {
1497 // Wait + Push to Queue
1498 //History.debug('History.back: we must wait', arguments);
1501 callback: History.back,
1508 // Make Busy + Continue
1511 // Fix certain browser bugs that prevent the state from changing
1512 History.doubleCheck(function(){
1513 History.back(false);
1524 * History.forward(queue)
1525 * Send the browser history forward one item
1526 * @param {Integer} queue [optional]
1528 History.forward = function(queue){
1529 //History.debug('History.forward: called', arguments);
1532 if ( queue !== false && History.busy() ) {
1533 // Wait + Push to Queue
1534 //History.debug('History.forward: we must wait', arguments);
1537 callback: History.forward,
1544 // Make Busy + Continue
1547 // Fix certain browser bugs that prevent the state from changing
1548 History.doubleCheck(function(){
1549 History.forward(false);
1555 // End forward closure
1560 * History.go(index,queue)
1561 * Send the browser history back or forward index times
1562 * @param {Integer} queue [optional]
1564 History.go = function(index,queue){
1565 //History.debug('History.go: called', arguments);
1573 for ( i=1; i<=index; ++i ) {
1574 History.forward(queue);
1577 else if ( index < 0 ) {
1579 for ( i=-1; i>=index; --i ) {
1580 History.back(queue);
1584 throw new Error('History.go: History.go requires a positive or negative integer passed.');
1592 // ====================================================================
1593 // HTML5 State Support
1595 // Non-Native pushState Implementation
1596 if ( History.emulated.pushState ) {
1598 * Provide Skeleton for HTML4 Browsers
1602 var emptyFunction = function(){};
1603 History.pushState = History.pushState||emptyFunction;
1604 History.replaceState = History.replaceState||emptyFunction;
1605 } // History.emulated.pushState
1607 // Native pushState Implementation
1610 * Use native HTML5 History API Implementation
1614 * History.onPopState(event,extra)
1615 * Refresh the Current State
1617 History.onPopState = function(event,extra){
1619 var stateId = false, newState = false, currentHash, currentState;
1621 // Reset the double check
1622 History.doubleCheckComplete();
1624 // Check for a Hash, and handle apporiatly
1625 currentHash = History.getHash();
1626 if ( currentHash ) {
1628 currentState = History.extractState(currentHash||History.getLocationHref(),true);
1629 if ( currentState ) {
1630 // We were able to parse it, it must be a State!
1631 // Let's forward to replaceState
1632 //History.debug('History.onPopState: state anchor', currentHash, currentState);
1633 History.replaceState(currentState.data, currentState.title, currentState.url, false);
1636 // Traditional Anchor
1637 //History.debug('History.onPopState: traditional anchor', currentHash);
1638 History.Adapter.trigger(window,'anchorchange');
1639 History.busy(false);
1642 // We don't care for hashes
1643 History.expectedStateId = false;
1648 stateId = History.Adapter.extractEventData('state',event,extra) || false;
1652 // Vanilla: Back/forward button was used
1653 newState = History.getStateById(stateId);
1655 else if ( History.expectedStateId ) {
1656 // Vanilla: A new state was pushed, and popstate was called manually
1657 newState = History.getStateById(History.expectedStateId);
1661 newState = History.extractState(History.getLocationHref());
1664 // The State did not exist in our store
1666 // Regenerate the State
1667 newState = History.createStateObject(null,null,History.getLocationHref());
1671 History.expectedStateId = false;
1673 // Check if we are the same state
1674 if ( History.isLastSavedState(newState) ) {
1675 // There has been no change (just the page's hash has finally propagated)
1676 //History.debug('History.onPopState: no change', newState, History.savedStates);
1677 History.busy(false);
1682 History.storeState(newState);
1683 History.saveState(newState);
1685 // Force update of the title
1686 History.setTitle(newState);
1689 History.Adapter.trigger(window,'statechange');
1690 History.busy(false);
1695 History.Adapter.bind(window,'popstate',History.onPopState);
1698 * History.pushState(data,title,url)
1699 * Add a new State to the history object, become it, and trigger onpopstate
1700 * We have to trigger for HTML4 compatibility
1701 * @param {object} data
1702 * @param {string} title
1703 * @param {string} url
1706 History.pushState = function(data,title,url,queue){
1707 //History.debug('History.pushState: called', arguments);
1710 if ( History.getHashByUrl(url) && History.emulated.pushState ) {
1711 throw new Error('History.js does not support states with fragement-identifiers (hashes/anchors).');
1715 if ( queue !== false && History.busy() ) {
1716 // Wait + Push to Queue
1717 //History.debug('History.pushState: we must wait', arguments);
1720 callback: History.pushState,
1727 // Make Busy + Continue
1730 // Create the newState
1731 var newState = History.createStateObject(data,title,url);
1734 if ( History.isLastSavedState(newState) ) {
1735 // Won't be a change
1736 History.busy(false);
1739 // Store the newState
1740 History.storeState(newState);
1741 History.expectedStateId = newState.id;
1743 // Push the newState
1744 history.pushState(newState.id,newState.title,newState.url);
1747 History.Adapter.trigger(window,'popstate');
1750 // End pushState closure
1755 * History.replaceState(data,title,url)
1756 * Replace the State and trigger onpopstate
1757 * We have to trigger for HTML4 compatibility
1758 * @param {object} data
1759 * @param {string} title
1760 * @param {string} url
1763 History.replaceState = function(data,title,url,queue){
1764 //History.debug('History.replaceState: called', arguments);
1767 if ( History.getHashByUrl(url) && History.emulated.pushState ) {
1768 throw new Error('History.js does not support states with fragement-identifiers (hashes/anchors).');
1772 if ( queue !== false && History.busy() ) {
1773 // Wait + Push to Queue
1774 //History.debug('History.replaceState: we must wait', arguments);
1777 callback: History.replaceState,
1784 // Make Busy + Continue
1787 // Create the newState
1788 var newState = History.createStateObject(data,title,url);
1791 if ( History.isLastSavedState(newState) ) {
1792 // Won't be a change
1793 History.busy(false);
1796 // Store the newState
1797 History.storeState(newState);
1798 History.expectedStateId = newState.id;
1800 // Push the newState
1801 history.replaceState(newState.id,newState.title,newState.url);
1804 History.Adapter.trigger(window,'popstate');
1807 // End replaceState closure
1811 } // !History.emulated.pushState
1814 // ====================================================================
1820 if ( sessionStorage ) {
1823 History.store = JSON.parse(sessionStorage.getItem('History.store'))||{};
1830 History.normalizeStore();
1835 History.normalizeStore();
1839 * Clear Intervals on exit to prevent memory leaks
1841 History.Adapter.bind(window,"unload",History.clearAllIntervals);
1844 * Create the initial State
1846 History.saveState(History.storeState(History.extractState(History.getLocationHref(),true)));
1849 * Bind for Saving Store
1851 if ( sessionStorage ) {
1852 // When the page is closed
1853 History.onUnload = function(){
1855 var currentStore, item, currentStoreString;
1859 currentStore = JSON.parse(sessionStorage.getItem('History.store'))||{};
1866 currentStore.idToState = currentStore.idToState || {};
1867 currentStore.urlToId = currentStore.urlToId || {};
1868 currentStore.stateToId = currentStore.stateToId || {};
1871 for ( item in History.idToState ) {
1872 if ( !History.idToState.hasOwnProperty(item) ) {
1875 currentStore.idToState[item] = History.idToState[item];
1877 for ( item in History.urlToId ) {
1878 if ( !History.urlToId.hasOwnProperty(item) ) {
1881 currentStore.urlToId[item] = History.urlToId[item];
1883 for ( item in History.stateToId ) {
1884 if ( !History.stateToId.hasOwnProperty(item) ) {
1887 currentStore.stateToId[item] = History.stateToId[item];
1891 History.store = currentStore;
1892 History.normalizeStore();
1894 // In Safari, going into Private Browsing mode causes the
1895 // Session Storage object to still exist but if you try and use
1896 // or set any property/function of it it throws the exception
1897 // "QUOTA_EXCEEDED_ERR: DOM Exception 22: An attempt was made to
1898 // add something to storage that exceeded the quota." infinitely
1900 currentStoreString = JSON.stringify(currentStore);
1903 sessionStorage.setItem('History.store', currentStoreString);
1906 if (e.code === DOMException.QUOTA_EXCEEDED_ERR) {
1907 if (sessionStorage.length) {
1908 // Workaround for a bug seen on iPads. Sometimes the quota exceeded error comes up and simply
1909 // removing/resetting the storage can work.
1910 sessionStorage.removeItem('History.store');
1911 sessionStorage.setItem('History.store', currentStoreString);
1913 // Otherwise, we're probably private browsing in Safari, so we'll ignore the exception.
1921 // For Internet Explorer
1922 History.intervalList.push(setInterval(History.onUnload,History.options.storeInterval));
1924 // For Other Browsers
1925 History.Adapter.bind(window,'beforeunload',History.onUnload);
1926 History.Adapter.bind(window,'unload',History.onUnload);
1928 // Both are enabled for consistency
1931 // Non-Native pushState Implementation
1932 if ( !History.emulated.pushState ) {
1933 // Be aware, the following is only for native pushState implementations
1934 // If you are wanting to include something for all browsers
1935 // Then include it above this if block
1940 if ( History.bugs.safariPoll ) {
1941 History.intervalList.push(setInterval(History.safariStatePoll, History.options.safariPollInterval));
1945 * Ensure Cross Browser Compatibility
1947 if ( window.navigator.vendor === 'Apple Computer, Inc.' || (window.navigator.appCodeName||'') === 'Mozilla' ) {
1949 * Fix Safari HashChange Issue
1953 History.Adapter.bind(window,'hashchange',function(){
1954 History.Adapter.trigger(window,'popstate');
1958 if ( History.getHash() ) {
1959 History.Adapter.onDomLoad(function(){
1960 History.Adapter.trigger(window,'hashchange');
1965 } // !History.emulated.pushState
1968 }; // History.initCore
1970 // Try to Initialise History
1971 if (!History.options || !History.options.delayInit) {