/**
* 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();
// Check
var isLast = newHash === oldHash;
// Return isLast
return isLast;
};
/**
* 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);
// Create Discard Object
var 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.discardState(State)
* Checks to see if the state is discarded
* @param {object} State
* @return {bool}
*/
History.discardedState = function(State) {
// Prepare
var StateHash = History.getHashByState(State);
// Check
var 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 = '';
// 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
var
iframeId = 'historyjs-iframe',
iframe = document.createElement('iframe');
// Adjust iFarme
iframe.setAttribute('id', iframeId);
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
var
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.unescapeHash(iframe.contentWindow.document.location.hash) || '';
// 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;
// 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
setInterval(History.checkerFunction, History.options.hashChangeInterval);
// Done
return true;
}; // History.hashChangeInit
// Bind hashChangeInit
History.Adapter.onDomLoad(History.hashChangeInit);
} // History.emulated.hashChange
// ----------------------------------------------------------------------
// HTML5 State Support
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) || document.location.href),
currentHash = History.getHashByUrl(currentUrl),
currentState = null,
currentStateHash = null,
currentStateHashExits = null;
// 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 || document.location.href, false), 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
var 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, 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);
// Check the State
if (History.getHashByUrl(url)) {
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
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();
// 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 HTML4 Hash
if (newStateHash !== html4Hash && newStateHash !== History.getShortUrl(document.location.href)) {
//History.debug('History.pushState: update hash', newStateHash, html4Hash);
History.setHash(newStateHash, false);
return false;
}
// Update HTML5 State
History.saveState(newState);
// Fire HTML5 Event
//History.debug('History.pushState: trigger popstate');
History.Adapter.trigger(window, 'statechange');
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);
// Check the State
if (History.getHashByUrl(url)) {
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
History.busy(true);
// Fetch the State Objects
var
newState = History.createStateObject(data, title, url),
oldState = History.getState(false),
previousState = History.getStateByIndex(-2);
// Discard Old State
History.discardState(oldState, newState, previousState);
// Alias to PushState
History.pushState(newState.data, newState.title, newState.url, false);
// End replaceState closure
return true;
};
/**
* 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 and Initialise History
History.init();
})(window);