/**
* AbstractStore is a superclass of {@link Ext.data.ProxyStore} and {@link Ext.data.ChainedStore}.
* It's never used directly, but offers a set of methods used by both of those subclasses.
*
* Unless you need to make a whole new type of Store, see {@link Ext.data.Store} instead.
*/
Ext.define('Ext.data.AbstractStore', {
mixins: [
'Ext.mixin.Observable',
'Ext.mixin.Factoryable'
],
requires: [
'Ext.util.Collection',
'Ext.data.Range',
'Ext.data.schema.Schema',
'Ext.util.Filter'
],
factoryConfig: {
defaultType: 'store',
type: 'store'
},
$configPrefixed: false,
$configStrict: false,
config: {
/**
* @cfg {Object[]/Function[]/Ext.util.Collection} filters
* Array of {@link Ext.util.Filter Filters} for this store. Can also be an array
* of functions which will be used as the {@link Ext.util.Filter#filterFn filterFn}
* config for filters:
*
* filters: [
* function(item) {
* return item.weight > 0;
* }
* ]
*
* Individual filters can be specified as an `Ext.util.Filter` instance, a config
* object for `Ext.util.Filter` or simply a function that will be wrapped in a
* instance with its {@link Ext.util.Filter#filterFn filterFn} set.
*
* If a `Collection` of filters is passed, its items (filters) will be added. Any
* subsequent modification to the collection will have no affect.
*
* For fine grain control of the filters collection, call `getFilters` to return
* the `Ext.util.Collection` instance that holds this store's filters.
*
* var filters = store.getFilters(); // an Ext.util.FilterCollection
*
* function legalAge (item) {
* return item.age >= 21;
* }
*
* filters.add(legalAge);
*
* //...
*
* filters.remove(legalAge);
*
* Any changes to the `filters` collection will cause this store to adjust
* its items accordingly.
*/
filters: null,
/**
* @cfg {Boolean} [autoDestroy]
* When a Store is used by only one {@link Ext.view.View DataView}, and should only exist
* for the lifetime of that view, then configure the autoDestroy flag as `true`. This
* causes the destruction of the view to trigger the destruction of its Store.
*/
autoDestroy: undefined,
/**
* @cfg {String} storeId
* Unique identifier for this store. If present, this Store will be registered with the
* {@link Ext.data.StoreManager}, making it easy to reuse elsewhere.
*
* Note that when a store is instantiated by a Controller, the storeId will default
* to the name of the store if not specified in the class.
*/
storeId: null,
/**
* @cfg {Boolean} statefulFilters
* Configure as `true` to have the filters saved when a client {@link Ext.grid.Panel grid}
* saves its state.
*/
statefulFilters: false,
/**
* @cfg {Ext.util.Sorter[]/Object[]} sorters
* The initial set of {@link Ext.util.Sorter Sorters}
*
* Individual sorters can be specified as an `Ext.util.Sorter` instance, a config
* object for `Ext.util.Sorter` or simply the name of a property by which to sort.
*
* An alternative way to extend the sorters is to call the `sort` method and pass
* a property or sorter config to add to the sorters.
*
* For fine grain control of the sorters collection, call `getSorters` to return
* the `Ext.util.Collection` instance that holds this collection's sorters.
*
* var sorters = store.getSorters(); // an Ext.util.SorterCollection
*
* sorters.add('name');
*
* //...
*
* sorters.remove('name');
*
* Any changes to the `sorters` collection will cause this store to adjust
* its items accordingly.
*/
sorters: null,
/**
* @cfg {Boolean} [remoteSort=false]
* `true` if the sorting should be performed on the server side, false if it is local only.
* **Note:** if {@link Ext.data.ProxyStore.html#cfg-autoLoad} is false, you will have to
* explicitly make store load initially before applying sorters. (since 7.5).
*/
remoteSort: {
lazy: true,
$value: false
},
/**
* @cfg {Boolean} [remoteFilter=false]
* `true` to defer any filtering operation to the server. If `false`, filtering is done
* locally on the client.
* **Note:** if {@link Ext.data.ProxyStore.html#cfg-autoLoad} is false, you will have to
* explicitly make store load initially before applying filters. (since 7.5).
*/
remoteFilter: {
lazy: true,
$value: false
},
/**
* @cfg {Boolean} [remoteSummary=false]
* `true` if the summary calculation should be performed on the server side,
* false if it is local only.
* If `true` then all groupers are sent to the server side.
* @since 7.4.0
*/
remoteSummary: {
lazy: true,
$value: false
},
/**
* @cfg {String} groupField
* The field by which to group data in the store. Internally, grouping is very similar to
* sorting - the groupField and {@link #groupDir} are injected as the first sorter
* (see {@link #method-sort}). Stores support a single level of grouping, and groups can be
* fetched via the {@link #getGroups} method.
*/
groupField: undefined,
/**
* @cfg {String} groupDir
* The direction in which sorting should be applied when grouping. Supported values are
* "ASC" and "DESC".
*/
groupDir: 'ASC',
/**
* @cfg {Object/Ext.util.Grouper} grouper
* The grouper by which to group the data store. May also be specified by the
* {@link #groupField} config, however
* they should not be used together.
*/
grouper: null,
/**
* @cfg {Ext.util.GrouperCollection} groupers
* The initial set of {@link Ext.util.Grouper Groupers}
* @since 7.4.0
*/
groupers: undefined,
/**
* @cfg {Number} pageSize
* The number of records considered to form a 'page'. This is used to power the built-in
* paging using the nextPage and previousPage functions when the grid is paged using a
* {@link Ext.toolbar.Paging PagingToolbar} Defaults to 25.
*
* To disable paging, set the pageSize to `0`.
*/
pageSize: 25,
/**
* @cfg {Boolean} [autoSort=true] `true` to maintain sorted order when records
* are added regardless of requested insertion point, or when an item mutation
* results in a new sort position.
*
* This does not affect a ChainedStore's reaction to mutations of the source
* Store. If sorters are present when the source Store is mutated, this ChainedStore's
* sort order will always be maintained.
* @private
*/
autoSort: null,
/**
* @cfg {Boolean} reloadOnClearSorters
* Set this to `true` to trigger a reload when the last sorter is removed (only
* applicable when {@link #cfg!remoteSort} is `true`).
*
* By default, the store reloads itself when a sorter is added or removed.
*
* When the last sorter is removed, however, the assumption is that the data
* does not need to become "unsorted", and so no reload is triggered.
*
* If the server has a default order to which it reverts in the absence of any
* sorters, then it is useful to set this config to `true`.
* @since 6.5.1
*/
reloadOnClearSorters: false,
/**
* @cfg {Boolean} autoLoadOnFilterEnd
* Set this to `true` to trigger store load on filter end.
*
* @private
*/
autoLoadOnFilterEnd: undefined
},
/**
* @property {Number} currentPage
* The page that the Store has most recently loaded
* (see {@link Ext.data.Store#loadPage loadPage})
*/
currentPage: 1,
/**
* @property {Boolean} loading
* `true` if the Store is currently loading via its Proxy.
* @private
*/
loading: false,
/**
* @property {Boolean} isStore
* `true` in this class to identify an object as an instantiated Store, or subclass thereof.
*/
isStore: true,
/**
* @property {Number} updating
* A counter that is increased by `beginUpdate` and decreased by `endUpdate`. When
* this transitions from 0 to 1 the `{@link #event-beginupdate beginupdate}` event is
* fired. When it transitions back from 1 to 0 the `{@link #event-endupdate endupdate}`
* event is fired.
* @readonly
* @since 5.0.0
*/
updating: 0,
observerPriority: 0,
constructor: function(config) {
var me = this,
storeId;
//<debug>
me.callParent([config]);
//</debug>
/**
* @event add
* Fired when a Model instance has been added to this Store.
*
* @param {Ext.data.Store} store The store.
* @param {Ext.data.Model[]} records The records that were added.
* @param {Number} index The index at which the records were inserted.
* @since 1.1.0
*/
/**
* @event remove
* Fired when one or more records have been removed from this Store.
*
* **The signature for this event has changed in 5.0:**
*
* @param {Ext.data.Store} store The Store object
* @param {Ext.data.Model[]} records The records that were removed. In previous
* releases this was a single record, not an array.
* @param {Number} index The index at which the records were removed.
* @param {Boolean} isMove `true` if the child node is being removed so it can be
* moved to another position in this Store.
* @since 5.0.0
*/
/**
* @event update
* Fires when a Model instance has been updated.
* @param {Ext.data.Store} this
* @param {Ext.data.Model} record The Model instance that was updated
* @param {String} operation The update operation being performed. Value may be one of:
*
* Ext.data.Model.EDIT
* Ext.data.Model.REJECT
* Ext.data.Model.COMMIT
* @param {String[]} modifiedFieldNames Array of field names changed during edit.
* @param {Object} details An object describing the change. See the
* {@link Ext.util.Collection#event-itemchange itemchange event} of the store's backing
* collection
* @since 1.1.0
*/
/**
* @event clear
* Fired after the {@link Ext.data.Store#removeAll removeAll} method is called.
* @param {Ext.data.Store} this
* @since 1.1.0
*/
/**
* @event datachanged
* Fires for any data change in the store. This is a catch-all event that is typically fired
* in conjunction with other events (such as `add`, `remove`, `update`, `refresh`).
* @param {Ext.data.Store} this The data store
* @since 1.1.0
*/
/**
* @event refresh
* Fires when the data cache has changed in a bulk manner (e.g., it has been sorted,
* filtered, etc.) and a widget that is using this Store as a Record cache should refresh
* its view.
* @param {Ext.data.Store} this The data store
*/
/**
* @event beginupdate
* Fires when the {@link #beginUpdate} method is called. Automatic synchronization as
* configured by the {@link Ext.data.ProxyStore#autoSync autoSync} flag is deferred until
* the {@link #endUpdate} method is called, so multiple mutations can be coalesced into one
* synchronization operation.
*/
/**
* @event endupdate
* Fires when the {@link #endUpdate} method is called. Automatic synchronization as
* configured by the {@link Ext.data.ProxyStore#autoSync autoSync} flag is deferred until
* the {@link #endUpdate} method is called, so multiple mutations can be coalesced into one
* synchronization operation.
*/
/**
* @event beforesort
* Fires before a store is sorted.
*
* For {@link #remoteSort remotely sorted} stores, this will be just before the load
* operation triggered by changing the store's sorters.
*
* For locally sorted stores, this will be just before the data items in the store's
* backing collection are sorted.
* @param {Ext.data.Store} store The store being sorted
* @param {Ext.util.Sorter[]} sorters Array of sorters applied to the store
*/
/**
* @event sort
* Fires after a store is sorted.
*
* For {@link #remoteSort remotely sorted} stores, this will be upon the success of a load
* operation triggered by changing the store's sorters.
*
* For locally sorted stores, this will be just after the data items in the store's backing
* collection are sorted.
* @param {Ext.data.Store} store The store being sorted
*/
me.isInitializing = true;
me.mixins.observable.constructor.call(me, config);
me.isInitializing = false;
storeId = me.getStoreId();
if (!storeId && (config && config.id)) {
me.setStoreId(storeId = config.id);
}
if (storeId) {
Ext.data.StoreManager.register(me);
}
},
/**
* Create a `Range` instance to access records by their index.
*
* @param {Object/Ext.data.Range} [config]
* @return {Ext.data.Range}
* @since 6.5.0
*/
createActiveRange: function(config) {
var range = Ext.apply({ store: this }, config);
return new Ext.data.Range(range);
},
/**
* @private
* Called from onCollectionItemsAdd. Collection add changes the items reference of the
* collection, and that array object if directly referenced by Ranges. The ranges
* have to refresh themselves upon add.
*/
syncActiveRanges: function() {
var activeRanges = this.activeRanges,
len = activeRanges && activeRanges.length,
i;
for (i = 0; i < len; i++) {
activeRanges[i].refresh();
}
},
/**
* Gets the number of records in store.
*
* If using paging, this may not be the total size of the dataset. If the data object used by
* the Reader contains the dataset size, then the {@link Ext.data.ProxyStore#getTotalCount}
* function returns the dataset size. **Note**: see the Important note in
* {@link Ext.data.ProxyStore#method-load}.
*
* When store is filtered, it's the number of records matching the filter.
*
* @return {Number} The number of Records in the Store.
*/
getCount: function() {
var data = this.getData();
// We may be destroyed, in which case "data" will be null... best to just
// report 0 items vs throw an exception
return data ? data.getCount() : 0;
},
/**
* Determines if the passed range is available in the page cache.
* @private
* @param {Number} start The start index
* @param {Number} end The end index in the range
*/
rangeCached: function(start, end) {
return this.getData().getCount() >= Math.max(start, end);
},
/**
* Checks if a record is in the current active data set.
* @param {Ext.data.Model} record The record
* @return {Boolean} `true` if the record is in the current active data set.
* @method contains
*/
/**
* Finds the index of the first matching Record in this store by a specific field value.
*
* When store is filtered, finds records only within filter.
*
* **IMPORTANT**
*
* **If this store is {@link Ext.data.BufferedStore Buffered}, this can ONLY find records
* which happen to be cached in the page cache. This will be parts of the dataset around the
* currently visible zone, or recently visited zones if the pages have not yet been purged from
* the cache.**
*
* @param {String} property The name of the Record field to test.
* @param {String/RegExp} value Either a string that the field value
* should begin with, or a RegExp to test against the field.
* @param {Number} [startIndex=0] The index to start searching at
* @param {Boolean} [anyMatch=false] True to match any part of the string, not just the
* beginning.
* @param {Boolean} [caseSensitive=false] True for case sensitive comparison
* @param {Boolean} [exactMatch=false] True to force exact match (^ and $ characters
* added to the regex). Ignored if `anyMatch` is `true`.
* @return {Number} The matched index or -1
*/
find: function(property, value, startIndex, anyMatch, caseSensitive, exactMatch) {
// exactMatch
// anyMatch F T
// F ^abc ^abc$
// T abc abc
//
var startsWith = !anyMatch,
endsWith = !!(startsWith && exactMatch);
return this.getData().findIndex(property, value, startIndex, startsWith, endsWith,
!caseSensitive);
},
/**
* Finds the first matching Record in this store by a specific field value.
*
* When store is filtered, finds records only within filter.
*
* **IMPORTANT**
*
* **If this store is {@link Ext.data.BufferedStore Buffered}, this can ONLY find records which
* happen to be cached in the page cache. This will be parts of the dataset around the
* currently visible zone, or recently visited zones if the pages have not yet been purged
* from the cache.**
*
* @param {String} fieldName The name of the Record field to test.
* @param {String/RegExp} value Either a string that the field value
* should begin with, or a RegExp to test against the field.
* @param {Number} [startIndex=0] The index to start searching at
* @param {Boolean} [anyMatch=false] True to match any part of the string, not just the
* beginning.
* @param {Boolean} [caseSensitive=false] True for case sensitive comparison
* @param {Boolean} [exactMatch=false] True to force exact match (^ and $ characters
* added to the regex). Ignored if `anyMatch` is `true`.
* @return {Ext.data.Model} The matched record or null
*/
findRecord: function() {
var me = this,
index = me.find.apply(me, arguments);
return index !== -1 ? me.getAt(index) : null;
},
/**
* Finds the index of the first matching Record in this store by a specific field value.
*
* When store is filtered, finds records only within filter.
*
* **IMPORTANT**
*
* **If this store is {@link Ext.data.BufferedStore Buffered}, this can ONLY find records which
* happen to be cached in the page cache. This will be parts of the dataset around the
* currently visible zone, or recently visited zones if the pages have not yet been purged
* from the cache.**
*
* @param {String} fieldName The name of the Record field to test.
* @param {Object} value The value to match the field against.
* @param {Number} [startIndex=0] The index to start searching at
* @return {Number} The matched index or -1
*/
findExact: function(fieldName, value, startIndex) {
return this.getData().findIndexBy(function(rec) {
return rec.isEqual(rec.get(fieldName), value);
}, this, startIndex);
},
/**
* Find the index of the first matching Record in this Store by a function.
* If the function returns `true` it is considered a match.
*
* When store is filtered, finds records only within filter.
*
* **IMPORTANT**
*
* **If this store is {@link Ext.data.BufferedStore Buffered}, this can ONLY find records which
* happen to be cached in the page cache. This will be parts of the dataset around the
* currently visible zone, or recently visited zones if the pages have not yet been purged
* from the cache.**
*
* @param {Function} fn The function to be called. It will be passed the following parameters:
* @param {Ext.data.Model} fn.record The record to test for filtering. Access field values
* using {@link Ext.data.Model#get}.
* @param {Object} fn.id The ID of the Record passed.
* @param {Object} [scope] The scope (this reference) in which the function is executed.
* Defaults to this Store.
* @param {Number} [start=0] The index at which to start searching.
* @return {Number} The matched index or -1
*/
findBy: function(fn, scope, start) {
return this.getData().findIndexBy(fn, scope, start);
},
/**
* Get the Record at the specified index.
*
* The index is effected by filtering.
*
* @param {Number} index The index of the Record to find.
* @return {Ext.data.Model} The Record at the passed index. Returns null if not found.
*/
getAt: function(index) {
return this.getData().getAt(index) || null;
},
/**
* Gathers a range of Records between specified indices.
*
* This method is affected by filtering.
*
* @param {Number} start The starting index. Defaults to zero.
* @param {Number} end The ending index. Defaults to the last record. The end index
* **is included**.
* @param [options] (private) Used by BufferedRenderer when using a BufferedStore.
* @return {Ext.data.Model[]} An array of records.
*/
getRange: function(start, end, options) {
// Collection's getRange is exclusive. Do NOT mutate the value: it is passed to the
// callback.
var result = this.getData().getRange(start, Ext.isNumber(end) ? end + 1 : end);
// BufferedRenderer requests a range with a callback to process that range.
// Because it may be dealing with a buffered store and the range may not be available
// synchronously.
if (options && options.callback) {
options.callback.call(options.scope || this, result, start, end, options);
}
return result;
},
/**
* Gets the filters for this store.
* @param {Boolean} [autoCreate] (private)
* @return {Ext.util.FilterCollection} The filters
*/
getFilters: function(autoCreate) {
var me = this,
result = me.callParent();
if (!result && autoCreate !== false) {
me.setFilters([]);
result = me.callParent();
}
return result;
},
applyFilters: function(filters, filtersCollection) {
var me = this,
created = !filtersCollection;
if (created) {
filtersCollection = me.createFiltersCollection();
}
if (filters !== filtersCollection) {
if (filters && filters.isCollection) {
filters = filters.items.slice();
}
filtersCollection.add(filters);
if (created) {
me.onRemoteFilterSet(filtersCollection, me.getRemoteFilter());
}
}
return filtersCollection;
},
/**
* Gets the sorters for this store.
* @param {Boolean} [autoCreate] (private)
* @return {Ext.util.SorterCollection} The sorters
*/
getSorters: function(autoCreate) {
var me = this,
result = me.callParent();
if (!result && autoCreate !== false) {
// If not preventing creation, force it here
me.setSorters([]);
result = me.callParent();
}
return result;
},
applySorters: function(sorters, sortersCollection) {
var me = this,
created;
if (!sortersCollection) {
sortersCollection = me.createSortersCollection();
created = true;
}
sortersCollection.add(sorters);
if (created) {
me.onRemoteSortSet(sortersCollection, me.getRemoteSort());
}
return sortersCollection;
},
/**
* Filters the data in the Store by one or more fields. Example usage:
*
* //filter with a single field
* myStore.filter('firstName', 'Don');
*
* //filtering with multiple filters
* myStore.filter([
* {
* property : 'firstName',
* value : 'Don'
* },
* {
* property : 'lastName',
* value : 'Griffin'
* }
* ]);
*
* Internally, Store converts the passed arguments into an array of
* {@link Ext.util.Filter} instances, and delegates the actual filtering to its internal
* {@link Ext.util.Collection} or the remote server.
*
* @param {String/Ext.util.Filter[]} [filters] Either a string name of one of the
* fields in this Store's configured {@link Ext.data.Model Model}, or an array of
* filter configurations.
* @param {String} [value] The property value by which to filter. Only applicable if
* `filters` is a string.
* @param {Boolean} [suppressEvent] (private)
*/
filter: function(filters, value, suppressEvent) {
if (Ext.isString(filters)) {
filters = {
property: filters,
value: value
};
}
this.suppressNextFilter = !!suppressEvent;
this.getFilters().add(filters);
this.suppressNextFilter = false;
},
/**
* Removes an individual Filter from the current {@link #cfg-filters filter set}
* using the passed Filter/Filter id and by default, applies the updated filter set
* to the Store's unfiltered dataset.
*
* @param {String/Ext.util.Filter} toRemove The id of a Filter to remove from the
* filter set, or a Filter instance to remove.
* @param {Boolean} [suppressEvent] If `true` the filter is cleared silently.
*/
removeFilter: function(toRemove, suppressEvent) {
var me = this,
filters = me.getFilters();
me.suppressNextFilter = !!suppressEvent;
if (toRemove instanceof Ext.util.Filter) {
filters.remove(toRemove);
}
else {
filters.removeByKey(toRemove);
}
me.suppressNextFilter = false;
},
updateAutoSort: function(autoSort) {
// Keep collection synced with our autoSort setting
this.getData().setAutoSort(autoSort);
},
updateRemoteSort: function(remoteSort) {
// Don't call the getter here, we don't want to force sorters to be created here.
// Also, applySorters calls getRemoteSort, which may trigger the initGetter.
this.onRemoteSortSet(this.getSorters(false), remoteSort);
},
updateRemoteFilter: function(remoteFilter) {
this.onRemoteFilterSet(this.getFilters(false), remoteFilter);
},
/**
* Adds a new Filter to this Store's {@link #cfg-filters filter set} and
* by default, applies the updated filter set to the Store's unfiltered dataset.
* @param {Object[]/Ext.util.Filter[]} filters The set of filters to add to the current
* {@link #cfg-filters filter set}.
* @param {Boolean} [suppressEvent] If `true` the filter is cleared silently.
*/
addFilter: function(filters, suppressEvent) {
this.suppressNextFilter = !!suppressEvent;
this.getFilters().add(filters);
this.suppressNextFilter = false;
},
/**
* Filters by a function. The specified function will be called for each
* Record in this Store. If the function returns `true` the Record is included,
* otherwise it is filtered out.
*
* When store is filtered, most of the methods for accessing store data will be working only
* within the set of filtered records. The notable exception is {@link #getById}.
*
* @param {Function} fn The function to be called. It will be passed the following parameters:
* @param {Ext.data.Model} fn.record The record to test for filtering. Access field values
* using {@link Ext.data.Model#get}.
* @param {Object} [scope] The scope (this reference) in which the function is executed.
* Defaults to this Store.
*/
filterBy: function(fn, scope) {
this.getFilters().add({
filterFn: fn,
scope: scope || this
});
},
/**
* Reverts to a view of the Record cache with no filtering applied.
* @param {Boolean} [suppressEvent] If `true` the filter is cleared silently.
*
* For a locally filtered Store, this means that the filter collection is cleared without
* firing the {@link #datachanged} event.
*
* For a remotely filtered Store, this means that the filter collection is cleared, but
* the store is not reloaded from the server.
*/
clearFilter: function(suppressEvent) {
var me = this,
filters = me.getFilters(false);
if (!filters || filters.getCount() === 0) {
return;
}
me.suppressNextFilter = !!suppressEvent;
filters.removeAll();
me.suppressNextFilter = false;
},
/**
* Tests whether the store currently has any active filters.
* @return {Boolean} `true` if the store is filtered.
*/
isFiltered: function() {
return this.getFilters().getCount() > 0;
},
/**
* Tests whether the store currently has any active sorters.
* @return {Boolean} `true` if the store is sorted.
*/
isSorted: function() {
var sorters = this.getSorters(false);
return !!(sorters && sorters.length > 0) || this.isGrouped();
},
addFieldTransform: function(sorter) {
// Transform already specified, leave it
if (sorter.getTransform()) {
return;
}
/* eslint-disable-next-line vars-on-top */
var fieldName = sorter.getProperty(),
Model = this.getModel(),
field, sortType;
if (Model) {
field = Model.getField(fieldName);
sortType = field ? field.getSortType() : null;
}
if (sortType && sortType !== Ext.identityFn) {
sorter.setTransform(sortType);
}
},
/**
* This method may be called to indicate the start of multiple changes to the store.
*
* Automatic synchronization as configured by the {@link Ext.data.ProxyStore#autoSync autoSync}
* flag is deferred until the {@link #endUpdate} method is called, so multiple mutations can be
* coalesced into one synchronization operation.
*
* Internally this method increments a counter that is decremented by `endUpdate`. It
* is important, therefore, that if you call `beginUpdate` directly you match that
* call with a call to `endUpdate` or you will prevent the collection from updating
* properly.
*
* For example:
*
* var store = Ext.StoreManager.lookup({
* //...
* autoSync: true
* });
*
* store.beginUpdate();
*
* record.set('fieldName', 'newValue');
*
* store.add(item);
* // ...
*
* store.insert(index, otherItem);
* //...
*
* // Interested parties will listen for the endupdate event
* store.endUpdate();
*
* @since 5.0.0
*/
beginUpdate: function() {
if (!this.updating++ && this.hasListeners.beginupdate) {
this.fireEvent('beginupdate');
}
},
/**
* This method is called after modifications are complete on a store. For details
* see `{@link #beginUpdate}`.
* @since 5.0.0
*/
endUpdate: function() {
if (this.updating && ! --this.updating) {
if (this.hasListeners.endupdate) {
this.fireEvent('endupdate');
}
this.onEndUpdate();
}
},
/**
* @private
* Returns the grouping, sorting and filtered state of this Store.
*/
getState: function() {
var me = this,
sorters = [],
filters = me.getFilters(),
grouper = me.getGrouper(),
groupers = [],
storeGroupers = me.getGroupers(false),
filterState, hasState, result;
// Create sorters config array.
me.getSorters().each(function(s) {
sorters[sorters.length] = s.getState();
hasState = true;
});
// Because we do not provide a filter changing mechanism, only statify the filters if they
// opt in. Otherwise filters would get "stuck".
if (me.statefulFilters && me.saveStatefulFilters) {
// If saveStatefulFilters is turned on then we know that the filter collection has
// changed since page load. Initiate the filterState as an empty stack, which is
// meaningful in itself. If there are any filter in the collection, persist them.
hasState = true;
filterState = [];
filters.each(function(f) {
filterState[filterState.length] = f.getState();
});
}
if (grouper) {
hasState = true;
}
else if (storeGroupers) {
storeGroupers.each(function(g) {
groupers[groupers.length] = g.getState();
hasState = true;
});
}
// If there is any state to save, return it as an object
if (hasState) {
result = {};
if (sorters.length) {
result.sorters = sorters;
}
if (filterState) {
result.filters = filterState;
}
if (grouper) {
result.grouper = grouper.getState();
}
else if (groupers.length) {
result.groupers = groupers;
}
}
return result;
},
/**
* @private
* Restores state to the passed state
*/
applyState: function(state) {
var me = this,
stateSorters = state.sorters,
stateFilters = state.filters,
stateGrouper = state.grouper,
stateGroupers = state.groupers;
if (stateSorters) {
me.getSorters().replaceAll(stateSorters);
}
if (stateFilters) {
// We found persisted filters so let's save stateful filters from this point forward.
me.saveStatefulFilters = true;
me.getFilters().replaceAll(stateFilters);
}
if (stateGrouper) {
me.setGrouper(stateGrouper);
}
else if (stateGroupers) {
me.setGroupers(stateGroupers);
}
},
/**
* Get the Record with the specified id.
*
* This method is not affected by filtering, lookup will be performed from all records
* inside the store, filtered or not.
*
* @param {Mixed} id The id of the Record to find.
* @return {Ext.data.Model} The Record with the passed id. Returns null if not found.
* @method getById
*/
/**
* Returns true if the store has a pending load task.
* @return {Boolean} `true` if the store has a pending load task.
* @private
* @method
*/
hasPendingLoad: Ext.emptyFn,
/**
* Returns `true` if the Store has been loaded.
* @return {Boolean} `true` if the Store has been loaded.
* @method
*/
isLoaded: Ext.emptyFn,
/**
* Returns `true` if the Store is currently performing a load operation.
* @return {Boolean} `true` if the Store is currently loading.
* @method
*/
isLoading: Ext.emptyFn,
destroy: function() {
var me = this;
if (me.hasListeners.beforedestroy) {
me.fireEvent('beforedestroy', me);
}
me.destroying = true;
if (me.getStoreId()) {
Ext.data.StoreManager.unregister(me);
}
me.doDestroy();
if (me.hasListeners.destroy) {
me.fireEvent('destroy', me);
}
// This just makes it hard to ask "was destroy() called?":
// me.destroying = false; // removed in 7.0
// This will finish the sequence and null object references
me.callParent();
},
/**
* Perform the Store destroying sequence. Override this method to add destruction
* behaviors to your custom Stores.
*
*/
doDestroy: Ext.emptyFn,
/**
* Sorts the data in the Store by one or more of its properties. Example usage:
*
* //sort by a single field
* myStore.sort('myField', 'DESC');
*
* //sorting by multiple fields
* myStore.sort([
* {
* property : 'age',
* direction: 'ASC'
* },
* {
* property : 'name',
* direction: 'DESC'
* }
* ]);
*
* Internally, Store converts the passed arguments into an array of {@link Ext.util.Sorter}
* instances, and either delegates the actual sorting to its internal
* {@link Ext.util.Collection} or the remote server.
*
* When passing a single string argument to sort, Store maintains a ASC/DESC toggler per field,
* so this code:
*
* store.sort('myField');
* store.sort('myField');
*
* Is equivalent to this code, because Store handles the toggling automatically:
*
* store.sort('myField', 'ASC');
* store.sort('myField', 'DESC');
*
* @param {String/Ext.util.Sorter[]} [field] Either a string name of one of the
* fields in this Store's configured {@link Ext.data.Model Model}, or an array of
* sorter configurations.
* @param {"ASC"/"DESC"} [direction="ASC"] The overall direction to sort the data by.
* @param {"append"/"prepend"/"replace"/"multi"} [mode="replace"]
*/
sort: function(field, direction, mode) {
var me = this;
if (arguments.length === 0) {
if (me.getRemoteSort()) {
me.load();
}
else {
me.forceLocalSort();
}
}
else {
me.getSorters().addSort(field, direction, mode);
}
},
// This is attached to the data Collection's beforesort event only if not remoteSort
// If remoteSort, the event is fired before the reload call in Ext.data.ProxyStore#load.
onBeforeCollectionSort: function(store, sorters) {
if (sorters) {
this.fireEvent('beforesort', this, sorters.getRange());
}
},
onSorterEndUpdate: function() {
var me = this,
fireSort = true,
sorters = me.getSorters(false),
sorterCount;
// If we're in the middle of grouping, it will take care of loading.
// If the collection is not instantiated yet, it's because we are constructing.
if (me.settingGroups || !sorters) {
return;
}
sorters = sorters.getRange();
sorterCount = sorters.length;
if (me.getRemoteSort()) {
// Only reload if there are sorters left to influence the sort order.
// Unless reloadOnClearSorters is set to indicate that there's a default
// order used by the server which must be returned to when there is no
// explicit sort order.
if ((me.isLoaded() || me.getAutoLoad()) &&
(sorters.length || me.getReloadOnClearSorters())) {
// The sort event will fire in the load callback;
fireSort = false;
me.load({
callback: function() {
me.fireEvent('sort', me, sorters);
}
});
}
}
else if (sorterCount) {
me.fireEvent('datachanged', me);
me.fireEvent('refresh', me);
}
if (fireSort) {
// Sort event must fire when sorters collection is updated to empty.
me.fireEvent('sort', me, sorters);
}
},
onFilterEndUpdate: function() {
var me = this,
suppressNext = me.suppressNextFilter,
filters = me.getFilters(false);
// If the collection is not instantiated yet, it's because we are constructing.
if (!filters) {
return;
}
if (me.getRemoteFilter()) {
//<debug>
me.getFilters().each(function(filter) {
if (filter.getInitialConfig().filterFn) {
Ext.raise('Unable to use a filtering function in conjunction with ' +
'remote filtering.');
}
});
//</debug>
me.currentPage = 1;
if (!suppressNext && (me.isLoaded() || me.getAutoLoad() ||
me.getAutoLoadOnFilterEnd())) {
me.load();
}
}
else if (!suppressNext) {
me.fireEvent('datachanged', me);
me.fireEvent('refresh', me);
}
if (me.trackStateChanges) {
// We just mutated the filter collection so let's save stateful filters
// from this point forward.
me.saveStatefulFilters = true;
}
// This is not affected by suppressEvent.
me.fireEvent('filterchange', me, me.getFilters().getRange());
},
updateGroupField: function(field) {
if (field) {
this.setGrouper({
property: field,
direction: this.getGroupDir()
});
}
else {
this.setGrouper(null);
}
},
/**
* @method setFilters
*/
/**
* @method setSorters
*/
getGrouper: function() {
return this.getData().getGrouper();
},
setGrouper: function(grouper) {
var me = this,
group = !me.isConfiguring || (me.isConfiguring && grouper);
if (group) {
me.usesGroupers = false;
me.group(grouper);
}
},
getGroupers: function(autoCreate) {
var data = this.getData();
return (data && data.isCollection) ? data.getGroupers(autoCreate) : null;
},
setGroupers: function(groupers) {
var me = this,
group = !me.isConfiguring || (me.isConfiguring && groupers);
if (group) {
me.usesGroupers = true;
me.group(groupers);
}
},
/**
* Groups data inside the store.
* @param {String/Object[]} groupers Either a string name of one of the fields in this Store's
* configured {@link Ext.data.Model Model}, or an object, or a {@link Ext.util.Grouper grouper}
* configuration object.
* @param {String} [direction] The overall direction to group the data by. Defaults to the
* value of {@link #groupDir}.
*/
group: function(groupers, direction) {
var me = this,
data = me.getData(),
colGroupers, grouper, newGroupers;
if (me.usesGroupers) {
me.fireEvent('beforegroupschange', me);
}
me.settingGroups = true;
colGroupers = data.getGroupers();
// If we were passed groupers, we replace the existing groupers in the sorter collection
// with the new ones
if (groupers) {
if (Ext.isArray(groupers)) {
newGroupers = groupers;
}
else if (Ext.isObject(groupers)) {
newGroupers = [groupers];
}
else if (Ext.isString(groupers)) {
grouper = colGroupers.get(groupers);
if (!grouper) {
grouper = {
property: groupers,
direction: direction || me.getGroupDir()
};
newGroupers = [grouper];
}
else if (direction === undefined) {
grouper.toggle();
}
else {
grouper.setDirection(direction);
}
}
// If we were passed groupers, replace our grouper collection
if (newGroupers && newGroupers.length) {
colGroupers.replaceAll(newGroupers);
}
}
else {
data.setGrouper(null);
data.setGroupers(null);
}
delete me.settingGroups;
},
fireGroupChange: function(grouper) {
var me = this;
if (!me.isConfiguring && !me.destroying && !me.destroyed) {
if (me.usesGroupers) {
me.fireEvent('groupschange', me, me.getGroupers(false));
}
else {
me.fireGroupChangeEvent(grouper || me.getGrouper());
}
}
},
fireGroupChangeEvent: function(grouper) {
this.fireEvent('groupchange', this, grouper);
},
/**
* Clear the store grouping
*/
clearGrouping: function() {
this.group(null);
},
getGroupField: function() {
var groupers = this.getGroupers(false),
group = '',
grouper;
if (groupers && groupers.length) {
group = groupers.getAt(0).getProperty();
}
else {
grouper = this.getGrouper();
if (grouper) {
group = grouper.getProperty();
}
}
return group;
},
/**
* Tests whether the store currently has an active grouper.
* @return {Boolean} `true` if the store is grouped.
*/
isGrouped: function() {
var groupers = this.getGroupers(false);
return !!(groupers && groupers.length > 0);
},
/**
* Returns a collection of readonly sub-collections of your store's records
* with grouping applied. These sub-collections are maintained internally by
* the collection.
*
* See {@link #groupField}, {@link #groupDir}. Example for a store
* containing records with a color field:
*
* var myStore = Ext.create('Ext.data.Store', {
* groupField: 'color',
* groupDir : 'DESC'
* });
*
* myStore.getGroups();
*
* The above should result in the following format:
*
* [
* {
* name: 'yellow',
* children: [
* // all records where the color field is 'yellow'
* ]
* },
* {
* name: 'red',
* children: [
* // all records where the color field is 'red'
* ]
* }
* ]
*
* Group contents are affected by filtering.
*
* @return {Ext.util.Collection} The grouped data
*/
getGroups: function() {
return this.getData().getGroups();
},
onGroupersEndUpdate: function() {
var me = this,
data = me.getData(),
groupers = data.getGroupers(false),
sorters = me.getSorters(false);
// we need to monitor the groupers in case their direction changes
if (groupers) {
groupers.addGroupersObserver(me);
}
if ((groupers && groupers.length) || (sorters && sorters.length)) {
if (me.getRemoteSort()) {
if (!me.isInitializing) {
me.load({
scope: me,
callback: function() {
me.fireGroupChange(); // do not pass on args
}
});
}
}
else {
me.fireEvent('datachanged', me);
me.fireEvent('refresh', me);
me.fireGroupChange();
}
}
else {
me.fireGroupChange();
}
},
onGrouperDirectionChange: function() {
var me = this;
me.fireEvent('beforegroupschange', me);
if (me.getRemoteSort()) {
if (!me.isInitializing) {
me.load({
scope: me,
callback: function() {
me.fireGroupChange(); // do not pass on args
}
});
}
}
else {
Ext.asap(me.delayedDirectionChange, me);
}
},
delayedDirectionChange: function() {
if (this.destroyed) {
return;
}
this.fireGroupChange();
},
onEndUpdate: Ext.emptyFn,
privates: {
_metaProperties: {
count: 'getCount',
first: 'first',
last: 'last',
loading: 'hasPendingLoad',
totalCount: 'getTotalCount'
},
interpret: function(name) {
var me = this,
accessor = me._metaProperties[name];
return accessor && me[accessor](); // e.g., me.getCount()
},
loadsSynchronously: Ext.privateFn,
onRemoteFilterSet: function(filters, remoteFilter) {
if (filters) {
filters[remoteFilter ? 'on' : 'un']('endupdate', 'onFilterEndUpdate', this);
}
},
// If remoteSort is set, we react to the endUpdate of the sorters Collection by reloading
// if there are still some sorters, or we're configured to reload on sorter remove.
// If remoteSort is set, we do not need to listen for the data Collection's beforesort
// event.
//
// If local sorting, we do not need to react to the endUpdate of the sorters Collection.
// If local sorting, we listen for the data Collection's beforesort event to fire our
// beforesort event.
onRemoteSortSet: function(sorters, remoteSort) {
var me = this,
data;
if (sorters) {
sorters[remoteSort ? 'on' : 'un']('endupdate', 'onSorterEndUpdate', me);
data = me.getData();
if (data) {
data[remoteSort ? 'un' : 'on']('beforesort', 'onBeforeCollectionSort', me);
}
}
}
},
deprecated: {
5: {
methods: {
destroyStore: function() {
this.destroy();
}
}
}
}
});