/**
* @class Ext.state.Stateful
* A mixin for being able to save the state of an object to an underlying
* {@link Ext.state.Provider}.
*/
Ext.define('Ext.state.Stateful', {
mixinId: 'state',
requires: [
'Ext.state.Manager',
'Ext.util.TaskRunner'
],
config: {
/**
* @cfg {Boolean/Object} [stateful=false]
* A flag which causes the object to attempt to restore the state of
* internal properties from a saved state on startup. The object must have
* a {@link #stateId} for state to be managed.
*
* Auto-generated ids are not guaranteed to be stable across page loads and
* cannot be relied upon to save and restore the same state for a object.
*
* For state saving to work, the state manager's provider must have been
* set to an implementation of {@link Ext.state.Provider} which overrides the
* {@link Ext.state.Provider#set set} and {@link Ext.state.Provider#get get}
* methods to save and recall name/value pairs. A built-in implementation,
* {@link Ext.state.CookieProvider} is available.
*
* To set the state provider for the current page:
*
* Ext.state.Manager.setProvider(new Ext.state.CookieProvider({
* expires: new Date(new Date().getTime()+(1000*60*60*24*7)), // 7 days from now
* }));
*
* A stateful object attempts to save state when one of the events
* listed in the {@link #stateEvents} configuration fires.
*
* To save state, a stateful object first serializes its state by
* calling *{@link #getState}*.
*
* The Component base class implements {@link #getState} to save its width and
* height within the state only if they were initially configured, and have
* changed from the configured value.
*
* The Panel class saves its collapsed state in addition to that.
*
* The Grid class saves its column state and store state (sorters and filters and grouper)
* in addition to its superclass state.
*
* If there is more application state to be save, the developer must provide
* an implementation which first calls the superclass method to inherit the above behaviour,
* and then injects new properties into the returned object.
*
* The value yielded by `getState` is passed to {@link Ext.state.Manager#set}
* which uses the configured {@link Ext.state.Provider} to save the object
* keyed by the {@link #stateId}.
*
* During construction, a stateful object attempts to *restore* its state by calling
* {@link Ext.state.Manager#get} passing the {@link #stateId}
*
* The resulting object is passed to {@link #applyState}*. The default implementation of
* {@link #applyState} simply copies properties into the object, but a developer may
* override this to support restoration of more complex application state.
*
* You can perform extra processing on state save and restore by attaching
* handlers to the {@link #beforestaterestore}, {@link #staterestore},
* {@link #beforestatesave} and {@link #statesave} events. In some simple cases,
* passing an object for the `stateful` config may suffice. If an object is
* provided, the properties of that object are used to include or exclude stateful
* properties returned by `getState`. For example:
*
* stateful: {
* height: false, // never persist the height
* width: true // always persist the width
* }
*
* The above is roughly equivalent to the following:
*
* getState: function () {
* var state = this.callParent();
*
* delete state.height;
* state.width = this.width;
*
* return state;
* }
*/
stateful: null
},
/**
* @cfg {String} stateId
* The unique id for this object to use for state management purposes.
*
* See {@link #stateful} for an explanation of saving and restoring state.
*/
/**
* @cfg {String[]} stateEvents
* An array of events that, when fired, should trigger this object to
* save its state. `stateEvents` defaults to the `stateEvents` associated with the
* component you are using. Any events you statically set will be appended to that list.
* `stateEvents` may be any type of event supported by this object, including
* browser or custom events (e.g., `['click', 'customerchange']`).
*
* See `{@link #stateful}` for an explanation of saving and
* restoring object state.
*/
/**
* @cfg {Number} saveDelay
* A buffer to be applied if many state events are fired within a short period.
*/
saveDelay: 100,
/**
* @event beforestaterestore
* Fires before the state of the object is restored. Return false from an event handler to stop
* the restore.
* @param {Ext.state.Stateful} this
* @param {Object} state The hash of state values returned from the StateProvider. If this
* event is not vetoed, then the state object is passed to *`applyState`*. By default,
* that simply copies property values into this object. The method maybe overriden to
* provide custom state restoration.
*/
/**
* @event staterestore
* Fires after the state of the object is restored.
* @param {Ext.state.Stateful} this
* @param {Object} state The hash of state values returned from the StateProvider.
* This is passed to *`applyState`*. By default, that simply copies property values into this
* object. The method maybe overridden to provide custom state restoration.
*/
/**
* @event beforestatesave
* Fires before the state of the object is saved to the configured state provider.
* Return false to stop the save.
* @param {Ext.state.Stateful} this
* @param {Object} state The hash of state values. This is determined by calling
* *`getState()`* on the object. This method must be provided by the
* developer to return whatever representation of state is required, by default,
* Ext.state.Stateful has a null implementation.
*/
/**
* @event statesave
* Fires after the state of the object is saved to the configured state provider.
* @param {Ext.state.Stateful} this
* @param {Object} state The hash of state values. This is determined by calling
* *`getState()`* on the object. This method must be provided by the
* developer to return whatever representation of state is required, by default,
* Ext.state.Stateful has a null implementation.
*/
constructor: function() {
var me = this;
if (!me.stateEvents) {
me.stateEvents = [];
}
// NOTE: The stateful config will be initConfig'd before we get here, so the
// stateful property will be correct...
if (me.stateful) {
me.addStateEvents(me.stateEvents);
me.initState();
}
},
/**
* Add events that will trigger the state to be saved. If the first argument is an
* array, each element of that array is the name of a state event. Otherwise, each
* argument passed to this method is the name of a state event.
*
* @param {String/String[]} events The event name or an array of event names.
*/
addStateEvents: function(events) {
var me = this,
i, event, stateEventsByName,
eventArray;
if (me.stateful && me.getStateId()) {
eventArray = (typeof events === 'string') ? arguments : events;
stateEventsByName = me.stateEventsByName || (me.stateEventsByName = {});
for (i = eventArray.length; i--;) {
event = eventArray[i];
if (event && !stateEventsByName[event]) {
stateEventsByName[event] = 1;
me.on(event, me.onStateChange, me);
}
}
}
},
/**
* This method is called when any of the {@link #stateEvents} are fired.
* @private
*/
onStateChange: function() {
var me = this,
delay = me.saveDelay,
statics, runner;
if (!me.stateful) {
return;
}
if (delay) {
if (!me.stateTask) {
statics = Ext.state.Stateful;
runner = statics.runner || (statics.runner = new Ext.util.TaskRunner());
me.stateTask = runner.newTask({
run: me.saveState,
scope: me,
interval: delay,
repeat: 1,
fireIdleEvent: false
});
}
me.stateTask.start();
}
else {
me.saveState();
}
},
/**
* Saves the state of the object to the persistence store.
*/
saveState: function() {
var me = this,
stateful = me.getStateful(),
id = stateful && me.getStateId(),
hasListeners = me.hasListeners,
cfg, configs, plugins,
plugin, i, len, state, pluginState;
if (id) {
state = me.getState() || {}; // pass along for custom interactions
if (Ext.isObject(stateful)) {
configs = me.self.getConfigurator();
configs = configs.configs; // configs[name]: Ext.Config(name)
for (i in stateful) {
if (stateful[i]) {
if (!(i in state)) {
cfg = configs[i]; // if class declared this as a config
state[i] = cfg ? me[cfg.get]() : me[i];
}
}
else {
delete state[i];
}
}
}
/*
* Gather state from those plugins that implement a getState method
*/
plugins = me.getPlugins() || [];
for (i = 0, len = plugins.length; i < len; i++) {
plugin = plugins[i];
if (plugin && plugin.getState) {
pluginState = plugin.getState(state);
if (pluginState && !state[plugin.ptype]) { // first duplicate plugin wins
state[plugin.ptype] = pluginState;
}
}
}
if (!hasListeners.beforestatesave ||
me.fireEvent('beforestatesave', me, state) !== false) {
Ext.state.Manager.set(id, state);
if (hasListeners.statesave) {
me.fireEvent('statesave', me, state);
}
}
}
},
/**
* Gets the current state of the object. By default this function returns null,
* it should be overridden in subclasses to implement methods for getting the state.
* @return {Object} The current state
*/
getState: function() {
return null;
},
/**
* Applies the state to the object. This should be overridden in subclasses to do
* more complex state operations. By default it applies the state properties onto
* the current object.
* @param {Object} state The state
*/
applyState: function(state) {
if (state) {
Ext.apply(this, state);
}
},
/**
* Gets the state id for this object.
* @return {String} The 'stateId' or the implicit 'id' specified by component configuration.
* @private
*/
getStateId: function() {
var me = this;
return me.stateId || (me.autoGenId ? null : me.id);
},
/**
* Initializes the state of the object upon construction.
* @private
*/
initState: function() {
var me = this,
id = me.stateful && me.getStateId(),
hasListeners = me.hasListeners,
state, combinedState, i, len, plugins, plugin, pluginType;
if (id) {
combinedState = Ext.state.Manager.get(id);
if (combinedState) {
state = Ext.apply({}, combinedState);
if (!hasListeners.beforestaterestore ||
me.fireEvent('beforestaterestore', me, combinedState) !== false) {
// Notify all plugins FIRST (if interested) in new state
plugins = me.getPlugins() || [];
for (i = 0, len = plugins.length; i < len; i++) {
plugin = plugins[i];
if (plugin) {
pluginType = plugin.ptype;
if (plugin.applyState) {
plugin.applyState(state[pluginType], combinedState);
}
// clean to prevent unwanted props on the component in final phase
delete state[pluginType];
}
}
me.applyState(state);
if (hasListeners.staterestore) {
me.fireEvent('staterestore', me, combinedState);
}
}
}
}
},
/**
* Conditionally saves a single property from this object to the given state object.
* The idea is to only save state which has changed from the initial state so that
* current software settings do not override future software settings. Only those
* values that are user-changed state should be saved.
*
* @param {String} propName The name of the property to save.
* @param {Object} state The state object in to which to save the property.
* @param {String} stateName (optional) The name to use for the property in state.
* @return {Boolean} True if the property was saved, false if not.
*/
savePropToState: function(propName, state, stateName) {
var me = this,
value = me[propName],
config = me.initialConfig;
if (me.hasOwnProperty(propName)) {
if (!config || config[propName] !== value) {
if (state) {
state[stateName || propName] = value;
}
return true;
}
}
return false;
},
/**
* Gathers additional named properties of the instance and adds their current values
* to the passed state object.
* @param {String/String[]} propNames The name (or array of names) of the property to save.
* @param {Object} state The state object in to which to save the property values.
* @return {Object} state
*/
savePropsToState: function(propNames, state) {
var me = this,
i, n;
if (typeof propNames === 'string') {
me.savePropToState(propNames, state);
}
else {
for (i = 0, n = propNames.length; i < n; ++i) {
me.savePropToState(propNames[i], state);
}
}
return state;
},
/**
* Destroys this stateful object.
*/
destroy: function() {
var task = this.stateTask;
if (task) {
task.destroy();
this.stateTask = null;
}
// No callParent() here, it's a mixin.
}
});