X-Git-Url: http://git.roojs.org/?a=blobdiff_plain;f=Roo%2FHistory.js;h=91ae8d22f71197b3e0a5a4c5f89d7ca2f31ac0da;hb=61bc45258e16a779856dd2ad0862630b489e4583;hp=118245b7d6523624948612320f65f37962a42856;hpb=7c43541e11b74c1f3899d4b07e54aa8cb2759d8d;p=roojs1 diff --git a/Roo/History.js b/Roo/History.js index 118245b7d6..91ae8d22f7 100644 --- a/Roo/History.js +++ b/Roo/History.js @@ -1,10 +1,20 @@ /** * Originally based of this code... - refactored for Roo... - * + * https://github.com/browserstate/history.js + * History.js Core * @author Benjamin Arthur Lupton * @copyright 2010-2011 Benjamin Arthur Lupton * @license New BSD License + * + * Hackily modifyed by alan@roojs.com + * + * this is not initialized automatically.. + * must call Roo.History.init( { ... options... }); + * + * TOTALLY UNTESTED... + * + * Documentation to be done.... */ Roo.History = { @@ -27,49 +37,49 @@ Roo.History = { safariPollInterval : 500, /** - * History.options.doubleCheckInterval + * doubleCheckInterval * How long should the interval be before we perform a double check */ doubleCheckInterval : 500, /** - * History.options.disableSuid + * disableSuid * Force this.not to append suid */ disableSuid : false, /** - * History.options.storeInterval + * storeInterval * How long should we wait between store calls */ storeInterval : 1000, /** - * History.options.busyDelay + * busyDelay * How long should we wait between busy events */ busyDelay : 250, /** - * History.options.debug + * debug * If true will enable debug messages to be logged */ debug : false, /** - * History.options.initialTitle + * initialTitle * What is the title of the initial state */ initialTitle : '', /** - * History.options.html4Mode + * html4Mode * If true, will force HTMl4 mode (hashtags) */ html4Mode : false, /** - * History.options.delayInit + * delayInit * Want to override default options and call init manually. */ delayInit : false, @@ -111,6 +121,10 @@ Roo.History = { sessionStorage : false, // sessionStorage intervalList : false, // array normally. + /** + * enabled + * Is History enabled? + */ enabled : false, // ==================================================================== @@ -123,112 +137,187 @@ Roo.History = { store :false, /** - * History.idToState + * idToState * 1-1: State ID to State Object */ idToState : false, /** - * History.stateToId + * stateToId * 1-1: State String to State ID */ stateToId :false, /** - * History.urlToId + * urlToId * 1-1: State URL to State ID */ urlToId : false, /** - * History.storedStates + * storedStates * Store the states in an array */ storedStates : false, /** - * History.savedStates + * savedStates * Saved the states in an array */ savedStates : false, + /** + * queues + * The list of queues to use + * First In, First Out + */ + queues : [], + + /** + * busy.flag + */ + busy_flag : false, + + // ==================================================================== + // IE Bug Fix + + /** + * History.stateChanged + * States whether or not the state has changed since the last double check was initialised + */ + stateChanged : false, + + /** + * History.doubleChecker + * Contains the timeout used for the double checks + */ + doubleChecker : false, // Initialise History - init : function(options){ + init : function(options) + { + + var emptyFunction = function(){}; + var _this = this; - initialTitle : window.document.title, + + this.initialTitle = window.document.title; + this.store = {}; + this.idToState={}; + this.stateToId={}; + this.urlToId={}; + this.storedStates=[]; + this.savedStates=[]; + this.queues = []; Roo.apply(this,options) - // Check Load Status of Adapter - //if ( typeof this.Adapter === 'undefined' ) { - // return false; - //} - + // Check Load Status of Core - if ( typeof this.initCore !== 'undefined' ) { - this.initCore(); - } - + this.initCore(); + // Check Load Status of HTML4 Support - if ( typeof this.initHtml4 !== 'undefined' ) { - this.initHtml4(); - } + //if ( typeof this.initHtml4 !== 'undefined' ) { + // this.initHtml4(); + //} this.initEmulated(); - /** - * History.enabled - * Is History enabled? - */ + + this.enabled = !this.emulated.pushState; + if ( this.emulated.pushState ) { + + // Prepare + + this.pushState = emptyFunction; + this.replaceState = emptyFunction; + } + + this.initBugs(); - // ==================================================================== - // State Storage - - /** - * store - * The store for all session specific data - */ - History.store = {}; - - /** - * History.idToState - * 1-1: State ID to State Object - */ - History.idToState = History.idToState||{}; - + + + Roo.get(window).on('popstate',this.onPopState, this); + + /** - * History.stateToId - * 1-1: State String to State ID + * Load the Store */ - History.stateToId = History.stateToId||{}; + if ( this.sessionStorage ) { + // Fetch + try { + this.store = JSON.parse(this.sessionStorage.getItem('Roo.History.store'))||{}; + } + catch ( err ) { + this.store = {}; + } + this.intervalList.push(setInterval(this.onUnload,this.storeInterval)); - /** - * History.urlToId - * 1-1: State URL to State ID - */ - History.urlToId = History.urlToId||{}; + // For Other Browsers + Roo.get(window).on('beforeunload',this.onUnload,this); + Roo.get(window).on('unload',this.onUnload, this); + } else { + this.onUnload = emptyFunction; + } + + + this.normalizeStore(); /** - * History.storedStates - * Store the states in an array + * Clear Intervals on exit to prevent memory leaks */ - History.storedStates = History.storedStates||[]; + Roo.get(window).on('unload',this.clearAllIntervals, this); /** - * History.savedStates - * Saved the states in an array + * Create the initial State */ - History.savedStates = History.savedStates||[]; + this.saveState(this.storeState(this.extractState(this.getLocationHref(),true))); - - - + // Non-Native pushState Implementation + if ( !this.emulated.pushState ) { + // Be aware, the following is only for native pushState implementations + // If you are wanting to include something for all browsers + // Then include it above this if block + + /** + * Setup Safari Fix + */ + if ( this.bugs.safariPoll ) { + this.intervalList.push(setInterval(this.safariStatePoll, this.safariPollInterval)); + } + + /** + * Ensure Cross Browser Compatibility + */ + //if ( window.navigator.vendor === 'Apple Computer, Inc.' || (window.navigator.appCodeName||'') === 'Mozilla' ) { + if (Roo.isSafari) { + //code + + /** + * Fix Safari HashChange Issue + */ + + // Setup Alias + Roo.get(window).on('hashchange',function(){ + Roo.get(window).fireEvent('popstate'); + }, this); + + // Initialise Alias + if ( this.getHash() ) { + Roo.onReady(function(){ + Roo.get(window).fireEvent('hashchange'); + }); + } + } + + } // !History.emulated.pushState + + // Return true @@ -267,7 +356,7 @@ Roo.History = { /** - * History.clearAllIntervals + * clearAllIntervals * Clears all setInterval instances. */ clearAllIntervals: function() @@ -286,13 +375,13 @@ Roo.History = { // Debug /** - * History.debugLog(message,...) + * debugLog(message,...) * Logs the passed arguments if debug enabled */ debugLog : function() { if ( (this.debug||false) ) { - Roo.log.apply(History,arguments); + Roo.log.apply(this,arguments); } }, @@ -302,7 +391,7 @@ Roo.History = { // Emulated Status /** - * History.getInternetExplorerMajorVersion() + * getInternetExplorerMajorVersion() * Get's the major version of Internet Explorer * @return {integer} * @license Public Domain @@ -373,9 +462,38 @@ Roo.History = { (this.isInternetExplorer() && this.getInternetExplorerMajorVersion() < 8) ); - } + }, - + initBugs : function () + { + + + + /** + * Safari 5 and Safari iOS 4 fail to return to the correct state once a hash is replaced by a `replaceState` call + * https://bugs.webkit.org/show_bug.cgi?id=56249 + */ + this.bugs.setHash = Boolean(!this.emulated.pushState && navigator.vendor === 'Apple Computer, Inc.' + && /AppleWebKit\/5([0-2]|3[0-3])/.test(navigator.userAgent)); + + /** + * Safari 5 and Safari iOS 4 sometimes fail to apply the state change under busy conditions + * https://bugs.webkit.org/show_bug.cgi?id=42940 + */ + this.bugs.safariPoll = Boolean(!this.emulated.pushState && navigator.vendor === 'Apple Computer, Inc.' + && /AppleWebKit\/5([0-2]|3[0-3])/.test(navigator.userAgent)); + + /** + * MSIE 6 and 7 sometimes do not apply a hash even it was told to (requiring a second call to the apply function) + */ + this.bugs.ieDoubleCheck = Boolean(this.isInternetExplorer() && this.getInternetExplorerMajorVersion() < 8); + + /** + * MSIE 6 requires the entire hash to be encoded for the hashes to trigger the onHashChange event + */ + this.bugs.hashEscape = Boolean(this.isInternetExplorer() && this.getInternetExplorerMajorVersion() < 7); + }, + /** * isEmptyObject(obj) @@ -383,1650 +501,1543 @@ Roo.History = { * @param {Object} obj * @return {boolean} */ - isEmptyObject = function(obj) { + isEmptyObject : function(obj) { for ( var name in obj ) { if ( obj.hasOwnProperty(name) ) { return false; } } return true; - }; + }, - /** - * cloneObject(obj) - * Clones a object and eliminate all references to the original contexts - * @param {Object} obj - * @return {Object} - */ - cloneObject = function(obj) { - var hash,newObj; - if ( obj ) { - hash = JSON.stringify(obj); - newObj = JSON.parse(hash); - } - else { - newObj = {}; - } - return newObj; - }; + /** + * cloneObject(obj) + * Clones a object and eliminate all references to the original contexts + * @param {Object} obj + * @return {Object} + */ + cloneObject : function(obj) { + var hash,newObj; + if ( obj ) { + hash = JSON.stringify(obj); + newObj = JSON.parse(hash); + } + else { + newObj = {}; + } + return newObj; + }, - // ==================================================================== - // URL Helpers + // ==================================================================== + // URL Helpers - /** - * getRootUrl() - * Turns "http://mysite.com/dir/page.html?asd" into "http://mysite.com" - * @return {String} rootUrl - */ - getRootUrl = function(){ - // Create - var rootUrl = window.document.location.protocol+'//'+(window.document.location.hostname||window.document.location.host); - if ( window.document.location.port||false ) { - rootUrl += ':'+window.document.location.port; - } - rootUrl += '/'; + /** + * getRootUrl() + * Turns "http://mysite.com/dir/page.html?asd" into "http://mysite.com" + * @return {String} rootUrl + */ + getRootUrl : function(){ + // Create + var rootUrl = window.document.location.protocol+'//'+(window.document.location.hostname||window.document.location.host); + if ( window.document.location.port||false ) { + rootUrl += ':'+window.document.location.port; + } + rootUrl += '/'; - // Return - return rootUrl; - }; + // Return + return rootUrl; + }, - /** - * getBaseHref() - * Fetches the `href` attribute of the `` element if it exists - * @return {String} baseHref - */ - getBaseHref = function(){ - // Create - var - baseElements = window.document.getElementsByTagName('base'), - baseElement = null, - baseHref = ''; - - // Test for Base Element - if ( baseElements.length === 1 ) { - // Prepare for Base Element - baseElement = baseElements[0]; - baseHref = baseElement.href.replace(/[^\/]+$/,''); - } + /** + * getBaseHref() + * Fetches the `href` attribute of the `` element if it exists + * @return {String} baseHref + */ + getBaseHref : function(){ + // Create + var + baseElements = window.document.getElementsByTagName('base'), + baseElement = null, + baseHref = ''; + + // Test for Base Element + if ( baseElements.length === 1 ) { + // Prepare for Base Element + baseElement = baseElements[0]; + baseHref = baseElement.href.replace(/[^\/]+$/,''); + } - // Adjust trailing slash - baseHref = baseHref.replace(/\/+$/,''); - if ( baseHref ) baseHref += '/'; + // Adjust trailing slash + baseHref = baseHref.replace(/\/+$/,''); + if ( baseHref ) baseHref += '/'; - // Return - return baseHref; - }; + // Return + return baseHref; + }, - /** - * getBaseUrl() - * Fetches the baseHref or basePageUrl or rootUrl (whichever one exists first) - * @return {String} baseUrl - */ - getBaseUrl = function(){ - // Create - var baseUrl = this.getBaseHref()||this.getBasePageUrl()||this.getRootUrl(); + /** + * getBaseUrl() + * Fetches the baseHref or basePageUrl or rootUrl (whichever one exists first) + * @return {String} baseUrl + */ + getBaseUrl : function(){ + // Create + var baseUrl = this.getBaseHref()||this.getBasePageUrl()||this.getRootUrl(); - // Return - return baseUrl; - }; + // Return + return baseUrl; + }, - /** - * getPageUrl() - * Fetches the URL of the current page - * @return {String} pageUrl - */ - getPageUrl = function(){ - // Fetch - var - State = this.getState(false,false), - stateUrl = (State||{}).url||this.getLocationHref(), - pageUrl; + /** + * getPageUrl() + * Fetches the URL of the current page + * @return {String} pageUrl + */ + getPageUrl : function(){ + // Fetch + var + State = this.getState(false,false), + stateUrl = (State||{}).url||this.getLocationHref(), + pageUrl; + + // Create + pageUrl = stateUrl.replace(/\/+$/,'').replace(/[^\/]+$/,function(part,index,string){ + return (/\./).test(part) ? part : part+'/'; + }); + + // Return + return pageUrl; + }, - // Create - pageUrl = stateUrl.replace(/\/+$/,'').replace(/[^\/]+$/,function(part,index,string){ - return (/\./).test(part) ? part : part+'/'; - }); + /** + * getBasePageUrl() + * Fetches the Url of the directory of the current page + * @return {String} basePageUrl + */ + getBasePageUrl : function(){ + // Create + var basePageUrl = (this.getLocationHref()).replace(/[#\?].*/,'').replace(/[^\/]+$/,function(part,index,string){ + return (/[^\/]$/).test(part) ? '' : part; + }).replace(/\/+$/,'')+'/'; + + // Return + return basePageUrl; + }, - // Return - return pageUrl; - }; + /** + * getFullUrl(url) + * Ensures that we have an absolute URL and not a relative URL + * @param {string} url + * @param {Boolean} allowBaseHref + * @return {string} fullUrl + */ + getFullUrl : function(url,allowBaseHref){ + // Prepare + var fullUrl = url, firstChar = url.substring(0,1); + allowBaseHref = (typeof allowBaseHref === 'undefined') ? true : allowBaseHref; + + // Check + if ( /[a-z]+\:\/\//.test(url) ) { + // Full URL + } + else if ( firstChar === '/' ) { + // Root URL + fullUrl = this.getRootUrl()+url.replace(/^\/+/,''); + } + else if ( firstChar === '#' ) { + // Anchor URL + fullUrl = this.getPageUrl().replace(/#.*/,'')+url; + } + else if ( firstChar === '?' ) { + // Query URL + fullUrl = this.getPageUrl().replace(/[\?#].*/,'')+url; + } + else { + // Relative URL + if ( allowBaseHref ) { + fullUrl = this.getBaseUrl()+url.replace(/^(\.\/)+/,''); + } else { + fullUrl = this.getBasePageUrl()+url.replace(/^(\.\/)+/,''); + } + // We have an if condition above as we do not want hashes + // which are relative to the baseHref in our URLs + // as if the baseHref changes, then all our bookmarks + // would now point to different locations + // whereas the basePageUrl will always stay the same + } - /** - * getBasePageUrl() - * Fetches the Url of the directory of the current page - * @return {String} basePageUrl - */ - getBasePageUrl = function(){ - // Create - var basePageUrl = (this.getLocationHref()).replace(/[#\?].*/,'').replace(/[^\/]+$/,function(part,index,string){ - return (/[^\/]$/).test(part) ? '' : part; - }).replace(/\/+$/,'')+'/'; + // Return + return fullUrl.replace(/\#$/,''); + }, - // Return - return basePageUrl; - }; + /** + * getShortUrl(url) + * Ensures that we have a relative URL and not a absolute URL + * @param {string} url + * @return {string} url + */ + getShortUrl : function(url){ + // Prepare + var shortUrl = url, baseUrl = this.getBaseUrl(), rootUrl = this.getRootUrl(); + + // Trim baseUrl + if ( this.emulated.pushState ) { + // We are in a if statement as when pushState is not emulated + // The actual url these short urls are relative to can change + // So within the same session, we the url may end up somewhere different + shortUrl = shortUrl.replace(baseUrl,''); + } - /** - * getFullUrl(url) - * Ensures that we have an absolute URL and not a relative URL - * @param {string} url - * @param {Boolean} allowBaseHref - * @return {string} fullUrl - */ - getFullUrl = function(url,allowBaseHref){ - // Prepare - var fullUrl = url, firstChar = url.substring(0,1); - allowBaseHref = (typeof allowBaseHref === 'undefined') ? true : allowBaseHref; + // Trim rootUrl + shortUrl = shortUrl.replace(rootUrl,'/'); - // Check - if ( /[a-z]+\:\/\//.test(url) ) { - // Full URL - } - else if ( firstChar === '/' ) { - // Root URL - fullUrl = this.getRootUrl()+url.replace(/^\/+/,''); - } - else if ( firstChar === '#' ) { - // Anchor URL - fullUrl = this.getPageUrl().replace(/#.*/,'')+url; - } - else if ( firstChar === '?' ) { - // Query URL - fullUrl = this.getPageUrl().replace(/[\?#].*/,'')+url; - } - else { - // Relative URL - if ( allowBaseHref ) { - fullUrl = this.getBaseUrl()+url.replace(/^(\.\/)+/,''); - } else { - fullUrl = this.getBasePageUrl()+url.replace(/^(\.\/)+/,''); - } - // We have an if condition above as we do not want hashes - // which are relative to the baseHref in our URLs - // as if the baseHref changes, then all our bookmarks - // would now point to different locations - // whereas the basePageUrl will always stay the same - } + // Ensure we can still detect it as a state + if ( this.isTraditionalAnchor(shortUrl) ) { + shortUrl = './'+shortUrl; + } - // Return - return fullUrl.replace(/\#$/,''); - }; + // Clean It + shortUrl = shortUrl.replace(/^(\.\/)+/g,'./').replace(/\#$/,''); - /** - * getShortUrl(url) - * Ensures that we have a relative URL and not a absolute URL - * @param {string} url - * @return {string} url - */ - getShortUrl = function(url){ - // Prepare - var shortUrl = url, baseUrl = this.getBaseUrl(), rootUrl = this.getRootUrl(); - - // Trim baseUrl - if ( this.emulated.pushState ) { - // We are in a if statement as when pushState is not emulated - // The actual url these short urls are relative to can change - // So within the same session, we the url may end up somewhere different - shortUrl = shortUrl.replace(baseUrl,''); - } + // Return + return shortUrl; + }, - // Trim rootUrl - shortUrl = shortUrl.replace(rootUrl,'/'); + /** + * getLocationHref(document) + * Returns a normalized version of document.location.href + * accounting for browser inconsistencies, etc. + * + * This URL will be URI-encoded and will include the hash + * + * @param {object} document + * @return {string} url + */ + getLocationHref : function(doc) { + doc = doc || window.document; - // Ensure we can still detect it as a state - if ( this.isTraditionalAnchor(shortUrl) ) { - shortUrl = './'+shortUrl; - } + // most of the time, this will be true + if (doc.URL === doc.location.href) + return doc.location.href; - // Clean It - shortUrl = shortUrl.replace(/^(\.\/)+/g,'./').replace(/\#$/,''); + // some versions of webkit URI-decode document.location.href + // but they leave document.URL in an encoded state + if (doc.location.href === decodeURIComponent(doc.URL)) + return doc.URL; - // Return - return shortUrl; - }; + // FF 3.6 only updates document.URL when a page is reloaded + // document.location.href is updated correctly + if (doc.location.hash && decodeURIComponent(doc.location.href.replace(/^[^#]+/, "")) === doc.location.hash) + return doc.location.href; - /** - * getLocationHref(document) - * Returns a normalized version of document.location.href - * accounting for browser inconsistencies, etc. - * - * This URL will be URI-encoded and will include the hash - * - * @param {object} document - * @return {string} url - */ - getLocationHref = function(doc) { - doc = doc || window.document; + if (doc.URL.indexOf('#') == -1 && doc.location.href.indexOf('#') != -1) + return doc.location.href; + + return doc.URL || doc.location.href; + }, - // most of the time, this will be true - if (doc.URL === doc.location.href) - return doc.location.href; - // some versions of webkit URI-decode document.location.href - // but they leave document.URL in an encoded state - if (doc.location.href === decodeURIComponent(doc.URL)) - return doc.URL; + + /** + * noramlizeStore() + * Noramlize the store by adding necessary values + */ + normalizeStore : function() + { + + this.store.idToState = this.store.idToState||{}; + this.store.urlToId = this.store.urlToId||{}; + this.store.stateToId = this.store.stateToId||{}; + }, - // FF 3.6 only updates document.URL when a page is reloaded - // document.location.href is updated correctly - if (doc.location.hash && decodeURIComponent(doc.location.href.replace(/^[^#]+/, "")) === doc.location.hash) - return doc.location.href; + /** + * getState() + * Get an object containing the data, title and url of the current state + * @param {Boolean} friendly + * @param {Boolean} create + * @return {Object} State + */ + getState : function(friendly,create){ + // Prepare + if ( typeof friendly === 'undefined' ) { friendly = true; } + if ( typeof create === 'undefined' ) { create = true; } - if (doc.URL.indexOf('#') == -1 && doc.location.href.indexOf('#') != -1) - return doc.location.href; - - return doc.URL || doc.location.href; - }; + // Fetch + var State = this.getLastSavedState(); + // Create + if ( !State && create ) { + State = this.createStateObject(); + } - - /** - * History.noramlizeStore() - * Noramlize the store by adding necessary values - */ - History.normalizeStore = function(){ - History.store.idToState = History.store.idToState||{}; - History.store.urlToId = History.store.urlToId||{}; - History.store.stateToId = History.store.stateToId||{}; - }; + // Adjust + if ( friendly ) { + State = this.cloneObject(State); + State.url = this.cleanUrl||State.url; + } - /** - * History.getState() - * Get an object containing the data, title and url of the current state - * @param {Boolean} friendly - * @param {Boolean} create - * @return {Object} State - */ - History.getState = function(friendly,create){ - // Prepare - if ( typeof friendly === 'undefined' ) { friendly = true; } - if ( typeof create === 'undefined' ) { create = true; } + // Return + return State; + }, - // Fetch - var State = History.getLastSavedState(); + /** + * getIdByState(State) + * Gets a ID for a State + * @param {State} newState + * @return {String} id + */ + getIdByState : function(newState){ - // Create - if ( !State && create ) { - State = History.createStateObject(); - } + // Fetch ID + var id = this.extractId(newState.url), + str; - // Adjust - if ( friendly ) { - State = History.cloneObject(State); - State.url = State.cleanUrl||State.url; - } + if ( !id ) { + // Find ID via State String + str = this.getStateString(newState); + if ( typeof this.stateToId[str] !== 'undefined' ) { + id = this.stateToId[str]; + } + else if ( typeof this.store.stateToId[str] !== 'undefined' ) { + id = this.store.stateToId[str]; + } + else { + // Generate a new ID + while ( true ) { + id = (new Date()).getTime() + String(Math.random()).replace(/\D/g,''); + if ( typeof this.idToState[id] === 'undefined' && typeof this.store.idToState[id] === 'undefined' ) { + break; + } + } + + // Apply the new State to the ID + this.stateToId[str] = id; + this.idToState[id] = newState; + } + } - // Return - return State; - }; + // Return ID + return id; + }, - /** - * History.getIdByState(State) - * Gets a ID for a State - * @param {State} newState - * @return {String} id - */ - History.getIdByState = function(newState){ + /** + * normalizeState(State) + * Expands a State Object + * @param {object} State + * @return {object} + */ + normalizeState : function(oldState){ + // Variables + var newState, dataNotEmpty; - // Fetch ID - var id = History.extractId(newState.url), - str; + // Prepare + if ( !oldState || (typeof oldState !== 'object') ) { + oldState = {}; + } - if ( !id ) { - // Find ID via State String - str = History.getStateString(newState); - if ( typeof History.stateToId[str] !== 'undefined' ) { - id = History.stateToId[str]; - } - else if ( typeof History.store.stateToId[str] !== 'undefined' ) { - id = History.store.stateToId[str]; - } - else { - // Generate a new ID - while ( true ) { - id = (new Date()).getTime() + String(Math.random()).replace(/\D/g,''); - if ( typeof History.idToState[id] === 'undefined' && typeof History.store.idToState[id] === 'undefined' ) { - break; - } - } - - // Apply the new State to the ID - History.stateToId[str] = id; - History.idToState[id] = newState; - } - } + // Check + if ( typeof oldState.normalized !== 'undefined' ) { + return oldState; + } - // Return ID - return id; - }; + // Adjust + if ( !oldState.data || (typeof oldState.data !== 'object') ) { + oldState.data = {}; + } - /** - * History.normalizeState(State) - * Expands a State Object - * @param {object} State - * @return {object} - */ - History.normalizeState = function(oldState){ - // Variables - var newState, dataNotEmpty; + // ---------------------------------------------------------------- - // Prepare - if ( !oldState || (typeof oldState !== 'object') ) { - oldState = {}; - } + // Create + newState = {}; + newState.normalized = true; + newState.title = oldState.title||''; + newState.url = this.getFullUrl(oldState.url?oldState.url:(this.getLocationHref())); + newState.hash = this.getShortUrl(newState.url); + newState.data = this.cloneObject(oldState.data); - // Check - if ( typeof oldState.normalized !== 'undefined' ) { - return oldState; - } + // Fetch ID + newState.id = this.getIdByState(newState); - // Adjust - if ( !oldState.data || (typeof oldState.data !== 'object') ) { - oldState.data = {}; - } + // ---------------------------------------------------------------- - // ---------------------------------------------------------------- + // Clean the URL + newState.cleanUrl = newState.url.replace(/\??\&_suid.*/,''); + newState.url = newState.cleanUrl; - // Create - newState = {}; - newState.normalized = true; - newState.title = oldState.title||''; - newState.url = History.getFullUrl(oldState.url?oldState.url:(History.getLocationHref())); - newState.hash = History.getShortUrl(newState.url); - newState.data = History.cloneObject(oldState.data); + // Check to see if we have more than just a url + dataNotEmpty = !this.isEmptyObject(newState.data); - // Fetch ID - newState.id = History.getIdByState(newState); + // Apply + if ( (newState.title || dataNotEmpty) && this.disableSuid !== true ) { + // Add ID to Hash + newState.hash = this.getShortUrl(newState.url).replace(/\??\&_suid.*/,''); + if ( !/\?/.test(newState.hash) ) { + newState.hash += '?'; + } + newState.hash += '&_suid='+newState.id; + } - // ---------------------------------------------------------------- + // Create the Hashed URL + newState.hashedUrl = this.getFullUrl(newState.hash); - // Clean the URL - newState.cleanUrl = newState.url.replace(/\??\&_suid.*/,''); - newState.url = newState.cleanUrl; + // ---------------------------------------------------------------- - // Check to see if we have more than just a url - dataNotEmpty = !History.isEmptyObject(newState.data); + // Update the URL if we have a duplicate + if ( (this.emulated.pushState || this.bugs.safariPoll) && this.hasUrlDuplicate(newState) ) { + newState.url = newState.hashedUrl; + } - // Apply - if ( (newState.title || dataNotEmpty) && History.options.disableSuid !== true ) { - // Add ID to Hash - newState.hash = History.getShortUrl(newState.url).replace(/\??\&_suid.*/,''); - if ( !/\?/.test(newState.hash) ) { - newState.hash += '?'; - } - newState.hash += '&_suid='+newState.id; - } + // ---------------------------------------------------------------- - // Create the Hashed URL - newState.hashedUrl = History.getFullUrl(newState.hash); + // Return + return newState; + }, - // ---------------------------------------------------------------- + /** + * createStateObject(data,title,url) + * Creates a object based on the data, title and url state params + * @param {object} data + * @param {string} title + * @param {string} url + * @return {object} + */ + createStateObject : function(data,title,url){ + // Hashify + var State = { + 'data': data, + 'title': title, + 'url': url + }; + + // Expand the State + State = this.normalizeState(State); + + // Return object + return State; + }, - // Update the URL if we have a duplicate - if ( (History.emulated.pushState || History.bugs.safariPoll) && History.hasUrlDuplicate(newState) ) { - newState.url = newState.hashedUrl; - } + /** + * getStateById(id) + * Get a state by it's UID + * @param {String} id + */ + getStateById : function(id){ + // Prepare + id = String(id); - // ---------------------------------------------------------------- + // Retrieve + var State = this.idToState[id] || this.store.idToState[id] || undefined; - // Return - return newState; - }; + // Return State + return State; + }, - /** - * History.createStateObject(data,title,url) - * Creates a object based on the data, title and url state params - * @param {object} data - * @param {string} title - * @param {string} url - * @return {object} - */ - History.createStateObject = function(data,title,url){ - // Hashify - var State = { - 'data': data, - 'title': title, - 'url': url - }; + /** + * Get a State's String + * @param {State} passedState + */ + getStateString : function(passedState){ + // Prepare + var State, cleanedState, str; - // Expand the State - State = History.normalizeState(State); + // Fetch + State = this.normalizeState(passedState); - // Return object - return State; - }; + // Clean + cleanedState = { + data: State.data, + title: passedState.title, + url: passedState.url + }; - /** - * History.getStateById(id) - * Get a state by it's UID - * @param {String} id - */ - History.getStateById = function(id){ - // Prepare - id = String(id); + // Fetch + str = JSON.stringify(cleanedState); - // Retrieve - var State = History.idToState[id] || History.store.idToState[id] || undefined; + // Return + return str; + }, - // Return State - return State; - }; + /** + * Get a State's ID + * @param {State} passedState + * @return {String} id + */ + getStateId : function(passedState){ + // Prepare + var State, id; - /** - * Get a State's String - * @param {State} passedState - */ - History.getStateString = function(passedState){ - // Prepare - var State, cleanedState, str; + // Fetch + State = this.normalizeState(passedState); - // Fetch - State = History.normalizeState(passedState); + // Fetch + id = State.id; - // Clean - cleanedState = { - data: State.data, - title: passedState.title, - url: passedState.url - }; + // Return + return id; + }, - // Fetch - str = JSON.stringify(cleanedState); + /** + * getHashByState(State) + * Creates a Hash for the State Object + * @param {State} passedState + * @return {String} hash + */ + getHashByState : function(passedState){ + // Prepare + var State, hash; - // Return - return str; - }; + // Fetch + State = this.normalizeState(passedState); - /** - * Get a State's ID - * @param {State} passedState - * @return {String} id - */ - History.getStateId = function(passedState){ - // Prepare - var State, id; - - // Fetch - State = History.normalizeState(passedState); - - // Fetch - id = State.id; - - // Return - return id; - }; - - /** - * History.getHashByState(State) - * Creates a Hash for the State Object - * @param {State} passedState - * @return {String} hash - */ - History.getHashByState = function(passedState){ - // Prepare - var State, hash; - - // Fetch - State = History.normalizeState(passedState); - - // Hash - hash = State.hash; - - // Return - return hash; - }; - - /** - * History.extractId(url_or_hash) - * Get a State ID by it's URL or Hash - * @param {string} url_or_hash - * @return {string} id - */ - History.extractId = function ( url_or_hash ) { - // Prepare - var id,parts,url, tmp; - - // Extract - - // If the URL has a #, use the id from before the # - if (url_or_hash.indexOf('#') != -1) - { - tmp = url_or_hash.split("#")[0]; - } - else - { - tmp = url_or_hash; - } - - parts = /(.*)\&_suid=([0-9]+)$/.exec(tmp); - url = parts ? (parts[1]||url_or_hash) : url_or_hash; - id = parts ? String(parts[2]||'') : ''; - - // Return - return id||false; - }; - - /** - * History.isTraditionalAnchor - * Checks to see if the url is a traditional anchor or not - * @param {String} url_or_hash - * @return {Boolean} - */ - History.isTraditionalAnchor = function(url_or_hash){ - // Check - var isTraditional = !(/[\/\?\.]/.test(url_or_hash)); - - // Return - return isTraditional; - }; + // Hash + hash = State.hash; - /** - * History.extractState - * Get a State by it's URL or Hash - * @param {String} url_or_hash - * @return {State|null} - */ - History.extractState = function(url_or_hash,create){ - // Prepare - var State = null, id, url; - create = create||false; - - // Fetch SUID - id = History.extractId(url_or_hash); - if ( id ) { - State = History.getStateById(id); - } - - // Fetch SUID returned no State - if ( !State ) { - // Fetch URL - url = History.getFullUrl(url_or_hash); - - // Check URL - id = History.getIdByUrl(url)||false; - if ( id ) { - State = History.getStateById(id); - } + // Return + return hash; + }, - // Create State - if ( !State && create && !History.isTraditionalAnchor(url_or_hash) ) { - State = History.createStateObject(null,null,url); - } - } + /** + * extractId(url_or_hash) + * Get a State ID by it's URL or Hash + * @param {string} url_or_hash + * @return {string} id + */ + extractId : function ( url_or_hash ) { + // Prepare + var id,parts,url, tmp; - // Return - return State; - }; + // Extract + + // If the URL has a #, use the id from before the # + if (url_or_hash.indexOf('#') != -1) + { + tmp = url_or_hash.split("#")[0]; + } + else + { + tmp = url_or_hash; + } + + parts = /(.*)\&_suid=([0-9]+)$/.exec(tmp); + url = parts ? (parts[1]||url_or_hash) : url_or_hash; + id = parts ? String(parts[2]||'') : ''; - /** - * History.getIdByUrl() - * Get a State ID by a State URL - */ - History.getIdByUrl = function(url){ - // Fetch - var id = History.urlToId[url] || History.store.urlToId[url] || undefined; + // Return + return id||false; + }, - // Return - return id; - }; + /** + * isTraditionalAnchor + * Checks to see if the url is a traditional anchor or not + * @param {String} url_or_hash + * @return {Boolean} + */ + isTraditionalAnchor : function(url_or_hash){ + // Check + var isTraditional = !(/[\/\?\.]/.test(url_or_hash)); - /** - * History.getLastSavedState() - * Get an object containing the data, title and url of the current state - * @return {Object} State - */ - History.getLastSavedState = function(){ - return History.savedStates[History.savedStates.length-1]||undefined; - }; + // Return + return isTraditional; + }, - /** - * History.getLastStoredState() - * Get an object containing the data, title and url of the current state - * @return {Object} State - */ - History.getLastStoredState = function(){ - return History.storedStates[History.storedStates.length-1]||undefined; - }; + /** + * extractState + * Get a State by it's URL or Hash + * @param {String} url_or_hash + * @return {State|null} + */ + extractState : function(url_or_hash,create){ + // Prepare + var State = null, id, url; + create = create||false; + + // Fetch SUID + id = this.extractId(url_or_hash); + if ( id ) { + State = this.getStateById(id); + } - /** - * History.hasUrlDuplicate - * Checks if a Url will have a url conflict - * @param {Object} newState - * @return {Boolean} hasDuplicate - */ - History.hasUrlDuplicate = function(newState) { - // Prepare - var hasDuplicate = false, - oldState; + // Fetch SUID returned no State + if ( !State ) { + // Fetch URL + url = this.getFullUrl(url_or_hash); - // Fetch - oldState = History.extractState(newState.url); + // Check URL + id = this.getIdByUrl(url)||false; + if ( id ) { + State = this.getStateById(id); + } - // Check - hasDuplicate = oldState && oldState.id !== newState.id; + // Create State + if ( !State && create && !this.isTraditionalAnchor(url_or_hash) ) { + State = this.createStateObject(null,null,url); + } + } - // Return - return hasDuplicate; - }; + // Return + return State; + }, - /** - * History.storeState - * Store a State - * @param {Object} newState - * @return {Object} newState - */ - History.storeState = function(newState){ - // Store the State - History.urlToId[newState.url] = newState.id; + /** + * getIdByUrl() + * Get a State ID by a State URL + */ + getIdByUrl : function(url){ + // Fetch + var id = this.urlToId[url] || this.store.urlToId[url] || undefined; - // Push the State - History.storedStates.push(History.cloneObject(newState)); + // Return + return id; + }, - // Return newState - return newState; - }; + /** + * getLastSavedState() + * Get an object containing the data, title and url of the current state + * @return {Object} State + */ + getLastSavedState : function(){ + return this.savedStates[this.savedStates.length-1]||undefined; + }, - /** - * History.isLastSavedState(newState) - * Tests to see if the state is the last state - * @param {Object} newState - * @return {boolean} isLast - */ - History.isLastSavedState = function(newState){ - // Prepare - var isLast = false, - newId, oldState, oldId; + /** + * getLastStoredState() + * Get an object containing the data, title and url of the current state + * @return {Object} State + */ + getLastStoredState : function(){ + return this.storedStates[this.storedStates.length-1]||undefined; + }, - // Check - if ( History.savedStates.length ) { - newId = newState.id; - oldState = History.getLastSavedState(); - oldId = oldState.id; + /** + * hasUrlDuplicate + * Checks if a Url will have a url conflict + * @param {Object} newState + * @return {Boolean} hasDuplicate + */ + hasUrlDuplicate : function(newState) { + // Prepare + var hasDuplicate = false, + oldState; - // Check - isLast = (newId === oldId); - } + // Fetch + oldState = this.extractState(newState.url); - // Return - return isLast; - }; + // Check + hasDuplicate = oldState && oldState.id !== newState.id; - /** - * History.saveState - * Push a State - * @param {Object} newState - * @return {boolean} changed - */ - History.saveState = function(newState){ - // Check Hash - if ( History.isLastSavedState(newState) ) { - return false; - } + // Return + return hasDuplicate; + }, - // Push the State - History.savedStates.push(History.cloneObject(newState)); + /** + * storeState + * Store a State + * @param {Object} newState + * @return {Object} newState + */ + storeState : function(newState){ + // Store the State + this.urlToId[newState.url] = newState.id; - // Return true - return true; - }; + // Push the State + this.storedStates.push(this.cloneObject(newState)); - /** - * History.getStateByIndex() - * Gets a state by the index - * @param {integer} index - * @return {Object} - */ - History.getStateByIndex = function(index){ - // Prepare - var State = null; + // Return newState + return newState; + }, - // Handle - if ( typeof index === 'undefined' ) { - // Get the last inserted - State = History.savedStates[History.savedStates.length-1]; - } - else if ( index < 0 ) { - // Get from the end - State = History.savedStates[History.savedStates.length+index]; - } - else { - // Get from the beginning - State = History.savedStates[index]; - } + /** + * isLastSavedState(newState) + * Tests to see if the state is the last state + * @param {Object} newState + * @return {boolean} isLast + */ + isLastSavedState : function(newState){ + // Prepare + var isLast = false, + newId, oldState, oldId; + + // Check + if ( this.savedStates.length ) { + newId = newState.id; + oldState = this.getLastSavedState(); + oldId = oldState.id; + + // Check + isLast = (newId === oldId); + } - // Return State - return State; - }; - - /** - * History.getCurrentIndex() - * Gets the current index - * @return (integer) - */ - History.getCurrentIndex = function(){ - // Prepare - var index = null; - - // No states saved - if(History.savedStates.length < 1) { - index = 0; - } - else { - index = History.savedStates.length-1; - } - return index; - }; + // Return + return isLast; + }, - // ==================================================================== - // Hash Helpers + /** + * saveState + * Push a State + * @param {Object} newState + * @return {boolean} changed + */ + saveState : function(newState){ + // Check Hash + if ( this.isLastSavedState(newState) ) { + return false; + } - /** - * History.getHash() - * @param {Location=} location - * Gets the current document hash - * Note: unlike location.hash, this is guaranteed to return the escaped hash in all browsers - * @return {string} - */ - History.getHash = function(doc){ - var url = History.getLocationHref(doc), - hash; - hash = History.getHashByUrl(url); - return hash; - }; + // Push the State + this.savedStates.push(this.cloneObject(newState)); - /** - * History.unescapeHash() - * normalize and Unescape a Hash - * @param {String} hash - * @return {string} - */ - History.unescapeHash = function(hash){ - // Prepare - var result = History.normalizeHash(hash); + // Return true + return true; + }, - // Unescape hash - result = decodeURIComponent(result); + /** + * getStateByIndex() + * Gets a state by the index + * @param {integer} index + * @return {Object} + */ + getStateByIndex : function(index){ + // Prepare + var State = null; + + // Handle + if ( typeof index === 'undefined' ) { + // Get the last inserted + State = this.savedStates[this.savedStates.length-1]; + } + else if ( index < 0 ) { + // Get from the end + State = this.savedStates[this.savedStates.length+index]; + } + else { + // Get from the beginning + State = this.savedStates[index]; + } - // Return result - return result; - }; + // Return State + return State; + }, + + /** + * getCurrentIndex() + * Gets the current index + * @return (integer) + */ + getCurrentIndex : function(){ + // Prepare + var index = null; + + // No states saved + if(this.savedStates.length < 1) { + index = 0; + } + else { + index = this.savedStates.length-1; + } + return index; + }, - /** - * History.normalizeHash() - * normalize a hash across browsers - * @return {string} - */ - History.normalizeHash = function(hash){ - // Prepare - var result = hash.replace(/[^#]*#/,'').replace(/#.*/, ''); + // ==================================================================== + // Hash Helpers - // Return result - return result; - }; + /** + * getHash() + * @param {Location=} location + * Gets the current document hash + * Note: unlike location.hash, this is guaranteed to return the escaped hash in all browsers + * @return {string} + */ + getHash : function(doc){ + var url = this.getLocationHref(doc), + hash; + hash = this.getHashByUrl(url); + return hash; + }, - /** - * History.setHash(hash) - * Sets the document hash - * @param {string} hash - * @return {History} - */ - History.setHash = function(hash,queue){ - // Prepare - var State, pageUrl; - - // Handle Queueing - if ( queue !== false && History.busy() ) { - // Wait + Push to Queue - //History.debug('History.setHash: we must wait', arguments); - History.pushQueue({ - scope: History, - callback: History.setHash, - args: arguments, - queue: queue - }); - return false; - } + /** + * unescapeHash() + * normalize and Unescape a Hash + * @param {String} hash + * @return {string} + */ + unescapeHash : function(hash){ + // Prepare + var result = this.normalizeHash(hash); - // Log - //History.debug('History.setHash: called',hash); + // Unescape hash + result = decodeURIComponent(result); - // Make Busy + Continue - History.busy(true); + // Return result + return result; + }, - // Check if hash is a state - State = History.extractState(hash,true); - if ( State && !History.emulated.pushState ) { - // Hash is a state so skip the setHash - //History.debug('History.setHash: Hash is a state so skipping the hash set with a direct pushState call',arguments); + /** + * normalizeHash() + * normalize a hash across browsers + * @return {string} + */ + normalizeHash : function(hash){ + // Prepare + var result = hash.replace(/[^#]*#/,'').replace(/#.*/, ''); - // PushState - History.pushState(State.data,State.title,State.url,false); - } - else if ( History.getHash() !== hash ) { - // Hash is a proper hash, so apply it + // Return result + return result; + }, - // Handle browser bugs - if ( History.bugs.setHash ) { - // Fix Safari Bug https://bugs.webkit.org/show_bug.cgi?id=56249 + /** + * setHash(hash) + * Sets the document hash + * @param {string} hash + * @return {Roo.History} + */ + setHash : function(hash,queue){ + // Prepare + var State, pageUrl; + + // Handle Queueing + if ( queue !== false && this.busy() ) { + // Wait + Push to Queue + //this.debug('this.setHash: we must wait', arguments); + this.pushQueue({ + scope: this, + callback: this.setHash, + args: arguments, + queue: queue + }); + return false; + } - // Fetch the base page - pageUrl = History.getPageUrl(); + // Log + //this.debug('this.setHash: called',hash); - // Safari hash apply - History.pushState(null,null,pageUrl+'#'+hash,false); - } - else { - // Normal hash apply - window.document.location.hash = hash; - } - } + // Make Busy + Continue + this.busy(true); - // Chain - return History; - }; + // Check if hash is a state + State = this.extractState(hash,true); + if ( State && !this.emulated.pushState ) { + // Hash is a state so skip the setHash + //this.debug('this.setHash: Hash is a state so skipping the hash set with a direct pushState call',arguments); - /** - * History.escape() - * normalize and Escape a Hash - * @return {string} - */ - History.escapeHash = function(hash){ - // Prepare - var result = History.normalizeHash(hash); - - // Escape hash - result = window.encodeURIComponent(result); - - // IE6 Escape Bug - if ( !History.bugs.hashEscape ) { - // Restore common parts - result = result - .replace(/\%21/g,'!') - .replace(/\%26/g,'&') - .replace(/\%3D/g,'=') - .replace(/\%3F/g,'?'); - } + // PushState + this.pushState(State.data,State.title,State.url,false); + } + else if ( this.getHash() !== hash ) { + // Hash is a proper hash, so apply it - // Return result - return result; - }; + // Handle browser bugs + if ( this.bugs.setHash ) { + // Fix Safari Bug https://bugs.webkit.org/show_bug.cgi?id=56249 - /** - * History.getHashByUrl(url) - * Extracts the Hash from a URL - * @param {string} url - * @return {string} url - */ - History.getHashByUrl = function(url){ - // Extract the hash - var hash = String(url) - .replace(/([^#]*)#?([^#]*)#?(.*)/, '$2') - ; + // Fetch the base page + pageUrl = this.getPageUrl(); - // Unescape hash - hash = History.unescapeHash(hash); + // Safari hash apply + this.pushState(null,null,pageUrl+'#'+hash,false); + } + else { + // Normal hash apply + window.document.location.hash = hash; + } + } - // Return hash - return hash; - }; + // Chain + return this; + }, - /** - * History.setTitle(title) - * Applies the title to the document - * @param {State} newState - * @return {Boolean} - */ - History.setTitle = function(newState){ - // Prepare - var title = newState.title, - firstState; - - // Initial - if ( !title ) { - firstState = History.getStateByIndex(0); - if ( firstState && firstState.url === newState.url ) { - title = firstState.title||History.options.initialTitle; - } - } + /** + * escape() + * normalize and Escape a Hash + * @return {string} + */ + escapeHash : function(hash){ + // Prepare + var result = normalizeHash(hash); + + // Escape hash + result = window.encodeURIComponent(result); + + // IE6 Escape Bug + if ( !this.bugs.hashEscape ) { + // Restore common parts + result = result + .replace(/\%21/g,'!') + .replace(/\%26/g,'&') + .replace(/\%3D/g,'=') + .replace(/\%3F/g,'?'); + } - // Apply - try { - window.document.getElementsByTagName('title')[0].innerHTML = title.replace('<','<').replace('>','>').replace(' & ',' & '); - } - catch ( Exception ) { } - window.document.title = title; + // Return result + return result; + }, - // Chain - return History; - }; + /** + * getHashByUrl(url) + * Extracts the Hash from a URL + * @param {string} url + * @return {string} url + */ + getHashByUrl : function(url){ + // Extract the hash + var hash = String(url) + .replace(/([^#]*)#?([^#]*)#?(.*)/, '$2') + ; + // Unescape hash + hash = this.unescapeHash(hash); - // ==================================================================== - // Queueing + // Return hash + return hash; + }, - /** - * History.queues - * The list of queues to use - * First In, First Out - */ - History.queues = []; + /** + * setTitle(title) + * Applies the title to the document + * @param {State} newState + * @return {Boolean} + */ + setTitle : function(newState){ + // Prepare + var title = newState.title, + firstState; + + // Initial + if ( !title ) { + firstState = this.getStateByIndex(0); + if ( firstState && firstState.url === newState.url ) { + title = firstState.title||this.initialTitle; + } + } - /** - * History.busy(value) - * @param {boolean} value [optional] - * @return {boolean} busy - */ - History.busy = function(value){ - // Apply - if ( typeof value !== 'undefined' ) { - //History.debug('History.busy: changing ['+(History.busy.flag||false)+'] to ['+(value||false)+']', History.queues.length); - History.busy.flag = value; - } - // Default - else if ( typeof History.busy.flag === 'undefined' ) { - History.busy.flag = false; - } + // Apply + try { + window.document.getElementsByTagName('title')[0].innerHTML = title.replace('<','<').replace('>','>').replace(' & ',' & '); + } + catch ( Exception ) { } + window.document.title = title; - // Queue - if ( !History.busy.flag ) { - // Execute the next item in the queue - window.clearTimeout(History.busy.timeout); - var fireNext = function(){ - var i, queue, item; - if ( History.busy.flag ) return; - for ( i=History.queues.length-1; i >= 0; --i ) { - queue = History.queues[i]; - if ( queue.length === 0 ) continue; - item = queue.shift(); - History.fireQueueItem(item); - History.busy.timeout = window.setTimeout(fireNext,History.options.busyDelay); - } - }; - History.busy.timeout = window.setTimeout(fireNext,History.options.busyDelay); - } + // Chain + return this; + }, - // Return - return History.busy.flag; - }; - /** - * History.busy.flag - */ - History.busy.flag = false; + // ==================================================================== + // Queueing - /** - * History.fireQueueItem(item) - * Fire a Queue Item - * @param {Object} item - * @return {Mixed} result - */ - History.fireQueueItem = function(item){ - return item.callback.apply(item.scope||History,item.args||[]); - }; - /** - * History.pushQueue(callback,args) - * Add an item to the queue - * @param {Object} item [scope,callback,args,queue] - */ - History.pushQueue = function(item){ - // Prepare the queue - History.queues[item.queue||0] = History.queues[item.queue||0]||[]; + /** + * busy(value) + * @param {boolean} value [optional] + * @return {boolean} busy + */ + busy : function(value){ + // Apply + + var _this = this; + if ( typeof value !== 'undefined' ) { + //this.debug('this.busy: changing ['+(this.busy.flag||false)+'] to ['+(value||false)+']', this.queues.length); + this.busy_flag = value; + } + // Default + else if ( typeof this.busy_flag === 'undefined' ) { + this.busy_flag = false; + } - // Add to the queue - History.queues[item.queue||0].push(item); + // Queue + if ( !this.busy_flag ) { + + + + // Execute the next item in the queue + window.clearTimeout(this.busy.timeout); + var fireNext = function(){ + var i, queue, item; + if ( _this.busy_flag ) return; + for ( i=_this.queues.length-1; i >= 0; --i ) { + queue = _this.queues[i]; + if ( queue.length === 0 ) continue; + item = queue.shift(); + _this.fireQueueItem(item); + _this.busy.timeout = window.setTimeout(fireNext,_this.busyDelay); + } + }; + this.busy.timeout = window.setTimeout(fireNext,this.busyDelay); + } - // Chain - return History; - }; + // Return + return this.busy_flag; + }, - /** - * History.queue (item,queue), (func,queue), (func), (item) - * Either firs the item now if not busy, or adds it to the queue - */ - History.queue = function(item,queue){ - // Prepare - if ( typeof item === 'function' ) { - item = { - callback: item - }; - } - if ( typeof queue !== 'undefined' ) { - item.queue = queue; - } + - // Handle - if ( History.busy() ) { - History.pushQueue(item); - } else { - History.fireQueueItem(item); - } + /** + * fireQueueItem(item) + * Fire a Queue Item + * @param {Object} item + * @return {Mixed} result + */ + fireQueueItem : function(item){ + return item.callback.apply(item.scope||this,item.args||[]); + }, - // Chain - return History; - }; + /** + * pushQueue(callback,args) + * Add an item to the queue + * @param {Object} item [scope,callback,args,queue] + */ + pushQueue : function(item){ + // Prepare the queue + this.queues[item.queue||0] = this.queues[item.queue||0]||[]; - /** - * History.clearQueue() - * Clears the Queue - */ - History.clearQueue = function(){ - History.busy.flag = false; - History.queues = []; - return History; - }; + // Add to the queue + this.queues[item.queue||0].push(item); + // Chain + return this; + }, - // ==================================================================== - // IE Bug Fix + /** + * queue (item,queue), (func,queue), (func), (item) + * Either firs the item now if not busy, or adds it to the queue + */ + queue : function(item,queue){ + // Prepare + if ( typeof item === 'function' ) { + item = { + callback: item + }; + } + if ( typeof queue !== 'undefined' ) { + item.queue = queue; + } - /** - * History.stateChanged - * States whether or not the state has changed since the last double check was initialised - */ - History.stateChanged = false; + // Handle + if ( this.busy() ) { + this.pushQueue(item); + } else { + this.fireQueueItem(item); + } - /** - * History.doubleChecker - * Contains the timeout used for the double checks - */ - History.doubleChecker = false; + // Chain + return this; + }, - /** - * History.doubleCheckComplete() - * Complete a double check - * @return {History} - */ - History.doubleCheckComplete = function(){ - // Update - History.stateChanged = true; + /** + * clearQueue() + * Clears the Queue + */ + clearQueue : function(){ + this.busy_flag = false; + this.queues = []; + return this; + }, - // Clear - History.doubleCheckClear(); - // Chain - return History; - }; - /** - * History.doubleCheckClear() - * Clear a double check - * @return {History} - */ - History.doubleCheckClear = function(){ - // Clear - if ( History.doubleChecker ) { - window.clearTimeout(History.doubleChecker); - History.doubleChecker = false; - } + /** + * doubleCheckComplete() + * Complete a double check + * @return {Roo.History} + */ + doubleCheckComplete : function(){ + // Update + this.stateChanged = true; - // Chain - return History; - }; + // Clear + this.doubleCheckClear(); - /** - * History.doubleCheck() - * Create a double check - * @return {History} - */ - History.doubleCheck = function(tryAgain){ - // Reset - History.stateChanged = false; - History.doubleCheckClear(); - - // Fix IE6,IE7 bug where calling history.back or history.forward does not actually change the hash (whereas doing it manually does) - // Fix Safari 5 bug where sometimes the state does not change: https://bugs.webkit.org/show_bug.cgi?id=42940 - if ( History.bugs.ieDoubleCheck ) { - // Apply Check - History.doubleChecker = window.setTimeout( - function(){ - History.doubleCheckClear(); - if ( !History.stateChanged ) { - //History.debug('History.doubleCheck: State has not yet changed, trying again', arguments); - // Re-Attempt - tryAgain(); - } - return true; - }, - History.options.doubleCheckInterval - ); - } + // Chain + return this;; + }, - // Chain - return History; - }; + /** + * doubleCheckClear() + * Clear a double check + * @return {Roo.History} + */ + doubleCheckClear : function(){ + // Clear + if ( this.doubleChecker ) { + window.clearTimeout(this.doubleChecker); + this.doubleChecker = false; + } + // Chain + return this; + }, - // ==================================================================== - // Safari Bug Fix + /** + * doubleCheck() + * Create a double check + * @return {Roo.History} + */ + doubleCheck : function(tryAgain) + { + var _this = this; + // Reset + this.stateChanged = false; + this.doubleCheckClear(); + + // Fix IE6,IE7 bug where calling history.back or history.forward does not actually change the hash (whereas doing it manually does) + // Fix Safari 5 bug where sometimes the state does not change: https://bugs.webkit.org/show_bug.cgi?id=42940 + if ( this.bugs.ieDoubleCheck ) { + // Apply Check + this.doubleChecker = window.setTimeout( + function(){ + _this.doubleCheckClear(); + if ( !_this.stateChanged ) { + //this.debug('History.doubleCheck: State has not yet changed, trying again', arguments); + // Re-Attempt + tryAgain(); + } + return true; + }, + this.doubleCheckInterval + ); + } - /** - * History.safariStatePoll() - * Poll the current state - * @return {History} - */ - History.safariStatePoll = function(){ - // Poll the URL + // Chain + return this; + }, - // Get the Last State which has the new URL - var - urlState = History.extractState(History.getLocationHref()), - newState; - // Check for a difference - if ( !History.isLastSavedState(urlState) ) { - newState = urlState; - } - else { - return; - } + // ==================================================================== + // Safari Bug Fix - // Check if we have a state with that url - // If not create it - if ( !newState ) { - //History.debug('History.safariStatePoll: new'); - newState = History.createStateObject(); - } + /** + * safariStatePoll() + * Poll the current state + * @return {Roo.History} + */ + safariStatePoll : function(){ + // Poll the URL - // Apply the New State - //History.debug('History.safariStatePoll: trigger'); - History.Adapter.trigger(window,'popstate'); + // Get the Last State which has the new URL + var + urlState = this.extractState(this.getLocationHref()), + newState; - // Chain - return History; - }; + // Check for a difference + if ( !this.isLastSavedState(urlState) ) { + newState = urlState; + } + else { + return; + } + // Check if we have a state with that url + // If not create it + if ( !newState ) { + //this.debug('this.safariStatePoll: new'); + newState = this.createStateObject(); + } - // ==================================================================== - // State Aliases + // Apply the New State + //this.debug('this.safariStatePoll: trigger'); + Roo.get(window).fireEvent('popstate'); - /** - * History.back(queue) - * Send the browser history back one item - * @param {Integer} queue [optional] - */ - History.back = function(queue){ - //History.debug('History.back: called', arguments); - - // Handle Queueing - if ( queue !== false && History.busy() ) { - // Wait + Push to Queue - //History.debug('History.back: we must wait', arguments); - History.pushQueue({ - scope: History, - callback: History.back, - args: arguments, - queue: queue - }); - return false; - } + // Chain + return this; + }, - // Make Busy + Continue - History.busy(true); - // Fix certain browser bugs that prevent the state from changing - History.doubleCheck(function(){ - History.back(false); - }); + // ==================================================================== + // State Aliases - // Go back - history.go(-1); + /** + * back(queue) + * Send the browser history back one item + * @param {Integer} queue [optional] + */ + back : function(queue) + { + //this.debug('this.back: called', arguments); + var _this = this; + // Handle Queueing + if ( queue !== false && this.busy() ) { + // Wait + Push to Queue + //this.debug('this.back: we must wait', arguments); + this.pushQueue({ + scope: this, + callback: this.back, + args: arguments, + queue: queue + }); + return false; + } - // End back closure - return true; - }; + // Make Busy + Continue + this.busy(true); - /** - * History.forward(queue) - * Send the browser history forward one item - * @param {Integer} queue [optional] - */ - History.forward = function(queue){ - //History.debug('History.forward: called', arguments); - - // Handle Queueing - if ( queue !== false && History.busy() ) { - // Wait + Push to Queue - //History.debug('History.forward: we must wait', arguments); - History.pushQueue({ - scope: History, - callback: History.forward, - args: arguments, - queue: queue - }); - return false; - } + // Fix certain browser bugs that prevent the state from changing + this.doubleCheck(function(){ + _this.back(false); + }); - // Make Busy + Continue - History.busy(true); + // Go back + history.go(-1); - // Fix certain browser bugs that prevent the state from changing - History.doubleCheck(function(){ - History.forward(false); - }); + // End back closure + return true; + }, - // Go forward - history.go(1); + /** + * forward(queue) + * Send the browser history forward one item + * @param {Integer} queue [optional] + */ + forward : function(queue){ + //this.debug('this.forward: called', arguments); + + // Handle Queueing + if ( queue !== false && this.busy() ) { + // Wait + Push to Queue + //this.debug('this.forward: we must wait', arguments); + this.pushQueue({ + scope: this, + callback: this.forward, + args: arguments, + queue: queue + }); + return false; + } - // End forward closure - return true; - }; + // Make Busy + Continue + this.busy(true); + + var _t = this; + // Fix certain browser bugs that prevent the state from changing + this.doubleCheck(function(){ + _t.forward(false); + }); - /** - * History.go(index,queue) - * Send the browser history back or forward index times - * @param {Integer} queue [optional] - */ - History.go = function(index,queue){ - //History.debug('History.go: called', arguments); + // Go forward + history.go(1); - // Prepare - var i; + // End forward closure + return true; + }, - // Handle - if ( index > 0 ) { - // Forward - for ( i=1; i<=index; ++i ) { - History.forward(queue); - } - } - else if ( index < 0 ) { - // Backward - for ( i=-1; i>=index; --i ) { - History.back(queue); - } - } - else { - throw new Error('History.go: History.go requires a positive or negative integer passed.'); - } + /** + * go(index,queue) + * Send the browser history back or forward index times + * @param {Integer} queue [optional] + */ + go : function(index,queue){ + //this.debug('this.go: called', arguments); - // Chain - return History; - }; + // Prepare + var i; + // Handle + if ( index > 0 ) { + // Forward + for ( i=1; i<=index; ++i ) { + this.forward(queue); + } + } + else if ( index < 0 ) { + // Backward + for ( i=-1; i>=index; --i ) { + this.back(queue); + } + } + else { + throw new Error('History.go: History.go requires a positive or negative integer passed.'); + } - // ==================================================================== - // HTML5 State Support + // Chain + return this; + }, - // Non-Native pushState Implementation - if ( History.emulated.pushState ) { - /* - * Provide Skeleton for HTML4 Browsers - */ - // Prepare - var emptyFunction = function(){}; - History.pushState = History.pushState||emptyFunction; - History.replaceState = History.replaceState||emptyFunction; - } // History.emulated.pushState + // ==================================================================== + // HTML5 State Support - // Native pushState Implementation - else { - /* - * Use native HTML5 History API Implementation - */ + + /* + * Use native HTML5 History API Implementation + */ - /** - * History.onPopState(event,extra) - * Refresh the Current State - */ - History.onPopState = function(event,extra){ - // Prepare - var stateId = false, newState = false, currentHash, currentState; - - // Reset the double check - History.doubleCheckComplete(); - - // Check for a Hash, and handle apporiatly - currentHash = History.getHash(); - if ( currentHash ) { - // Expand Hash - currentState = History.extractState(currentHash||History.getLocationHref(),true); - if ( currentState ) { - // We were able to parse it, it must be a State! - // Let's forward to replaceState - //History.debug('History.onPopState: state anchor', currentHash, currentState); - History.replaceState(currentState.data, currentState.title, currentState.url, false); - } - else { - // Traditional Anchor - //History.debug('History.onPopState: traditional anchor', currentHash); - History.Adapter.trigger(window,'anchorchange'); - History.busy(false); - } - - // We don't care for hashes - History.expectedStateId = false; - return false; - } + /** + * onPopState(event,extra) + * Refresh the Current State + */ + onPopState : function(event,extra){ + // Prepare + var stateId = false, newState = false, currentHash, currentState; + + // Reset the double check + this.doubleCheckComplete(); + + // Check for a Hash, and handle apporiatly + currentHash = this.getHash(); + if ( currentHash ) { + // Expand Hash + currentState = this.extractState(currentHash||this.getLocationHref(),true); + if ( currentState ) { + // We were able to parse it, it must be a State! + // Let's forward to replaceState + //this.debug('this.onPopState: state anchor', currentHash, currentState); + this.replaceState(currentState.data, currentState.title, currentState.url, false); + } + else { + // Traditional Anchor + //this.debug('this.onPopState: traditional anchor', currentHash); + Roo.get(window).fireEvent('anchorchange'); + this.busy(false); + } - // Ensure - stateId = History.Adapter.extractEventData('state',event,extra) || false; + // We don't care for hashes + this.expectedStateId = false; + return false; + } + stateId = (event && event.browserEvent && event.browserEvent['state']) || (extra && extra['state']) || undefined; - // Fetch State - if ( stateId ) { - // Vanilla: Back/forward button was used - newState = History.getStateById(stateId); - } - else if ( History.expectedStateId ) { - // Vanilla: A new state was pushed, and popstate was called manually - newState = History.getStateById(History.expectedStateId); - } - else { - // Initial State - newState = History.extractState(History.getLocationHref()); - } + // Ensure + //stateId = this.Adapter.extractEventData('state',event,extra) || false; - // The State did not exist in our store - if ( !newState ) { - // Regenerate the State - newState = History.createStateObject(null,null,History.getLocationHref()); - } + // Fetch State + if ( stateId ) { + // Vanilla: Back/forward button was used + newState = this.getStateById(stateId); + } + else if ( this.expectedStateId ) { + // Vanilla: A new state was pushed, and popstate was called manually + newState = this.getStateById(this.expectedStateId); + } + else { + // Initial State + newState = this.extractState(this.getLocationHref()); + } - // Clean - History.expectedStateId = false; + // The State did not exist in our store + if ( !newState ) { + // Regenerate the State + newState = this.createStateObject(null,null,this.getLocationHref()); + } - // Check if we are the same state - if ( History.isLastSavedState(newState) ) { - // There has been no change (just the page's hash has finally propagated) - //History.debug('History.onPopState: no change', newState, History.savedStates); - History.busy(false); - return false; - } + // Clean + this.expectedStateId = false; - // Store the State - History.storeState(newState); - History.saveState(newState); + // Check if we are the same state + if ( this.isLastSavedState(newState) ) { + // There has been no change (just the page's hash has finally propagated) + //this.debug('this.onPopState: no change', newState, this.savedStates); + this.busy(false); + return false; + } - // Force update of the title - History.setTitle(newState); + // Store the State + this.storeState(newState); + this.saveState(newState); - // Fire Our Event - History.Adapter.trigger(window,'statechange'); - History.busy(false); + // Force update of the title + this.setTitle(newState); - // Return true - return true; - }; - History.Adapter.bind(window,'popstate',History.onPopState); + // Fire Our Event + Roo.get(window).fireEvent('statechange'); + this.busy(false); - /** - * History.pushState(data,title,url) - * Add a new State to the history object, become it, and trigger onpopstate - * We have to trigger for HTML4 compatibility - * @param {object} data - * @param {string} title - * @param {string} url - * @return {true} - */ - History.pushState = function(data,title,url,queue){ - //History.debug('History.pushState: called', arguments); + // Return true + return true; + }, + + + + + + - // Check the State - if ( History.getHashByUrl(url) && History.emulated.pushState ) { - throw new Error('History.js does not support states with fragement-identifiers (hashes/anchors).'); - } + /** + * pushState(data,title,url) + * Add a new State to the history object, become it, and trigger onpopstate + * We have to trigger for HTML4 compatibility + * @param {object} data + * @param {string} title + * @param {string} url + * @return {true} + */ + pushState : function(data,title,url,queue){ + //this.debug('this.pushState: called', arguments); - // Handle Queueing - if ( queue !== false && History.busy() ) { - // Wait + Push to Queue - //History.debug('History.pushState: we must wait', arguments); - History.pushQueue({ - scope: History, - callback: History.pushState, - args: arguments, - queue: queue - }); - return false; - } + // Check the State + if ( this.getHashByUrl(url) && this.emulated.pushState ) { + throw new Error('History.js does not support states with fragement-identifiers (hashes/anchors).'); + } - // Make Busy + Continue - History.busy(true); + // Handle Queueing + if ( queue !== false && this.busy() ) { + // Wait + Push to Queue + //this.debug('this.pushState: we must wait', arguments); + this.pushQueue({ + scope: this, + callback: this.pushState, + args: arguments, + queue: queue + }); + return false; + } - // Create the newState - var newState = History.createStateObject(data,title,url); + // Make Busy + Continue + this.busy(true); - // Check it - if ( History.isLastSavedState(newState) ) { - // Won't be a change - History.busy(false); - } - else { - // Store the newState - History.storeState(newState); - History.expectedStateId = newState.id; + // Create the newState + var newState = this.createStateObject(data,title,url); - // Push the newState - history.pushState(newState.id,newState.title,newState.url); + // Check it + if ( this.isLastSavedState(newState) ) { + // Won't be a change + this.busy(false); + } + else { + // Store the newState + this.storeState(newState); + this.expectedStateId = newState.id; - // Fire HTML5 Event - History.Adapter.trigger(window,'popstate'); - } + // Push the newState + history.pushState(newState.id,newState.title,newState.url); - // End pushState closure - return true; - }; + // Fire HTML5 Event + Roo.get(window).fireEvent('popstate'); + } - /** - * History.replaceState(data,title,url) - * Replace the State and trigger onpopstate - * We have to trigger for HTML4 compatibility - * @param {object} data - * @param {string} title - * @param {string} url - * @return {true} - */ - History.replaceState = function(data,title,url,queue){ - //History.debug('History.replaceState: called', arguments); + // End pushState closure + return true; + }, - // Check the State - if ( History.getHashByUrl(url) && History.emulated.pushState ) { - throw new Error('History.js does not support states with fragement-identifiers (hashes/anchors).'); - } + /** + * replaceState(data,title,url) + * Replace the State and trigger onpopstate + * We have to trigger for HTML4 compatibility + * @param {object} data + * @param {string} title + * @param {string} url + * @return {true} + */ + replaceState : function(data,title,url,queue){ + //this.debug('this.replaceState: called', arguments); - // Handle Queueing - if ( queue !== false && History.busy() ) { - // Wait + Push to Queue - //History.debug('History.replaceState: we must wait', arguments); - History.pushQueue({ - scope: History, - callback: History.replaceState, - args: arguments, - queue: queue - }); - return false; - } + // Check the State + if ( this.getHashByUrl(url) && this.emulated.pushState ) { + throw new Error('this.js does not support states with fragement-identifiers (hashes/anchors).'); + } - // Make Busy + Continue - History.busy(true); + // Handle Queueing + if ( queue !== false && this.busy() ) { + // Wait + Push to Queue + //this.debug('this.replaceState: we must wait', arguments); + this.pushQueue({ + scope: this, + callback: this.replaceState, + args: arguments, + queue: queue + }); + return false; + } - // Create the newState - var newState = History.createStateObject(data,title,url); + // Make Busy + Continue + this.busy(true); - // Check it - if ( History.isLastSavedState(newState) ) { - // Won't be a change - History.busy(false); - } - else { - // Store the newState - History.storeState(newState); - History.expectedStateId = newState.id; + // Create the newState + var newState = this.createStateObject(data,title,url); - // Push the newState - history.replaceState(newState.id,newState.title,newState.url); + // Check it + if ( this.isLastSavedState(newState) ) { + // Won't be a change + this.busy(false); + } + else { + // Store the newState + this.storeState(newState); + this.expectedStateId = newState.id; - // Fire HTML5 Event - History.Adapter.trigger(window,'popstate'); - } + // Push the newState + history.replaceState(newState.id,newState.title,newState.url); - // End replaceState closure - return true; - }; + // Fire HTML5 Event + Roo.get(window).fireEvent('popstate'); + } - } // !History.emulated.pushState + // End replaceState closure + return true; + }, // ==================================================================== // Initialise - /** - * Load the Store - */ - if ( sessionStorage ) { - // Fetch - try { - History.store = JSON.parse(sessionStorage.getItem('History.store'))||{}; - } - catch ( err ) { - History.store = {}; - } - - // Normalize - History.normalizeStore(); - } - else { - // Default Load - History.store = {}; - History.normalizeStore(); - } - - /** - * Clear Intervals on exit to prevent memory leaks - */ - History.Adapter.bind(window,"unload",History.clearAllIntervals); - - /** - * Create the initial State - */ - History.saveState(History.storeState(History.extractState(History.getLocationHref(),true))); - /** * Bind for Saving Store */ - if ( sessionStorage ) { - // When the page is closed - History.onUnload = function(){ - // Prepare - var currentStore, item, currentStoreString; - - // Fetch - try { - currentStore = JSON.parse(sessionStorage.getItem('History.store'))||{}; - } - catch ( err ) { - currentStore = {}; - } - - // Ensure - currentStore.idToState = currentStore.idToState || {}; - currentStore.urlToId = currentStore.urlToId || {}; - currentStore.stateToId = currentStore.stateToId || {}; - - // Sync - for ( item in History.idToState ) { - if ( !History.idToState.hasOwnProperty(item) ) { - continue; - } - currentStore.idToState[item] = History.idToState[item]; - } - for ( item in History.urlToId ) { - if ( !History.urlToId.hasOwnProperty(item) ) { - continue; - } - currentStore.urlToId[item] = History.urlToId[item]; - } - for ( item in History.stateToId ) { - if ( !History.stateToId.hasOwnProperty(item) ) { - continue; - } - currentStore.stateToId[item] = History.stateToId[item]; - } - - // Update - History.store = currentStore; - History.normalizeStore(); - - // In Safari, going into Private Browsing mode causes the - // Session Storage object to still exist but if you try and use - // or set any property/function of it it throws the exception - // "QUOTA_EXCEEDED_ERR: DOM Exception 22: An attempt was made to - // add something to storage that exceeded the quota." infinitely - // every second. - currentStoreString = JSON.stringify(currentStore); - try { - // Store - sessionStorage.setItem('History.store', currentStoreString); - } - catch (e) { - if (e.code === DOMException.QUOTA_EXCEEDED_ERR) { - if (sessionStorage.length) { - // Workaround for a bug seen on iPads. Sometimes the quota exceeded error comes up and simply - // removing/resetting the storage can work. - sessionStorage.removeItem('History.store'); - sessionStorage.setItem('History.store', currentStoreString); - } else { - // Otherwise, we're probably private browsing in Safari, so we'll ignore the exception. - } - } else { - throw e; - } - } - }; - - // For Internet Explorer - History.intervalList.push(setInterval(History.onUnload,History.options.storeInterval)); - - // For Other Browsers - History.Adapter.bind(window,'beforeunload',History.onUnload); - History.Adapter.bind(window,'unload',History.onUnload); - - // Both are enabled for consistency - } - - // Non-Native pushState Implementation - if ( !History.emulated.pushState ) { - // Be aware, the following is only for native pushState implementations - // If you are wanting to include something for all browsers - // Then include it above this if block - - /** - * Setup Safari Fix - */ - if ( History.bugs.safariPoll ) { - History.intervalList.push(setInterval(History.safariStatePoll, History.options.safariPollInterval)); - } - - /** - * Ensure Cross Browser Compatibility - */ - if ( window.navigator.vendor === 'Apple Computer, Inc.' || (window.navigator.appCodeName||'') === 'Mozilla' ) { - /** - * Fix Safari HashChange Issue - */ - - // Setup Alias - History.Adapter.bind(window,'hashchange',function(){ - History.Adapter.trigger(window,'popstate'); - }); - - // Initialise Alias - if ( History.getHash() ) { - History.Adapter.onDomLoad(function(){ - History.Adapter.trigger(window,'hashchange'); - }); - } - } - - } // !History.emulated.pushState - - - }; // History.initCore - - // Try to Initialise History - if (!History.options || !History.options.delayInit) { - History.init(); - } - -})(window); \ No newline at end of file + + // When the page is closed + onUnload : function(){ + // Prepare + var currentStore, item, currentStoreString; + + // Fetch + try { + currentStore = JSON.parse(this.sessionStorage.getItem('Roo.History.store'))||{}; + } + catch ( err ) { + currentStore = {}; + } + + // Ensure + currentStore.idToState = currentStore.idToState || {}; + currentStore.urlToId = currentStore.urlToId || {}; + currentStore.stateToId = currentStore.stateToId || {}; + + // Sync + for ( item in this.idToState ) { + if ( !this.idToState.hasOwnProperty(item) ) { + continue; + } + currentStore.idToState[item] = this.idToState[item]; + } + for ( item in this.urlToId ) { + if ( !this.urlToId.hasOwnProperty(item) ) { + continue; + } + currentStore.urlToId[item] = this.urlToId[item]; + } + for ( item in this.stateToId ) { + if ( !this.stateToId.hasOwnProperty(item) ) { + continue; + } + currentStore.stateToId[item] = this.stateToId[item]; + } + + // Update + this.store = currentStore; + this.normalizeStore(); + + // In Safari, going into Private Browsing mode causes the + // Session Storage object to still exist but if you try and use + // or set any property/function of it it throws the exception + // "QUOTA_EXCEEDED_ERR: DOM Exception 22: An attempt was made to + // add something to storage that exceeded the quota." infinitely + // every second. + currentStoreString = JSON.stringify(currentStore); + try { + // Store + this.sessionStorage.setItem('Roo.History.store', currentStoreString); + } + catch (e) { + if (e.code === DOMException.QUOTA_EXCEEDED_ERR) { + if (this.sessionStorage.length) { + // Workaround for a bug seen on iPads. Sometimes the quota exceeded error comes up and simply + // removing/resetting the storage can work. + this.sessionStorage.removeItem('Roo.History.store'); + this.sessionStorage.setItem('Roo.History.store', currentStoreString); + } else { + // Otherwise, we're probably private browsing in Safari, so we'll ignore the exception. + } + } else { + throw e; + } + } + } +}; \ No newline at end of file