/** * History.js Core * @author Benjamin Arthur Lupton * @copyright 2010-2011 Benjamin Arthur Lupton * @license New BSD License */ (function(window, undefined) { "use strict"; // -------------------------------------------------------------------------- // Initialise // Localise Globals var console = window.console || undefined, // Prevent a JSLint complain document = window.document, // Make sure we are using the correct document navigator = window.navigator, // Make sure we are using the correct navigator amplify = window.amplify || false, // Amplify.js setTimeout = window.setTimeout, clearTimeout = window.clearTimeout, setInterval = window.setInterval, JSON = window.JSON, History = window.History = window.History || {}, // Public History Object history = window.history; // Old History Object // MooTools Compatibility JSON.stringify = JSON.stringify || JSON.encode; JSON.parse = JSON.parse || JSON.decode; // Check Existence if (typeof History.init !== 'undefined') { throw new Error('History.js Core has already been loaded...'); } // Initialise History History.init = function() { // Check Load Status of Adapter if (typeof History.Adapter === 'undefined') { return false; } // Check Load Status of Core if (typeof History.initCore !== 'undefined') { History.initCore(); } // Check Load Status of HTML4 Support if (typeof History.initHtml4 !== 'undefined') { History.initHtml4(); } // Return true return true; }; // -------------------------------------------------------------------------- // Initialise Core // Initialise Core History.initCore = function() { // Initialise if (typeof History.initCore.initialized !== 'undefined') { // Already Loaded return false; } else { History.initCore.initialized = true; } // ---------------------------------------------------------------------- // Options /** * History.options * Configurable options */ History.options = History.options || {}; /** * History.options.hashChangeInterval * How long should the interval be before hashchange checks */ History.options.hashChangeInterval = History.options.hashChangeInterval || 100; /** * History.options.safariPollInterval * How long should the interval be before safari poll checks */ History.options.safariPollInterval = History.options.safariPollInterval || 500; /** * History.options.doubleCheckInterval * How long should the interval be before we perform a double check */ History.options.doubleCheckInterval = History.options.doubleCheckInterval || 500; /** * History.options.storeInterval * How long should we wait between store calls */ History.options.storeInterval = History.options.storeInterval || 1000; /** * History.options.busyDelay * How long should we wait between busy events */ History.options.busyDelay = History.options.busyDelay || 250; /** * History.options.debug * If true will enable debug messages to be logged */ History.options.debug = History.options.debug || false; /** * History.options.initialTitle * What is the title of the initial state */ History.options.initialTitle = History.options.initialTitle || document.title; // ---------------------------------------------------------------------- // Debug /** * History.debug(message,...) * Logs the passed arguments if debug enabled */ History.debug = function() { if ((History.options.debug || false)) { History.log.apply(History, arguments); } }; /** * History.log(message,...) * Logs the passed arguments */ History.log = function() { // Prepare var consoleExists = !(typeof console === 'undefined' || typeof console.log === 'undefined' || typeof console.log.apply === 'undefined'), textarea = document.getElementById('log'), message, i,n ; // Write to Console if (consoleExists) { var args = Array.prototype.slice.call(arguments); message = args.shift(); if (typeof console.debug !== 'undefined') { console.debug.apply(console, [message,args]); } else { console.log.apply(console, [message,args]); } } else { message = ("\n" + arguments[0] + "\n"); } // Write to log for (i = 1,n = arguments.length; i < n; ++i) { var arg = arguments[i]; if (typeof arg === 'object' && typeof JSON !== 'undefined') { try { arg = JSON.stringify(arg); } catch (Exception) { // Recursive Object } } message += "\n" + arg + "\n"; } // Textarea if (textarea) { textarea.value += message + "\n-----\n"; textarea.scrollTop = textarea.scrollHeight - textarea.clientHeight; } // No Textarea, No Console else if (!consoleExists) { alert(message); } // Return true return true; }; // ---------------------------------------------------------------------- // Emulated Status /** * History.getInternetExplorerMajorVersion() * Get's the major version of Internet Explorer * @return {integer} * @license Public Domain * @author Benjamin Arthur Lupton * @author James Padolsey */ History.getInternetExplorerMajorVersion = function() { var result = History.getInternetExplorerMajorVersion.cached = (typeof History.getInternetExplorerMajorVersion.cached !== 'undefined') ? History.getInternetExplorerMajorVersion.cached : (function() { var v = 3, div = document.createElement('div'), all = div.getElementsByTagName('i'); while ((div.innerHTML = '') && all[0]) { } return (v > 4) ? v : false; })() ; return result; }; /** * History.isInternetExplorer() * Are we using Internet Explorer? * @return {boolean} * @license Public Domain * @author Benjamin Arthur Lupton */ History.isInternetExplorer = function() { var result = History.isInternetExplorer.cached = (typeof History.isInternetExplorer.cached !== 'undefined') ? History.isInternetExplorer.cached : Boolean(History.getInternetExplorerMajorVersion()) ; return result; }; /** * History.emulated * Which features require emulating? */ History.emulated = { pushState: !Boolean( window.history && window.history.pushState && window.history.replaceState && !( (/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i).test(navigator.userAgent) /* disable for versions of iOS before version 4.3 (8F190) */ || (/AppleWebKit\/5([0-2]|3[0-2])/i).test(navigator.userAgent) /* disable for the mercury iOS browser, or at least older versions of the webkit engine */ ) ), hashChange: Boolean( !(('onhashchange' in window) || ('onhashchange' in document)) || (History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 8) ) }; /** * History.enabled * Is History enabled? */ History.enabled = !History.emulated.pushState; /** * History.bugs * Which bugs are present */ History.bugs = { /** * 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 */ setHash: Boolean(!History.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 */ safariPoll: Boolean(!History.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) */ ieDoubleCheck: Boolean(History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 8), /** * MSIE 6 requires the entire hash to be encoded for the hashes to trigger the onHashChange event */ hashEscape: Boolean(History.isInternetExplorer() && History.getInternetExplorerMajorVersion() < 7) }; /** * History.isEmptyObject(obj) * Checks to see if the Object is Empty * @param {Object} obj * @return {boolean} */ History.isEmptyObject = function(obj) { for (var name in obj) { return false; } return true; }; /** * History.cloneObject(obj) * Clones a object * @param {Object} obj * @return {Object} */ History.cloneObject = function(obj) { var hash,newObj; if (obj) { hash = JSON.stringify(obj); newObj = JSON.parse(hash); } else { newObj = {}; } return newObj; }; // ---------------------------------------------------------------------- // URL Helpers /** * History.getRootUrl() * Turns "http://mysite.com/dir/page.html?asd" into "http://mysite.com" * @return {String} rootUrl */ History.getRootUrl = function() { // Create var rootUrl = document.location.protocol + '//' + (document.location.hostname || document.location.host); if (document.location.port || false) { rootUrl += ':' + document.location.port; } rootUrl += '/'; // Return return rootUrl; }; /** * History.getBaseHref() * Fetches the `href` attribute of the `` element if it exists * @return {String} baseHref */ History.getBaseHref = function() { // Create var baseElements = 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 += '/'; // Return return baseHref; }; /** * History.getBaseUrl() * Fetches the baseHref or basePageUrl or rootUrl (whichever one exists first) * @return {String} baseUrl */ History.getBaseUrl = function() { // Create var baseUrl = History.getBaseHref() || History.getBasePageUrl() || History.getRootUrl(); // Return return baseUrl; }; /** * History.getPageUrl() * Fetches the URL of the current page * @return {String} pageUrl */ History.getPageUrl = function() { // Fetch var State = History.getState(false, false), stateUrl = (State || {}).url || document.location.href; // Create var pageUrl = stateUrl.replace(/\/+$/, '').replace(/[^\/]+$/, function(part, index, string) { return (/\./).test(part) ? part : part + '/'; }); // Return return pageUrl; }; /** * History.getBasePageUrl() * Fetches the Url of the directory of the current page * @return {String} basePageUrl */ History.getBasePageUrl = function() { // Create var basePageUrl = document.location.href.replace(/[#\?].*/, '').replace(/[^\/]+$/, function(part, index, string) { return (/[^\/]$/).test(part) ? '' : part; }).replace(/\/+$/, '') + '/'; // Return return basePageUrl; }; /** * History.getFullUrl(url) * Ensures that we have an absolute URL and not a relative URL * @param {string} url * @param {Boolean} allowBaseHref * @return {string} fullUrl */ History.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 = History.getRootUrl() + url.replace(/^\/+/, ''); } else if (firstChar === '#') { // Anchor URL fullUrl = History.getPageUrl().replace(/#.*/, '') + url; } else if (firstChar === '?') { // Query URL fullUrl = History.getPageUrl().replace(/[\?#].*/, '') + url; } else { // Relative URL if (allowBaseHref) { fullUrl = History.getBaseUrl() + url.replace(/^(\.\/)+/, ''); } else { fullUrl = History.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 } // Return return fullUrl.replace(/\#$/, ''); }; /** * History.getShortUrl(url) * Ensures that we have a relative URL and not a absolute URL * @param {string} url * @return {string} url */ History.getShortUrl = function(url) { // Prepare var shortUrl = url, baseUrl = History.getBaseUrl(), rootUrl = History.getRootUrl(); // Trim baseUrl if (History.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, ''); } // Trim rootUrl shortUrl = shortUrl.replace(rootUrl, '/'); // Ensure we can still detect it as a state if (History.isTraditionalAnchor(shortUrl)) { shortUrl = './' + shortUrl; } // Clean It shortUrl = shortUrl.replace(/^(\.\/)+/g, './').replace(/\#$/, ''); // Return return shortUrl; }; // ---------------------------------------------------------------------- // State Storage /** * History.store * The store for all session specific data */ History.store = amplify ? (amplify.store('History.store') || {}) : {}; History.store.idToState = History.store.idToState || {}; History.store.urlToId = History.store.urlToId || {}; History.store.stateToId = History.store.stateToId || {}; /** * History.idToState * 1-1: State ID to State Object */ History.idToState = History.idToState || {}; /** * History.stateToId * 1-1: State String to State ID */ History.stateToId = History.stateToId || {}; /** * History.urlToId * 1-1: State URL to State ID */ History.urlToId = History.urlToId || {}; /** * History.storedStates * Store the states in an array */ History.storedStates = History.storedStates || []; /** * History.savedStates * Saved the states in an array */ History.savedStates = History.savedStates || []; /** * 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; } // Fetch var State = History.getLastSavedState(); // Create if (!State && create) { State = History.createStateObject(); } // Adjust if (friendly) { State = History.cloneObject(State); State.url = State.cleanUrl || State.url; } // Return return State; }; /** * History.getIdByState(State) * Gets a ID for a State * @param {State} newState * @return {String} id */ History.getIdByState = function(newState) { // Fetch ID var id = History.extractId(newState.url); if (!id) { // Find ID via State String var 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 = String(Math.floor(Math.random() * 1000)); 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; } } // Return ID return id; }; /** * History.normalizeState(State) * Expands a State Object * @param {object} State * @return {object} */ History.normalizeState = function(oldState) { // Prepare if (!oldState || (typeof oldState !== 'object')) { oldState = {}; } // Check if (typeof oldState.normalized !== 'undefined') { return oldState; } // Adjust if (!oldState.data || (typeof oldState.data !== 'object')) { oldState.data = {}; } // ---------------------------------------------------------------------- // Create var newState = {}; newState.normalized = true; newState.title = oldState.title || ''; newState.url = History.getFullUrl(History.unescapeString(oldState.url || document.location.href)); newState.hash = History.getShortUrl(newState.url); newState.data = History.cloneObject(oldState.data); // Fetch ID newState.id = History.getIdByState(newState); // ---------------------------------------------------------------------- // Clean the URL newState.cleanUrl = newState.url.replace(/\??\&_suid.*/, ''); newState.url = newState.cleanUrl; // Check to see if we have more than just a url var dataNotEmpty = !History.isEmptyObject(newState.data); // Apply if (newState.title || dataNotEmpty) { // 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); // ---------------------------------------------------------------------- // Update the URL if we have a duplicate if ((History.emulated.pushState || History.bugs.safariPoll) && History.hasUrlDuplicate(newState)) { newState.url = newState.hashedUrl; } // ---------------------------------------------------------------------- // Return return newState; }; /** * 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 }; // Expand the State State = History.normalizeState(State); // Return object return State; }; /** * History.getStateById(id) * Get a state by it's UID * @param {String} id */ History.getStateById = function(id) { // Prepare id = String(id); // Retrieve var State = History.idToState[id] || History.store.idToState[id] || undefined; // Return State return State; }; /** * Get a State's String * @param {State} passedState */ History.getStateString = function(passedState) { // Prepare var State = History.normalizeState(passedState); // Clean var cleanedState = { data: State.data, title: passedState.title, url: passedState.url }; // Fetch var str = JSON.stringify(cleanedState); // Return return str; }; /** * Get a State's ID * @param {State} passedState * @return {String} id */ History.getStateId = function(passedState) { // Prepare var State = History.normalizeState(passedState); // Fetch var 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 hash, State = History.normalizeState(passedState); // Fetch 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; // Extract var parts,url; parts = /(.*)\&_suid=([0-9]+)$/.exec(url_or_hash); 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; }; /** * 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; create = create || false; // Fetch SUID var id = History.extractId(url_or_hash); if (id) { State = History.getStateById(id); } // Fetch SUID returned no State if (!State) { // Fetch URL var url = History.getFullUrl(url_or_hash); // Check URL id = History.getIdByUrl(url) || false; if (id) { State = History.getStateById(id); } // Create State if (!State && create && !History.isTraditionalAnchor(url_or_hash)) { State = History.createStateObject(null, null, url); } } // Return return State; }; /** * 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; }; /** * 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; }; /** * 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; }; /** * 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; // Fetch var oldState = History.extractState(newState.url); // Check hasDuplicate = oldState && oldState.id !== newState.id; // Return return hasDuplicate; }; /** * History.storeState * Store a State * @param {Object} newState * @return {Object} newState */ History.storeState = function(newState) { // Store the State History.urlToId[newState.url] = newState.id; // Push the State History.storedStates.push(History.cloneObject(newState)); // Return newState return newState; }; /** * 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; // Check if (History.savedStates.length) { var newId = newState.id, oldState = History.getLastSavedState(), oldId = oldState.id; // Check isLast = (newId === oldId); } // Return return isLast; }; /** * History.saveState * Push a State * @param {Object} newState * @return {boolean} changed */ History.saveState = function(newState) { // Check Hash if (History.isLastSavedState(newState)) { return false; } // Push the State History.savedStates.push(History.cloneObject(newState)); // Return true return true; }; /** * History.getStateByIndex() * Gets a state by the index * @param {integer} index * @return {Object} */ History.getStateByIndex = function(index) { // Prepare var State = null; // 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]; } // Return State return State; }; // ---------------------------------------------------------------------- // Hash Helpers /** * History.getHash() * Gets the current document hash * @return {string} */ History.getHash = function() { var hash = History.unescapeHash(document.location.hash); return hash; }; /** * History.unescapeString() * Unescape a string * @param {String} str * @return {string} */ History.unescapeString = function(str) { // Prepare var result = str; // Unescape hash var tmp; while (true) { tmp = window.unescape(result); if (tmp === result) { break; } result = tmp; } // Return result return result; }; /** * History.unescapeHash() * normalize and Unescape a Hash * @param {String} hash * @return {string} */ History.unescapeHash = function(hash) { // Prepare var result = History.normalizeHash(hash); // Unescape hash result = History.unescapeString(result); // Return result return result; }; /** * History.normalizeHash() * normalize a hash across browsers * @return {string} */ History.normalizeHash = function(hash) { var result = hash.replace(/[^#]*#/, '').replace(/#.*/, ''); // Return result return result; }; /** * History.setHash(hash) * Sets the document hash * @param {string} hash * @return {History} */ History.setHash = function(hash, queue) { // 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; } // Log //History.debug('History.setHash: called',hash); // Prepare var adjustedHash = History.escapeHash(hash); // Make Busy + Continue History.busy(true); // Check if hash is a state var 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); // PushState History.pushState(State.data, State.title, State.url, false); } else if (document.location.hash !== adjustedHash) { // Hash is a proper hash, so apply it // Handle browser bugs if (History.bugs.setHash) { // Fix Safari Bug https://bugs.webkit.org/show_bug.cgi?id=56249 // Fetch the base page var pageUrl = History.getPageUrl(); // Safari hash apply History.pushState(null, null, pageUrl + '#' + adjustedHash, false); } else { // Normal hash apply document.location.hash = adjustedHash; } } // Chain return History; }; /** * History.escape() * normalize and Escape a Hash * @return {string} */ History.escapeHash = function(hash) { var result = History.normalizeHash(hash); // Escape hash result = window.escape(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, '?'); } // Return result return result; }; /** * 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') ; // Unescape hash hash = History.unescapeHash(hash); // Return hash return hash; }; /** * History.setTitle(title) * Applies the title to the document * @param {State} newState * @return {Boolean} */ History.setTitle = function(newState) { // Prepare var title = newState.title; // Initial if (!title) { var firstState = History.getStateByIndex(0); if (firstState && firstState.url === newState.url) { title = firstState.title || History.options.initialTitle; } } // Apply try { document.getElementsByTagName('title')[0].innerHTML = title.replace('<', '<').replace('>', '>').replace(' & ', ' & '); } catch (Exception) { } document.title = title; // Chain return History; }; // ---------------------------------------------------------------------- // Queueing /** * History.queues * The list of queues to use * First In, First Out */ History.queues = []; /** * 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; } // Queue if (!History.busy.flag) { // Execute the next item in the queue clearTimeout(History.busy.timeout); var fireNext = function() { if (History.busy.flag) return; for (var i = History.queues.length - 1; i >= 0; --i) { var queue = History.queues[i]; if (queue.length === 0) continue; var item = queue.shift(); History.fireQueueItem(item); History.busy.timeout = setTimeout(fireNext, History.options.busyDelay); } }; History.busy.timeout = setTimeout(fireNext, History.options.busyDelay); } // Return return History.busy.flag; }; /** * 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] || []; // Add to the queue History.queues[item.queue || 0].push(item); // Chain return History; }; /** * 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); } // Chain return History; }; /** * History.clearQueue() * Clears the Queue */ History.clearQueue = function() { History.busy.flag = false; History.queues = []; return History; }; // ---------------------------------------------------------------------- // IE Bug Fix /** * History.stateChanged * States whether or not the state has changed since the last double check was initialised */ History.stateChanged = false; /** * History.doubleChecker * Contains the timeout used for the double checks */ History.doubleChecker = false; /** * History.doubleCheckComplete() * Complete a double check * @return {History} */ History.doubleCheckComplete = function() { // Update History.stateChanged = true; // Clear History.doubleCheckClear(); // Chain return History; }; /** * History.doubleCheckClear() * Clear a double check * @return {History} */ History.doubleCheckClear = function() { // Clear if (History.doubleChecker) { clearTimeout(History.doubleChecker); History.doubleChecker = false; } // Chain return History; }; /** * 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 = 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 History; }; // ---------------------------------------------------------------------- // Safari Bug Fix /** * History.safariStatePoll() * Poll the current state * @return {History} */ History.safariStatePoll = function() { // Poll the URL // Get the Last State which has the new URL var urlState = History.extractState(document.location.href), newState; // Check for a difference if (!History.isLastSavedState(urlState)) { newState = urlState; } else { return; } // Check if we have a state with that url // If not create it if (!newState) { //History.debug('History.safariStatePoll: new'); newState = History.createStateObject(); } // Apply the New State //History.debug('History.safariStatePoll: trigger'); History.Adapter.trigger(window, 'popstate'); // Chain return History; }; // ---------------------------------------------------------------------- // State Aliases /** * 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; } // Make Busy + Continue History.busy(true); // Fix certain browser bugs that prevent the state from changing History.doubleCheck(function() { History.back(false); }); // Go back history.go(-1); // End back closure return 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; } // Make Busy + Continue History.busy(true); // Fix certain browser bugs that prevent the state from changing History.doubleCheck(function() { History.forward(false); }); // Go forward history.go(1); // End forward closure return true; }; /** * 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); // Prepare var i; // 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.'); } // Chain return History; }; // ---------------------------------------------------------------------- // Initialise /** * Create the initial State */ History.saveState(History.storeState(History.extractState(document.location.href, true))); /** * Bind for Saving Store */ if (amplify) { History.onUnload = function() { // Prepare var currentStore = amplify.store('History.store') || {}, item; // 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; // Store amplify.store('History.store', currentStore); }; // For Internet Explorer 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 } // ---------------------------------------------------------------------- // HTML5 State Support if (History.emulated.pushState) { /* * Provide Skeleton for HTML4 Browsers */ // Prepare var emptyFunction = function() { }; History.pushState = History.pushState || emptyFunction; History.replaceState = History.replaceState || emptyFunction; } else { /* * Use native HTML5 History API Implementation */ /** * History.onPopState(event,extra) * Refresh the Current State */ History.onPopState = function(event) { // Reset the double check History.doubleCheckComplete(); // Check for a Hash, and handle apporiatly var currentHash = History.getHash(); if (currentHash) { // Expand Hash var currentState = History.extractState(currentHash || document.location.href, 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; } // Prepare var newState = false; // Prepare event = event || {}; if (typeof event.state === 'undefined') { // jQuery if (typeof event.originalEvent !== 'undefined' && typeof event.originalEvent.state !== 'undefined') { event.state = event.originalEvent.state || false; } // MooTools else if (typeof event.event !== 'undefined' && typeof event.event.state !== 'undefined') { event.state = event.event.state || false; } } // Ensure event.state = (event.state || false); // Fetch State if (event.state) { // Vanilla: Back/forward button was used newState = History.getStateById(event.state); } 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(document.location.href); } // The State did not exist in our store if (!newState) { // Regenerate the State newState = History.createStateObject(null, null, document.location.href); } // Clean History.expectedStateId = false; // 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; } // Store the State History.storeState(newState); History.saveState(newState); // Force update of the title History.setTitle(newState); // Fire Our Event History.Adapter.trigger(window, 'statechange'); History.busy(false); // Return true return true; }; History.Adapter.bind(window, 'popstate', History.onPopState); /** * 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); // Check the State if (History.getHashByUrl(url) && History.emulated.pushState) { throw new Error('History.js does not support states with fragement-identifiers (hashes/anchors).'); } // 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; } // Make Busy + Continue History.busy(true); // Create the newState var newState = History.createStateObject(data, title, url); // 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; // Push the newState history.pushState(newState.id, newState.title, newState.url); // Fire HTML5 Event History.Adapter.trigger(window, 'popstate'); } // End pushState closure return true; }; /** * 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); // Check the State if (History.getHashByUrl(url) && History.emulated.pushState) { throw new Error('History.js does not support states with fragement-identifiers (hashes/anchors).'); } // 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; } // Make Busy + Continue History.busy(true); // Create the newState var newState = History.createStateObject(data, title, url); // 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; // Push the newState history.replaceState(newState.id, newState.title, newState.url); // Fire HTML5 Event History.Adapter.trigger(window, 'popstate'); } // End replaceState closure return true; }; // 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) { setInterval(History.safariStatePoll, History.options.safariPollInterval); } /** * Ensure Cross Browser Compatibility */ if (navigator.vendor === 'Apple Computer, Inc.' || (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 and Initialise History History.init(); })(window);