/**
* 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){
// --------------------------------------------------------------------------
// Initialise
// History Object
window.History = window.History||{};
window._History = window._History||{};
// Localise Globals
var
document = window.document, // Make sure we are using the correct document
_History = window._History, // Private History Object
History = window.History; // Public History Object
// Check Existence of History.js
if ( typeof History.initHtml4 !== 'undefined' ) {
throw new Error('History.js HTML4 Support has already been loaded...');
}
// Initialise HTML4 Support
History.initHtml4 = function(){
// ----------------------------------------------------------------------
// Check Status
if ( typeof History.initHtml5 === 'undefined' || typeof History.Adapter === 'undefined' ) {
return false;
}
// ----------------------------------------------------------------------
// 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 undef,
v = 3,
div = document.createElement('div'),
all = div.getElementsByTagName('i');
while (
div.innerHTML = '',
all[0]
);
return v > 4 ? v : undef;
})()
;
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
: (_History.getInternetExplorerMajorVersion() !== 0)
;
return result;
};
/**
* History.emulated
* Which features require emulating?
*/
History.emulated.hashChange = Boolean(
!('onhashchange' in window || 'onhashchange' in document)
||
(_History.isInternetExplorer() && _History.getInternetExplorerMajorVersion() < 8)
);
// ----------------------------------------------------------------------
// 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;
};
/**
* _History.stateHashExists
* Checks if the State Hash Exists
* @param {string} stateHash
* @return {boolean} exists
*/
_History.stateHashExists = function(stateHash){
// Prepare
var exists = typeof _History.statesByHash[stateHash] !== 'undefined';
// Return exists
return exists;
};
// ----------------------------------------------------------------------
// 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',this,arguments);
// Prepare
var discardedStateHash = History.contractState(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',this,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.contractState(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',this,arguments);
// Prepare
var StateHash = History.contractState(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.Adapter.onDomLoad(function(){
// Define our Checker Function
_History.checkerFunction = null;
// 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
lastDocumentHash = null,
lastIframeHash = null,
checkerRunning = false;
// Define the checker function
_History.checkerFunction = function(){
// Check Running
if ( checkerRunning ) {
History.debug('hashchange.checker: checker is running');
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 some variables that will help in our checker function
var
lastDocumentHash = null;
// 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.hashChangeCheckerDelay);
// End onDomLoad closure
return true;
});
}
// ----------------------------------------------------------------------
// 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',this,arguments);
// Prepare
var
currentUrl = (event && event.newURL) || document.location.href;
currentHash = unescape(History.extractHashFromUrl(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;
}
// Store our location for use in detecting back/forward direction
_History.saveHash(currentHash);
// Expand Hash
currentState = History.expandHash(currentHash);
if ( !currentState ) {
// Traditional Anchor Hash
History.debug('_History.onHashChange: traditional anchor');
History.Adapter.trigger(window,'anchorchange');
History.busy(false);
return false;
}
// Check if we are the same state
if ( _History.isLastState(currentState) ) {
// There has been no change (just the page's hash has finally propagated)
History.debug('_History.onHashChange: no change');
History.busy(false);
return false;
}
// Create the state Hash
currentStateHash = History.contractState(currentState);
// Log
History.debug('_History.onHashChange: ',
'currentStateHash',
currentStateHash,
'Hash -1',
_History.getHashByIndex(-1),
'Hash -2',
_History.getHashByIndex(-2),
'Hash -3',
_History.getHashByIndex(-3),
'Hash -4',
_History.getHashByIndex(-4),
'Hash -5',
_History.getHashByIndex(-5),
'Hash -6',
_History.getHashByIndex(-6),
'Hash -7',
_History.getHashByIndex(-7)
);
// Check if we are DiscardedState
var discardObject = _History.discardedState(currentState);
if ( discardObject ) {
History.debug('forwardState:',History.contractState(discardObject.forwardState),'backState:',History.contractState(discardObject.backState));
// Ignore this state as it has been discarded and go back to the state before it
if ( _History.getHashByIndex(-2) === History.contractState(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);
}
History.busy(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',this,arguments);
// Check the State
if ( History.extractHashFromUrl(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.contractState(newState),
oldState = History.getState(),
oldStateHash = History.getStateHash(),
html4Hash = unescape(History.getHash());
// Store the newState
_History.storeState(newState);
// Recycle the State
_History.recycleState(newState);
// Force update of the title
if ( document.title !== newState.title ) {
document.title = newState.title
try {
document.getElementsByTagName('title')[0].innerHTML = newState.title;
}
catch ( Exception ) { }
}
History.debug(
'History.pushState: details',
'newStateHash:', newStateHash,
'oldStateHash:', oldStateHash,
'html4Hash:', html4Hash
);
// Check if we are the same State
if ( newStateHash === oldStateHash ) {
History.debug('History.pushState: no change', newStateHash);
return false;
}
// Update HTML4 Hash
if ( newStateHash !== html4Hash ) {
History.debug('History.pushState: update hash', newStateHash);
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',this,arguments);
// Check the State
if ( History.extractHashFromUrl(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(),
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 ( !document.location.hash || document.location.hash === '#' ) {
History.Adapter.onDomLoad(function(){
History.debug('Internet Explorer Initial State Change Fix');
var currentState = History.createStateObject({},'',document.location.href);
History.pushState(currentState.data,currentState.title,currentState.url);
});
} else if ( !History.emulated.hashChange ) {
History.debug('Firefox Initial State Change Fix');
History.Adapter.onDomLoad(function(){
_History.onHashChange();
});
}
}
}
// Try Load HTML4 Support
History.initHtml4();
})(window);