/**
* History management component that allows you to register arbitrary tokens that signify
* application history state on navigation actions. You can then handle the history
* {@link #change} event in order to reset your application UI to the appropriate state when
* the user navigates forward or backward through the browser history stack.
*
* ## Initializing
*
* The {@link #init} method of the History object must be called before using History. This sets up
* the internal state and must be the first thing called before using History.
*/
Ext.define('Ext.util.History', {
singleton: true,
alternateClassName: 'Ext.History',
mixins: {
observable: 'Ext.util.Observable'
},
/**
* @property
* True to use `window.top.location.hash` or false to use `window.location.hash`. Must be set
* before {@link #init} is called because the `hashchange` event listener is added to the window
* at initialization time.
*/
useTopWindow: false,
/**
* @property {Boolean} hashbang If set to `true`, when a hash is set, the hash will be prefixed
* with an exclamation making it a hash bang instead of just a hash.
*
* Ext.util.History.add('foo'); // will result in #foo
*
* Ext.util.History.hashbang = true;
* Ext.util.History.add('bar'); // will result in #!bar
*/
/**
* @property {String} currentToken The current token.
* @private
*/
/**
* @event ready
* Fires when the Ext.util.History singleton has been initialized and is ready for use.
* @param {Ext.util.History} history The Ext.util.History singleton.
*/
/**
* @event change
* Fires when navigation back or forwards within the local page's history occurs.
* @param {String} token An identifier associated with the page state at that point
* in its history.
*/
hashRe: /^(#?!?)/,
constructor: function() {
var me = this;
me.ready = false;
me.currentToken = null;
me.mixins.observable.constructor.call(me);
},
/**
* Gets the actual hash from the url. This shouldn't need to be used directly but use the
* {@link #getToken} method instead.
*
* @return {String} The hash from the window object.
* @private
*/
getHash: function() {
return (this.win.location.hash || '').replace(this.hashRe, '');
},
/**
* Updates the hash on the window. This shouldn't need to be used directly but use the
* {@link #add} method instead.
*
* @param {String} hash The hash to use
* @param {Boolean} replace If `true`, the hash passed in will replace the current resource
* by using the `location.replace()` API.
* @private
*/
setHash: function(hash, replace) {
var me = this,
hashRe = me.hashRe,
loc = me.win.location;
// may or may not already be prefixed with # or #! already
hash = hash.replace(hashRe, me.hashbang ? '#!' : '#');
try {
if (replace) {
loc.replace(hash);
}
else {
loc.hash = hash;
}
// need to make sure currentToken is not prefixed
me.currentToken = hash.replace(hashRe, '');
}
catch (e) {
// IE can give Access Denied (esp. in popup windows)
}
},
/**
* Handles when the hash in the URL has been updated. Will also fired the change event.
*
* @param {String} token The token that was changed to
* @private
*/
handleStateChange: function(token) {
// browser won't have # here but may have !
token = token.replace(this.hashRe, '');
this.fireEvent('change', this.currentToken = token);
},
/**
* Bootstraps the initialization the location.hash.
* @private
*/
startUp: function() {
var me = this;
me.currentToken = me.getHash();
Ext.get(me.win).on('hashchange', me.onHashChange, me);
me.ready = true;
me.fireEvent('ready', me);
},
onHashChange: function() {
var me = this,
newHash = me.getHash();
if (newHash !== me.hash) {
me.hash = newHash;
me.handleStateChange(newHash);
}
},
/**
* Initializes the global History instance.
* @param {Function} [onReady] A callback function that will be called once the history
* component is fully initialized.
* @param {Object} [scope] The scope (`this` reference) in which the callback is executed.
* Defaults to the browser window.
*/
init: function(onReady, scope) {
var me = this;
if (me.ready) {
Ext.callback(onReady, scope, [me]);
return;
}
if (!Ext.isReady) {
Ext.onInternalReady(function() {
me.init(onReady, scope);
});
return;
}
me.win = me.useTopWindow ? window.top : window;
me.hash = me.getHash();
if (onReady) {
me.on('ready', onReady, scope, { single: true });
}
me.startUp();
},
/**
* Add a new token to the history stack. This can be any arbitrary value, although it would
* commonly be the concatenation of a component id and another id marking the specific history
* state of that component. Example usage:
*
* // Handle tab changes on a TabPanel
* tabPanel.on('tabchange', function(tabPanel, tab){
* Ext.History.add(tabPanel.id + ':' + tab.id);
* });
*
* @param {String} token The value that defines a particular application-specific history state
* @param {Boolean} [preventDuplicates=true] When true, if the passed token matches the current
* token it will not save a new history step. Set to false if the same state can be saved more
* than once at the same history stack location.
*
* @return {Boolean} Whether the token was set in the case if the current token matches
* the token passed.
*/
add: function(token, preventDuplicates) {
var me = this,
set = false;
if (preventDuplicates === false || me.getToken() !== token) {
me.setHash(token);
set = true;
}
return set;
},
/**
* Replaces the current resource in history.
*
* @param {String} token The value that will replace the current resource in the history state.
* @param {Boolean} [preventDuplicates=true] When `true`, if the passed token matches
* the current token it will not save a new history step. Set to `false` if the same state
* can be saved more than once at the same history stack location.
*
* @return {Boolean} Whether the token was set in the case if the current token matches
* the token passed.
*/
replace: function(token, preventDuplicates) {
var me = this,
set = false;
if (preventDuplicates === false || me.getToken() !== token) {
this.setHash(token, true);
set = true;
}
return set;
},
/**
* Programmatically steps back one step in browser history (equivalent to the user pressing
* the Back button).
*/
back: function() {
this.win.history.go(-1);
},
/**
* Programmatically steps forward one step in browser history (equivalent to the user pressing
* the Forward button).
*/
forward: function() {
this.win.history.go(1);
},
/**
* Retrieves the currently-active history token.
* @return {String} The token
*/
getToken: function() {
return this.ready ? this.currentToken : this.getHash();
}
});