/** * History.js HTML4 Support * Depends on the HTML5 Support * @author Benjamin Arthur Lupton * @copyright 2010-2011 Benjamin Arthur Lupton * @license New BSD License */ (function(window,undefined){ "use strict"; // ======================================================================== // Initialise // Localise Globals var document = window.document, // Make sure we are using the correct document setTimeout = window.setTimeout||setTimeout, clearTimeout = window.clearTimeout||clearTimeout, setInterval = window.setInterval||setInterval, History = window.History = window.History||{}; // Public History Object // Check Existence if ( typeof History.initHtml4 !== 'undefined' ) { throw new Error('History.js HTML4 Support has already been loaded...'); } // ======================================================================== // Initialise HTML4 Support // Initialise HTML4 Support History.initHtml4 = function(){ // Initialise if ( typeof History.initHtml4.initialized !== 'undefined' ) { // Already Loaded return false; } else { History.initHtml4.initialized = true; } // ==================================================================== // Properties /** * History.enabled * Is History enabled? */ History.enabled = true; // ==================================================================== // Hash Storage /** * History.savedHashes * Store the hashes in an array */ History.savedHashes = []; /** * History.isLastHash(newHash) * Checks if the hash is the last hash * @param {string} newHash * @return {boolean} true */ History.isLastHash = function(newHash){ // Prepare var oldHash = History.getHashByIndex(), isLast; // Check isLast = newHash === oldHash; // Return isLast return isLast; }; /** * History.isHashEqual(newHash, oldHash) * Checks to see if two hashes are functionally equal * @param {string} newHash * @param {string} oldHash * @return {boolean} true */ History.isHashEqual = function(newHash, oldHash){ newHash = encodeURIComponent(newHash).replace(/%25/g, "%"); oldHash = encodeURIComponent(oldHash).replace(/%25/g, "%"); return newHash === oldHash; }; /** * History.saveHash(newHash) * Push a Hash * @param {string} newHash * @return {boolean} true */ History.saveHash = function(newHash){ // Check Hash if ( History.isLastHash(newHash) ) { return false; } // Push the Hash History.savedHashes.push(newHash); // Return true return true; }; /** * History.getHashByIndex() * Gets a hash by the index * @param {integer} index * @return {string} */ History.getHashByIndex = function(index){ // Prepare var hash = null; // Handle if ( typeof index === 'undefined' ) { // Get the last inserted hash = History.savedHashes[History.savedHashes.length-1]; } else if ( index < 0 ) { // Get from the end hash = History.savedHashes[History.savedHashes.length+index]; } else { // Get from the beginning hash = History.savedHashes[index]; } // Return hash return hash; }; // ==================================================================== // Discarded States /** * History.discardedHashes * A hashed array of discarded hashes */ History.discardedHashes = {}; /** * History.discardedStates * A hashed array of discarded states */ History.discardedStates = {}; /** * History.discardState(State) * Discards the state by ignoring it through History * @param {object} State * @return {true} */ History.discardState = function(discardedState,forwardState,backState){ //History.debug('History.discardState', arguments); // Prepare var discardedStateHash = History.getHashByState(discardedState), discardObject; // Create Discard Object discardObject = { 'discardedState': discardedState, 'backState': backState, 'forwardState': forwardState }; // Add to DiscardedStates History.discardedStates[discardedStateHash] = discardObject; // Return true return true; }; /** * History.discardHash(hash) * Discards the hash by ignoring it through History * @param {string} hash * @return {true} */ History.discardHash = function(discardedHash,forwardState,backState){ //History.debug('History.discardState', arguments); // Create Discard Object var discardObject = { 'discardedHash': discardedHash, 'backState': backState, 'forwardState': forwardState }; // Add to discardedHash History.discardedHashes[discardedHash] = discardObject; // Return true return true; }; /** * History.discardedState(State) * Checks to see if the state is discarded * @param {object} State * @return {bool} */ History.discardedState = function(State){ // Prepare var StateHash = History.getHashByState(State), discarded; // Check discarded = History.discardedStates[StateHash]||false; // Return true return discarded; }; /** * History.discardedHash(hash) * Checks to see if the state is discarded * @param {string} State * @return {bool} */ History.discardedHash = function(hash){ // Check var discarded = History.discardedHashes[hash]||false; // Return true return discarded; }; /** * History.recycleState(State) * Allows a discarded state to be used again * @param {object} data * @param {string} title * @param {string} url * @return {true} */ History.recycleState = function(State){ //History.debug('History.recycleState', arguments); // Prepare var StateHash = History.getHashByState(State); // Remove from DiscardedStates if ( History.discardedState(State) ) { delete History.discardedStates[StateHash]; } // Return true return true; }; // ==================================================================== // HTML4 HashChange Support if ( History.emulated.hashChange ) { /* * We must emulate the HTML4 HashChange Support by manually checking for hash changes */ /** * History.hashChangeInit() * Init the HashChange Emulation */ History.hashChangeInit = function(){ // Define our Checker Function History.checkerFunction = null; // Define some variables that will help in our checker function var lastDocumentHash = '', iframeId, iframe, lastIframeHash, checkerRunning, startedWithHash = Boolean(History.getHash()); // Handle depending on the browser if ( History.isInternetExplorer() ) { // IE6 and IE7 // We need to use an iframe to emulate the back and forward buttons // Create iFrame iframeId = 'historyjs-iframe'; iframe = document.createElement('iframe'); // Adjust iFarme // IE 6 requires iframe to have a src on HTTPS pages, otherwise it will throw a // "This page contains both secure and nonsecure items" warning. iframe.setAttribute('id', iframeId); iframe.setAttribute('src', '#'); iframe.style.display = 'none'; // Append iFrame document.body.appendChild(iframe); // Create initial history entry iframe.contentWindow.document.open(); iframe.contentWindow.document.close(); // Define some variables that will help in our checker function lastIframeHash = ''; checkerRunning = false; // Define the checker function History.checkerFunction = function(){ // Check Running if ( checkerRunning ) { return false; } // Update Running checkerRunning = true; // Fetch var documentHash = History.getHash(), iframeHash = History.getHash(iframe.contentWindow.document); // The Document Hash has changed (application caused) if ( documentHash !== lastDocumentHash ) { // Equalise lastDocumentHash = documentHash; // Create a history entry in the iframe if ( iframeHash !== documentHash ) { //History.debug('hashchange.checker: iframe hash change', 'documentHash (new):', documentHash, 'iframeHash (old):', iframeHash); // Equalise lastIframeHash = iframeHash = documentHash; // Create History Entry iframe.contentWindow.document.open(); iframe.contentWindow.document.close(); // Update the iframe's hash iframe.contentWindow.document.location.hash = History.escapeHash(documentHash); } // Trigger Hashchange Event History.Adapter.trigger(window,'hashchange'); } // The iFrame Hash has changed (back button caused) else if ( iframeHash !== lastIframeHash ) { //History.debug('hashchange.checker: iframe hash out of sync', 'iframeHash (new):', iframeHash, 'documentHash (old):', documentHash); // Equalise lastIframeHash = iframeHash; // If there is no iframe hash that means we're at the original // iframe state. // And if there was a hash on the original request, the original // iframe state was replaced instantly, so skip this state and take // the user back to where they came from. if (startedWithHash && iframeHash === '') { History.back(); } else { // Update the Hash History.setHash(iframeHash,false); } } // Reset Running checkerRunning = false; // Return true return true; }; } else { // We are not IE // Firefox 1 or 2, Opera // Define the checker function History.checkerFunction = function(){ // Prepare var documentHash = History.getHash()||''; // The Document Hash has changed (application caused) if ( documentHash !== lastDocumentHash ) { // Equalise lastDocumentHash = documentHash; // Trigger Hashchange Event History.Adapter.trigger(window,'hashchange'); } // Return true return true; }; } // Apply the checker function History.intervalList.push(setInterval(History.checkerFunction, History.options.hashChangeInterval)); // Done return true; }; // History.hashChangeInit // Bind hashChangeInit History.Adapter.onDomLoad(History.hashChangeInit); } // History.emulated.hashChange // ==================================================================== // HTML5 State Support // Non-Native pushState Implementation if ( History.emulated.pushState ) { /* * We must emulate the HTML5 State Management by using HTML4 HashChange */ /** * History.onHashChange(event) * Trigger HTML5's window.onpopstate via HTML4 HashChange Support */ History.onHashChange = function(event){ //History.debug('History.onHashChange', arguments); // Prepare var currentUrl = ((event && event.newURL) || History.getLocationHref()), currentHash = History.getHashByUrl(currentUrl), currentState = null, currentStateHash = null, currentStateHashExits = null, discardObject; // Check if we are the same state if ( History.isLastHash(currentHash) ) { // There has been no change (just the page's hash has finally propagated) //History.debug('History.onHashChange: no change'); History.busy(false); return false; } // Reset the double check History.doubleCheckComplete(); // Store our location for use in detecting back/forward direction History.saveHash(currentHash); // Expand Hash if ( currentHash && History.isTraditionalAnchor(currentHash) ) { //History.debug('History.onHashChange: traditional anchor', currentHash); // Traditional Anchor Hash History.Adapter.trigger(window,'anchorchange'); History.busy(false); return false; } // Create State currentState = History.extractState(History.getFullUrl(currentHash||History.getLocationHref()),true); // Check if we are the same state if ( History.isLastSavedState(currentState) ) { //History.debug('History.onHashChange: no change'); // There has been no change (just the page's hash has finally propagated) History.busy(false); return false; } // Create the state Hash currentStateHash = History.getHashByState(currentState); // Check if we are DiscardedState discardObject = History.discardedState(currentState); if ( discardObject ) { // Ignore this state as it has been discarded and go back to the state before it if ( History.getHashByIndex(-2) === History.getHashByState(discardObject.forwardState) ) { // We are going backwards //History.debug('History.onHashChange: go backwards'); History.back(false); } else { // We are going forwards //History.debug('History.onHashChange: go forwards'); History.forward(false); } return false; } // Push the new HTML5 State //History.debug('History.onHashChange: success hashchange'); History.pushState(currentState.data,currentState.title,encodeURI(currentState.url),false); // End onHashChange closure return true; }; History.Adapter.bind(window,'hashchange',History.onHashChange); /** * 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); // We assume that the URL passed in is URI-encoded, but this makes // sure that it's fully URI encoded; any '%'s that are encoded are // converted back into '%'s url = encodeURI(url).replace(/%25/g, "%"); // Check the State if ( History.getHashByUrl(url) ) { throw new Error('History.js does not support states with fragment-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 History.busy(true); // Fetch the State Object var newState = History.createStateObject(data,title,url), newStateHash = History.getHashByState(newState), oldState = History.getState(false), oldStateHash = History.getHashByState(oldState), html4Hash = History.getHash(), wasExpected = History.expectedStateId == newState.id; // Store the newState History.storeState(newState); History.expectedStateId = newState.id; // Recycle the State History.recycleState(newState); // Force update of the title History.setTitle(newState); // Check if we are the same State if ( newStateHash === oldStateHash ) { //History.debug('History.pushState: no change', newStateHash); History.busy(false); return false; } // Update HTML5 State History.saveState(newState); // Fire HTML5 Event if(!wasExpected) History.Adapter.trigger(window,'statechange'); // Update HTML4 Hash if ( !History.isHashEqual(newStateHash, html4Hash) && !History.isHashEqual(newStateHash, History.getShortUrl(History.getLocationHref())) ) { History.setHash(newStateHash,false); } History.busy(false); // 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); // We assume that the URL passed in is URI-encoded, but this makes // sure that it's fully URI encoded; any '%'s that are encoded are // converted back into '%'s url = encodeURI(url).replace(/%25/g, "%"); // Check the State if ( History.getHashByUrl(url) ) { throw new Error('History.js does not support states with fragment-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 History.busy(true); // Fetch the State Objects var newState = History.createStateObject(data,title,url), newStateHash = History.getHashByState(newState), oldState = History.getState(false), oldStateHash = History.getHashByState(oldState), previousState = History.getStateByIndex(-2); // Discard Old State History.discardState(oldState,newState,previousState); // If the url hasn't changed, just store and save the state // and fire a statechange event to be consistent with the // html 5 api if ( newStateHash === oldStateHash ) { // Store the newState History.storeState(newState); History.expectedStateId = newState.id; // Recycle the State History.recycleState(newState); // Force update of the title History.setTitle(newState); // Update HTML5 State History.saveState(newState); // Fire HTML5 Event //History.debug('History.pushState: trigger popstate'); History.Adapter.trigger(window,'statechange'); History.busy(false); } else { // Alias to PushState History.pushState(newState.data,newState.title,newState.url,false); } // End replaceState closure return true; }; } // History.emulated.pushState // ==================================================================== // Initialise // Non-Native pushState Implementation if ( History.emulated.pushState ) { /** * Ensure initial state is handled correctly */ if ( History.getHash() && !History.emulated.hashChange ) { History.Adapter.onDomLoad(function(){ History.Adapter.trigger(window,'hashchange'); }); } } // History.emulated.pushState }; // History.initHtml4 // Try to Initialise History if ( typeof History.init !== 'undefined' ) { History.init(); } })(window);