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.
1949 lines
47 KiB
JavaScript
1949 lines
47 KiB
JavaScript
/**
|
|
* History.js Core
|
|
* @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
|
|
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
|
|
sessionStorage = window.sessionStorage||false, // sessionStorage
|
|
setTimeout = window.setTimeout,
|
|
clearTimeout = window.clearTimeout,
|
|
setInterval = window.setInterval,
|
|
clearInterval = window.clearInterval,
|
|
JSON = window.JSON,
|
|
alert = window.alert,
|
|
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;
|
|
|
|
|
|
// ====================================================================
|
|
// Interval record
|
|
|
|
/**
|
|
* History.intervalList
|
|
* List of intervals set, to be cleared when document is unloaded.
|
|
*/
|
|
History.intervalList = [];
|
|
|
|
/**
|
|
* History.clearAllIntervals
|
|
* Clears all setInterval instances.
|
|
*/
|
|
History.clearAllIntervals = function(){
|
|
var i, il = History.intervalList;
|
|
if (typeof il !== "undefined" && il !== null) {
|
|
for (i = 0; i < il.length; i++) {
|
|
clearInterval(il[i]);
|
|
}
|
|
History.intervalList = null;
|
|
}
|
|
};
|
|
|
|
|
|
// ====================================================================
|
|
// 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,
|
|
args,arg
|
|
;
|
|
|
|
// Write to Console
|
|
if ( consoleExists ) {
|
|
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 ) {
|
|
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 <contact@balupton.com>
|
|
* @author James Padolsey <https://gist.github.com/527683>
|
|
*/
|
|
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 = '<!--[if gt IE ' + (++v) + ']><i></i><![endif]-->') && 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 <contact@balupton.com>
|
|
*/
|
|
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 and eliminate all references to the original contexts
|
|
* @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 `<base href="...">` 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||History.getLocationHref(),
|
|
pageUrl;
|
|
|
|
// Create
|
|
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 = (History.getLocationHref()).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;
|
|
};
|
|
|
|
/**
|
|
* History.getLocationHref(document)
|
|
* Returns a normalized version of document.location.href
|
|
* accounting for browser inconsistencies, etc.
|
|
*
|
|
* This URL will be URI-encoded and will include the hash
|
|
*
|
|
* @param {object} document
|
|
* @return {string} url
|
|
*/
|
|
History.getLocationHref = function(doc) {
|
|
doc = doc || document;
|
|
|
|
// most of the time, this will be true
|
|
if (doc.URL === doc.location.href)
|
|
return doc.location.href;
|
|
|
|
// some versions of webkit URI-decode document.location.href
|
|
// but they leave document.URL in an encoded state
|
|
if (doc.location.href === decodeURIComponent(doc.URL))
|
|
return doc.URL;
|
|
|
|
// FF 3.6 only updates document.URL when a page is reloaded
|
|
// document.location.href is updated correctly
|
|
if (doc.location.hash && decodeURIComponent(doc.location.href.replace(/^[^#]+/, "")) === doc.location.hash)
|
|
return doc.location.href;
|
|
|
|
return doc.URL || doc.location.href;
|
|
};
|
|
|
|
|
|
// ====================================================================
|
|
// State Storage
|
|
|
|
/**
|
|
* History.store
|
|
* The store for all session specific data
|
|
*/
|
|
History.store = {};
|
|
|
|
/**
|
|
* 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.noramlizeStore()
|
|
* Noramlize the store by adding necessary values
|
|
*/
|
|
History.normalizeStore = function(){
|
|
History.store.idToState = History.store.idToState||{};
|
|
History.store.urlToId = History.store.urlToId||{};
|
|
History.store.stateToId = History.store.stateToId||{};
|
|
};
|
|
|
|
/**
|
|
* 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),
|
|
str;
|
|
|
|
if ( !id ) {
|
|
// Find ID via State String
|
|
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 = (new Date()).getTime() + String(Math.random()).replace(/\D/g,'');
|
|
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){
|
|
// Variables
|
|
var newState, dataNotEmpty;
|
|
|
|
// 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
|
|
newState = {};
|
|
newState.normalized = true;
|
|
newState.title = oldState.title||'';
|
|
newState.url = History.getFullUrl(oldState.url?decodeURIComponent(oldState.url):(History.getLocationHref()));
|
|
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
|
|
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': encodeURIComponent(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, cleanedState, str;
|
|
|
|
// Fetch
|
|
State = History.normalizeState(passedState);
|
|
|
|
// Clean
|
|
cleanedState = {
|
|
data: State.data,
|
|
title: passedState.title,
|
|
url: passedState.url
|
|
};
|
|
|
|
// Fetch
|
|
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, id;
|
|
|
|
// Fetch
|
|
State = History.normalizeState(passedState);
|
|
|
|
// Fetch
|
|
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 State, hash;
|
|
|
|
// Fetch
|
|
State = History.normalizeState(passedState);
|
|
|
|
// Hash
|
|
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,parts,url;
|
|
|
|
// Extract
|
|
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, id, url;
|
|
create = create||false;
|
|
|
|
// Fetch SUID
|
|
id = History.extractId(url_or_hash);
|
|
if ( id ) {
|
|
State = History.getStateById(id);
|
|
}
|
|
|
|
// Fetch SUID returned no State
|
|
if ( !State ) {
|
|
// Fetch URL
|
|
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,
|
|
oldState;
|
|
|
|
// Fetch
|
|
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,
|
|
newId, oldState, oldId;
|
|
|
|
// Check
|
|
if ( History.savedStates.length ) {
|
|
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()
|
|
* @param {Location=} location
|
|
* Gets the current document hash
|
|
* Note: unlike location.hash, this is guaranteed to return the escaped hash in all browsers
|
|
* @return {string}
|
|
*/
|
|
History.getHash = function(location){
|
|
if ( !location ) location = document.location;
|
|
var href = location.href.replace( /^[^#]*/, "" );
|
|
return href.substr(1);
|
|
};
|
|
|
|
/**
|
|
* 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 = decodeURIComponent(result);
|
|
|
|
// Return result
|
|
return result;
|
|
};
|
|
|
|
/**
|
|
* History.normalizeHash()
|
|
* normalize a hash across browsers
|
|
* @return {string}
|
|
*/
|
|
History.normalizeHash = function(hash){
|
|
// Prepare
|
|
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){
|
|
// Prepare
|
|
var State, pageUrl;
|
|
|
|
// 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);
|
|
|
|
// Make Busy + Continue
|
|
History.busy(true);
|
|
|
|
// Check if hash is a state
|
|
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 ( History.getHash() !== hash ) {
|
|
// 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
|
|
pageUrl = History.getPageUrl();
|
|
|
|
// Safari hash apply
|
|
History.pushState(null,null,pageUrl+'#'+hash,false);
|
|
}
|
|
else {
|
|
// Normal hash apply
|
|
document.location.hash = hash;
|
|
}
|
|
}
|
|
|
|
// Chain
|
|
return History;
|
|
};
|
|
|
|
/**
|
|
* History.escape()
|
|
* normalize and Escape a Hash
|
|
* @return {string}
|
|
*/
|
|
History.escapeHash = function(hash){
|
|
// Prepare
|
|
var result = History.normalizeHash(hash);
|
|
|
|
// Escape hash
|
|
result = window.encodeURIComponent(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,
|
|
firstState;
|
|
|
|
// Initial
|
|
if ( !title ) {
|
|
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(){
|
|
var i, queue, item;
|
|
if ( History.busy.flag ) return;
|
|
for ( i=History.queues.length-1; i >= 0; --i ) {
|
|
queue = History.queues[i];
|
|
if ( queue.length === 0 ) continue;
|
|
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.busy.flag
|
|
*/
|
|
History.busy.flag = false;
|
|
|
|
/**
|
|
* 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(History.getLocationHref()),
|
|
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;
|
|
};
|
|
|
|
|
|
// ====================================================================
|
|
// HTML5 State Support
|
|
|
|
// Non-Native pushState Implementation
|
|
if ( History.emulated.pushState ) {
|
|
/*
|
|
* Provide Skeleton for HTML4 Browsers
|
|
*/
|
|
|
|
// Prepare
|
|
var emptyFunction = function(){};
|
|
History.pushState = History.pushState||emptyFunction;
|
|
History.replaceState = History.replaceState||emptyFunction;
|
|
} // History.emulated.pushState
|
|
|
|
// Native pushState Implementation
|
|
else {
|
|
/*
|
|
* Use native HTML5 History API Implementation
|
|
*/
|
|
|
|
/**
|
|
* History.onPopState(event,extra)
|
|
* Refresh the Current State
|
|
*/
|
|
History.onPopState = function(event,extra){
|
|
// Prepare
|
|
var stateId = false, newState = false, currentHash, currentState;
|
|
|
|
// Reset the double check
|
|
History.doubleCheckComplete();
|
|
|
|
// Check for a Hash, and handle apporiatly
|
|
currentHash = History.getHash();
|
|
if ( currentHash ) {
|
|
// Expand Hash
|
|
currentState = History.extractState(currentHash||History.getLocationHref(),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;
|
|
}
|
|
|
|
// Ensure
|
|
stateId = History.Adapter.extractEventData('state',event,extra) || false;
|
|
|
|
// Fetch State
|
|
if ( stateId ) {
|
|
// Vanilla: Back/forward button was used
|
|
newState = History.getStateById(stateId);
|
|
}
|
|
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(History.getLocationHref());
|
|
}
|
|
|
|
// The State did not exist in our store
|
|
if ( !newState ) {
|
|
// Regenerate the State
|
|
newState = History.createStateObject(null,null,History.getLocationHref());
|
|
}
|
|
|
|
// 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;
|
|
};
|
|
|
|
} // !History.emulated.pushState
|
|
|
|
|
|
// ====================================================================
|
|
// Initialise
|
|
|
|
/**
|
|
* Load the Store
|
|
*/
|
|
if ( sessionStorage ) {
|
|
// Fetch
|
|
try {
|
|
History.store = JSON.parse(sessionStorage.getItem('History.store'))||{};
|
|
}
|
|
catch ( err ) {
|
|
History.store = {};
|
|
}
|
|
|
|
// Normalize
|
|
History.normalizeStore();
|
|
}
|
|
else {
|
|
// Default Load
|
|
History.store = {};
|
|
History.normalizeStore();
|
|
}
|
|
|
|
/**
|
|
* Clear Intervals on exit to prevent memory leaks
|
|
*/
|
|
History.Adapter.bind(window,"beforeunload",History.clearAllIntervals);
|
|
History.Adapter.bind(window,"unload",History.clearAllIntervals);
|
|
|
|
/**
|
|
* Create the initial State
|
|
*/
|
|
History.saveState(History.storeState(History.extractState(History.getLocationHref(),true)));
|
|
|
|
/**
|
|
* Bind for Saving Store
|
|
*/
|
|
if ( sessionStorage ) {
|
|
// When the page is closed
|
|
History.onUnload = function(){
|
|
// Prepare
|
|
var currentStore, item;
|
|
|
|
// Fetch
|
|
try {
|
|
currentStore = JSON.parse(sessionStorage.getItem('History.store'))||{};
|
|
}
|
|
catch ( err ) {
|
|
currentStore = {};
|
|
}
|
|
|
|
// 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;
|
|
History.normalizeStore();
|
|
|
|
// Store
|
|
sessionStorage.setItem('History.store',JSON.stringify(currentStore));
|
|
};
|
|
|
|
// For Internet Explorer
|
|
History.intervalList.push(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
|
|
}
|
|
|
|
// Non-Native pushState Implementation
|
|
if ( !History.emulated.pushState ) {
|
|
// 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 ) {
|
|
History.intervalList.push(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); |