##// END OF EJS Templates
fixes stupid git ci call that can break on systems that don't have this alias
fixes stupid git ci call that can break on systems that don't have this alias

File last commit:

r3038:16456e7b beta
r3344:801a80ec default
Show More
native.history.js
1973 lines | 48.5 KiB | application/javascript | JavascriptLexer
/**
* History.js Native Adapter
* @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/>
*/
// Closure
(function(window,undefined){
"use strict";
// Localise Globals
var History = window.History = window.History||{};
// Check Existence
if ( typeof History.Adapter !== 'undefined' ) {
throw new Error('History.js Adapter has already been loaded...');
}
// Add the Adapter
History.Adapter = {
/**
* History.Adapter.handlers[uid][eventName] = Array
*/
handlers: {},
/**
* History.Adapter._uid
* The current element unique identifier
*/
_uid: 1,
/**
* History.Adapter.uid(element)
* @param {Element} element
* @return {String} uid
*/
uid: function(element){
return element._uid || (element._uid = History.Adapter._uid++);
},
/**
* History.Adapter.bind(el,event,callback)
* @param {Element} element
* @param {String} eventName - custom and standard events
* @param {Function} callback
* @return
*/
bind: function(element,eventName,callback){
// Prepare
var uid = History.Adapter.uid(element);
// Apply Listener
History.Adapter.handlers[uid] = History.Adapter.handlers[uid] || {};
History.Adapter.handlers[uid][eventName] = History.Adapter.handlers[uid][eventName] || [];
History.Adapter.handlers[uid][eventName].push(callback);
// Bind Global Listener
element['on'+eventName] = (function(element,eventName){
return function(event){
History.Adapter.trigger(element,eventName,event);
};
})(element,eventName);
},
/**
* History.Adapter.trigger(el,event)
* @param {Element} element
* @param {String} eventName - custom and standard events
* @param {Object} event - a object of event data
* @return
*/
trigger: function(element,eventName,event){
// Prepare
event = event || {};
var uid = History.Adapter.uid(element),
i,n;
// Apply Listener
History.Adapter.handlers[uid] = History.Adapter.handlers[uid] || {};
History.Adapter.handlers[uid][eventName] = History.Adapter.handlers[uid][eventName] || [];
// Fire Listeners
for ( i=0,n=History.Adapter.handlers[uid][eventName].length; i<n; ++i ) {
History.Adapter.handlers[uid][eventName][i].apply(this,[event]);
}
},
/**
* History.Adapter.extractEventData(key,event,extra)
* @param {String} key - key for the event data to extract
* @param {String} event - custom and standard events
* @return {mixed}
*/
extractEventData: function(key,event){
var result = (event && event[key]) || undefined;
return result;
},
/**
* History.Adapter.onDomLoad(callback)
* @param {Function} callback
* @return
*/
onDomLoad: function(callback) {
var timeout = window.setTimeout(function(){
callback();
},2000);
window.onload = function(){
clearTimeout(timeout);
callback();
};
}
};
// Try and Initialise History
if ( typeof History.init !== 'undefined' ) {
History.init();
}
})(window);
/**
* 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
amplify = window.amplify||false, // Amplify.js
setTimeout = window.setTimeout,
clearTimeout = window.clearTimeout,
setInterval = window.setInterval,
clearInterval = window.clearInterval,
JSON = window.JSON,
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;
}
};
History.Adapter.bind(window,"beforeunload",History.clearAllIntervals);
History.Adapter.bind(window,"unload",History.clearAllIntervals);
// ----------------------------------------------------------------------
// 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
;
// Write to Console
if ( consoleExists ) {
var 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 ) {
var 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
* @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||document.URL||document.location.href;
// Create
var 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 = (document.URL||document.location.href).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;
};
// ----------------------------------------------------------------------
// State Storage
/**
* History.store
* The store for all session specific data
*/
History.store = amplify ? (amplify.store('History.store')||{}) : {};
History.store.idToState = History.store.idToState||{};
History.store.urlToId = History.store.urlToId||{};
History.store.stateToId = History.store.stateToId||{};
/**
* 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.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);
if ( !id ) {
// Find ID via State String
var 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 = String(Math.floor(Math.random()*1000));
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){
// 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
var newState = {};
newState.normalized = true;
newState.title = oldState.title||'';
newState.url = History.getFullUrl(oldState.url?decodeURIComponent(oldState.url):(document.URL||document.location.href));
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
var 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 = History.normalizeState(passedState);
// Clean
var cleanedState = {
data: State.data,
title: passedState.title,
url: passedState.url
};
// Fetch
var 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 = History.normalizeState(passedState);
// Fetch
var 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 hash, State = History.normalizeState(passedState);
// Fetch
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;
// Extract
var parts,url;
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;
create = create||false;
// Fetch SUID
var id = History.extractId(url_or_hash);
if ( id ) {
State = History.getStateById(id);
}
// Fetch SUID returned no State
if ( !State ) {
// Fetch URL
var 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;
// Fetch
var 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;
// Check
if ( History.savedStates.length ) {
var
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.escapeString()
* Escape a string
* @param {String} str
* @return {string}
*/
History.escapeString = function(str){
return encodeURI(url).replace(/%25/g, "%", "g");
};
/**
* 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){
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){
// 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
var 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
var 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){
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;
// Initial
if ( !title ) {
var 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('<','&lt;').replace('>','&gt;').replace(' & ',' &amp; ');
}
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(){
if ( History.busy.flag ) return;
for ( var i=History.queues.length-1; i >= 0; --i ) {
var queue = History.queues[i];
if ( queue.length === 0 ) continue;
var 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.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(document.URL||document.location.href),
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;
};
// ----------------------------------------------------------------------
// Initialise
/**
* Create the initial State
*/
History.saveState(History.storeState(History.extractState(document.URL||document.location.href,true)));
/**
* Bind for Saving Store
*/
if ( amplify ) {
History.onUnload = function(){
// Prepare
var
currentStore = amplify.store('History.store')||{},
item;
// 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;
// Store
amplify.store('History.store',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
}
// ----------------------------------------------------------------------
// HTML5 State Support
if ( History.emulated.pushState ) {
/*
* Provide Skeleton for HTML4 Browsers
*/
// Prepare
var emptyFunction = function(){};
History.pushState = History.pushState||emptyFunction;
History.replaceState = History.replaceState||emptyFunction;
}
else {
/*
* Use native HTML5 History API Implementation
*/
/**
* History.onPopState(event,extra)
* Refresh the Current State
*/
History.onPopState = function(event){
// Reset the double check
History.doubleCheckComplete();
// Check for a Hash, and handle apporiatly
var currentHash = History.getHash();
if ( currentHash ) {
// Expand Hash
var currentState = History.extractState(currentHash||document.URL||document.location.href,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;
}
// Prepare
var newState = false;
// Prepare
event = event||{};
if ( typeof event.state === 'undefined' ) {
// jQuery
if ( typeof event.originalEvent !== 'undefined' && typeof event.originalEvent.state !== 'undefined' ) {
event.state = event.originalEvent.state||false;
}
// MooTools
else if ( typeof event.event !== 'undefined' && typeof event.event.state !== 'undefined' ) {
event.state = event.event.state||false;
}
// Ensure
event.state = (event.state||false);
}
// Fetch State
if ( event.state ) {
// Vanilla: Back/forward button was used
newState = History.getStateById(event.state);
}
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(document.URL||document.location.href);
}
// The State did not exist in our store
if ( !newState ) {
// Regenerate the State
newState = History.createStateObject(null,null,document.URL||document.location.href);
}
// 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;
};
// 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);