mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-09-09 03:01:18 +02:00
Merged in pull request https://github.com/balupton/history.js/pull/108. This has been a serious problem with the library for more than a year, see https://github.com/balupton/history.js/issues/228.
649 lines
17 KiB
JavaScript
649 lines
17 KiB
JavaScript
/**
|
|
* History.js HTML4 Support
|
|
* Depends on the HTML5 Support
|
|
* @author Benjamin Arthur Lupton <contact@balupton.com>
|
|
* @copyright 2010-2011 Benjamin Arthur Lupton <contact@balupton.com>
|
|
* @license New BSD License <http://creativecommons.org/licenses/BSD/>
|
|
*/
|
|
|
|
(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.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),
|
|
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;
|
|
|
|
// 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
|
|
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
|
|
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.location);
|
|
|
|
// 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
|
|
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
|
|
|
|
// MODIFIED ischommer: URL normalization needs to respect our <base> tag,
|
|
// otherwise will go into infinite loops
|
|
currentState = History.extractState(History.getFullUrl(currentHash||History.getLocationHref(),true),true);
|
|
// END MODIFIED
|
|
|
|
// 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 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 ( !History.isHashEqual(newStateHash, html4Hash) && !History.isHashEqual(newStateHash, History.getShortUrl(History.getLocationHref())) ) {
|
|
//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);
|
|
|
|
// 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),
|
|
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;
|
|
};
|
|
|
|
} // 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 and Initialise History
|
|
if ( typeof History.init !== 'undefined' ) {
|
|
History.init();
|
|
}
|
|
|
|
})(window); |