/**
* A mixin that provides common store methods for Ext.data.Store & Ext.data.ChainedStore.
* @private
*/
Ext.define('Ext.data.LocalStore', {
extend: 'Ext.Mixin',
requires: ['Ext.data.Group'],
mixinConfig: {
id: 'localstore',
after: {
fireGroupChangeEvent: 'onGrouperChange'
}
},
config: {
extraKeys: null
},
applyExtraKeys: function(extraKeys) {
var indexName,
data = this.getData();
// Add the extra keys to the data collection
data.setExtraKeys(extraKeys);
// Pluck the extra keys out so that we can keep them by index name
extraKeys = data.getExtraKeys();
for (indexName in extraKeys) {
this[indexName] = extraKeys[indexName];
}
},
/**
* Adds Model instance to the Store. This method accepts either:
*
* - An array of Model instances or Model configuration objects.
* - Any number of Model instance or Model configuration object arguments.
*
* The new Model instances will be added at the end of the existing collection.
*
* Sample usage:
*
* myStore.add({some: 'data'}, {some: 'other data'});
*
* Note that if this Store is sorted, the new Model instances will be inserted
* at the correct point in the Store to maintain the sort order.
*
* @param {Ext.data.Model[]/Ext.data.Model.../Object[]/Object...} record An array of
* records or configuration objects, or variable number of record or config arguments.
* @return {Ext.data.Model[]} The record instances that were added.
*/
add: function(record) {
return this.insert(this.getCount(), arguments.length === 1 ? record : arguments);
},
constructDataCollection: function() {
var result = new Ext.util.Collection({
//<debug>
id: this.getId() + '-data',
//</debug>
rootProperty: 'data',
groupConfig: {
xclass: 'Ext.data.Group',
store: this
}
});
// Add this store as an observer immediately so that we are informed of any
// synchronous autoLoad which may occur in this event.
result.addObserver(this);
return result;
},
/**
* Converts a literal to a model, if it's not a model already
* @private
* @param {Ext.data.Model/Object} record The record to create
* @return {Ext.data.Model}
*/
createModel: function(record) {
var session = this.getSession(),
Model;
if (!record.isModel) {
Model = this.getModel();
record = new Model(record, session);
}
return record;
},
createFiltersCollection: function() {
return this.getData().getFilters();
},
createSortersCollection: function() {
var sorters = this.getData().getSorters();
sorters.setSorterConfigure(this.addFieldTransform, this);
return sorters;
},
/**
* Get the summary record for this store. See {@link Ext.data.Model#summary}.
* @return {Ext.data.Model}
* @since 6.5.0
*/
getSummaryRecord: function() {
var me = this,
summaryRecord = me.summaryRecord,
data = me.getData(),
generation = data.generation,
M, T, idProperty;
if (!summaryRecord) {
M = me.getModel();
T = M.getSummaryModel();
me.summaryRecord = summaryRecord = new T();
idProperty = M.idField.name;
summaryRecord.data[idProperty] = summaryRecord.id = M.identifier.generate();
summaryRecord.commit();
summaryRecord.isNonData = true;
}
if (!summaryRecord.isRemote && summaryRecord.summaryGeneration !== generation) {
summaryRecord.calculateSummary(data.items);
summaryRecord.summaryGeneration = generation;
}
return summaryRecord;
},
onCollectionBeginUpdate: function() {
this.beginUpdate();
},
onCollectionEndUpdate: function() {
this.endUpdate();
},
// When the collection informs us that it has sorted, this LocalStore must react.
// AbstractStore#onSorterEndUpdate does the correct thing (fires a refresh) if remote sorting
// is false
onCollectionSort: function() {
this.onSorterEndUpdate();
},
// When the collection informs us that it has filtered, this LocalStore must react.
// AbstractStore#onFilterEndUpdate does the correct thing (fires a refresh) if remote sorting
// is false
onCollectionFilter: function() {
this.onFilterEndUpdate();
},
// When the collection informs us that it has grouped, this LocalStore must react.
// AbstractStore#onGrouperEndUpdate does the correct thing (fires a refresh) if remote sorting
// is false
onCollectionGroup: function() {
this.onGroupersEndUpdate();
},
onGrouperChange: function(grouper) {
this.callObservers('GrouperChange', [ grouper ]);
},
notifySorterChange: function() {
this.getData().onSorterChange();
},
forceLocalSort: function() {
var sorters = this.getSorters();
// Sorter collection must inform all interested parties.
// We cannot just tell our data Collection to react - there
// may be GroupCollections hooked into the endUpdate call.
sorters.beginUpdate();
sorters.endUpdate();
},
// Inherit docs
contains: function(record) {
return this.indexOf(record) > -1;
},
/**
* Calls the specified function for each {@link Ext.data.Model record} in the store.
*
* When store is filtered, only loops over the filtered records.
*
* @param {Function} fn The function to call. The {@link Ext.data.Model Record} is passed
* as the first parameter. Returning `false` aborts and exits the iteration.
* @param {Object} [scope] The scope (`this` reference) in which the function is executed.
* Defaults to the current {@link Ext.data.Model record} in the iteration.
* @param {Object/Boolean} [includeOptions] An object which contains options which
* modify how the store is traversed. Or simply the `filtered` option.
* @param {Boolean} [includeOptions.filtered] Pass `true` to include filtered out
* nodes in the iteration.
*/
each: function(fn, scope, includeOptions) {
var data = this.getData(),
bypassFilters = includeOptions,
len, record, i;
if (typeof includeOptions === 'object') {
bypassFilters = includeOptions.filtered;
}
if (bypassFilters && data.filtered) {
data = data.getSource();
}
data = data.items.slice(0); // safe for re-entrant calls
len = data.length;
for (i = 0; i < len; ++i) {
record = data[i];
if (fn.call(scope || record, record, i, len) === false) {
break;
}
}
},
/**
* Collects unique values for a particular dataIndex from this store.
*
* Note that the `filtered` option can also be passed as a separate parameter for
* compatibility with previous versions.
*
* var store = Ext.create('Ext.data.Store', {
* fields: ['name'],
* data: [{
* name: 'Larry'
* }, {
* name: 'Darryl'
* }, {
* name: 'Darryl'
* }]
* });
*
* store.collect('name');
* // returns ["Larry", "Darryl"]
*
* @param {String} property The property to collect
* @param {Object} [includeOptions] An object which contains options which modify how
* the store is traversed. For compatibility, this argument may be the `allowNull`
* value itself. If so, the next argument is the `filtered` value.
* @param {Boolean} [includeOptions.allowNull] Pass true to allow null, undefined or
* empty string values.
* @param {Boolean} [includeOptions.filtered] Pass `true` to collect from all records,
* even ones which are filtered.
* @param {Boolean} [filtered] This argument only applies when the legacy call form
* is used and `includeOptions` is actually the `allowNull` value.
*
* @return {Object[]} An array of the unique values
*/
collect: function(property, includeOptions, filtered) {
var me = this,
allowNull = includeOptions,
data = me.getData();
if (typeof includeOptions === 'object') {
filtered = includeOptions.filtered;
allowNull = includeOptions.allowNull;
}
if (filtered && data.filtered) {
data = data.getSource();
}
return data.collect(property, 'data', allowNull);
},
/**
* 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.
*/
getById: function(id) {
var data = this.getData();
if (data.filtered) {
data = data.getSource();
}
return data.get(id) || null;
},
/**
* @private
* Get the Record with the specified internalId.
*
* This method is not affected by filtering, lookup will be performed from all records
* inside the store, filtered or not.
*
* @param {Mixed} internalId The id of the Record to find.
* @return {Ext.data.Model} The Record with the passed internalId. Returns null if not found.
*/
getByInternalId: function(internalId) {
var data = this.getData(),
keyCfg;
if (data.filtered) {
if (!data.$hasExtraKeys) {
keyCfg = this.makeInternalKeyCfg();
data.setExtraKeys(keyCfg);
data.$hasExtraKeys = true;
}
data = data.getSource();
}
if (!data.$hasExtraKeys) {
data.setExtraKeys(keyCfg || this.makeInternalKeyCfg());
data.$hasExtraKeys = true;
}
return data.byInternalId.get(internalId) || null;
},
/**
* Returns the complete unfiltered collection.
* @private
*/
getDataSource: function() {
var data = this.getData();
return data.getSource() || data;
},
/**
* Get the index of the record within the store.
*
* When store is filtered, records outside of filter will not be found.
*
* @param {Ext.data.Model} record The Ext.data.Model object to find.
* @return {Number} The index of the passed Record. Returns -1 if not found.
*/
indexOf: function(record) {
return this.getData().indexOf(record);
},
/**
* Get the index within the store of the Record with the passed id.
*
* Like #indexOf, this method is affected by filtering.
*
* @param {String} id The id of the Record to find.
* @return {Number} The index of the Record. Returns -1 if not found.
*/
indexOfId: function(id) {
return this.indexOf(this.getById(id));
},
/**
* Inserts Model instances into the Store at the given index and fires the add event.
* See also {@link #method-add}.
*
* @param {Number} index The start index at which to insert the passed Records.
* @param {Ext.data.Model/Ext.data.Model[]/Object/Object[]} records An `Ext.data.Model`
* instance, the data needed to populate an instance or an array of either of these.
*
* @return {Ext.data.Model[]} records The added records
*/
insert: function(index, records) {
var me = this,
len, i;
if (records) {
if (!Ext.isIterable(records)) {
records = [records];
}
else {
records = Ext.Array.clone(records);
}
len = records.length;
}
if (!len) {
return [];
}
for (i = 0; i < len; ++i) {
records[i] = me.createModel(records[i]);
}
me.getData().insert(index, records);
return records;
},
/**
* Query all the cached records in this Store using a filtering function. The specified function
* will be called with each record in this Store. If the function returns `true` the record is
* included in the results.
*
* This method is not affected by filtering, it will always search *all* records in the store
* regardless of filtering.
*
* @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.
* @return {Ext.util.Collection} The matched records
*/
queryBy: function(fn, scope) {
var data = this.getData();
return (data.getSource() || data).createFiltered(fn, scope);
},
/**
* Query all the cached records in this Store by name/value pair.
* The parameters will be used to generated a filter function that is given
* to the queryBy method.
*
* This method complements queryBy by generating the query function automatically.
*
* This method is not affected by filtering, it will always search *all* records in the store
* regardless of filtering.
*
* @param {String} property The property to create the filter function for
* @param {String/RegExp} value The string/regex to compare the property value to
* @param {Boolean} [anyMatch=false] True to match any part of the string, not just the
* beginning.
* @param {Boolean} [caseSensitive=false] `true` to create a case-sensitive regex.
* @param {Boolean} [exactMatch=false] True to force exact match (^ and $ characters
* added to the regex). Ignored if `anyMatch` is `true`.
* @return {Ext.util.Collection} The matched records
*/
query: function(property, value, anyMatch, caseSensitive, exactMatch) {
var data = this.getData();
return (data.getSource() || data).createFiltered(property, value, anyMatch, caseSensitive,
exactMatch);
},
/**
* Convenience function for getting the first model instance in the store.
*
* When store is filtered, will return first item within the filter.
*
* @param {Boolean} [grouped] True to perform the operation for each group
* in the store. The value returned will be an object literal with the key being the group
* name and the first record being the value. The grouped parameter is only honored if
* the store has a groupField.
* @return {Ext.data.Model/undefined} The first model instance in the store, or undefined
*/
first: function(grouped) {
return this.getData().first(grouped) || null;
},
/**
* Convenience function for getting the last model instance in the store.
*
* When store is filtered, will return last item within the filter.
*
* @param {Boolean} [grouped] True to perform the operation for each group
* in the store. The value returned will be an object literal with the key being the group
* name and the last record being the value. The grouped parameter is only honored if
* the store has a groupField.
* @return {Ext.data.Model/undefined} The last model instance in the store, or undefined
*/
last: function(grouped) {
return this.getData().last(grouped) || null;
},
/**
* Sums the value of `field` for each {@link Ext.data.Model record} in store
* and returns the result.
*
* When store is filtered, only sums items within the filter.
*
* @param {String} field A field in each record
* @param {Boolean} [grouped] True to perform the operation for each group
* in the store. The value returned will be an object literal with the key being the group
* name and the sum for that group being the value. The grouped parameter is only honored if
* the store has a groupField.
* @return {Number} The sum
*/
sum: function(field, grouped) {
var data = this.getData();
return (grouped && this.isGrouped()) ? data.sumByGroup(field) : data.sum(field);
},
/**
* Gets the count of items in the store.
*
* When store is filtered, only items within the filter are counted.
*
* @param {Boolean} [grouped] True to perform the operation for each group
* in the store. The value returned will be an object literal with the key being the group
* name and the count for each group being the value. The grouped parameter is only honored if
* the store has a groupField.
* @return {Number} the count
*/
count: function(grouped) {
var data = this.getData();
return (grouped && this.isGrouped()) ? data.countByGroup() : data.count();
},
/**
* Gets the minimum value in the store.
*
* When store is filtered, only items within the filter are aggregated.
*
* @param {String} field The field in each record
* @param {Boolean} [grouped] True to perform the operation for each group
* in the store. The value returned will be an object literal with the key being the group
* name and the minimum in the group being the value. The grouped parameter is only honored if
* the store has a groupField.
* @return {Object} The minimum value, if no items exist, undefined.
*/
min: function(field, grouped) {
var data = this.getData();
return (grouped && this.isGrouped()) ? data.minByGroup(field) : data.min(field);
},
/**
* Gets the maximum value in the store.
*
* When store is filtered, only items within the filter are aggregated.
*
* @param {String} field The field in each record
* @param {Boolean} [grouped] True to perform the operation for each group
* in the store. The value returned will be an object literal with the key being the group
* name and the maximum in the group being the value. The grouped parameter is only honored if
* the store has a groupField.
* @return {Object} The maximum value, if no items exist, undefined.
*/
max: function(field, grouped) {
var data = this.getData();
return (grouped && this.isGrouped()) ? data.maxByGroup(field) : data.max(field);
},
/**
* Gets the average value in the store.
*
* When store is filtered, only items within the filter are aggregated.
*
* @param {String} field The field in each record
* @param {Boolean} [grouped] True to perform the operation for each group
* in the store. The value returned will be an object literal with the key being the group
* name and the group average being the value. The grouped parameter is only honored if
* the store has a groupField.
* @return {Object} The average value, if no items exist, 0.
*/
average: function(field, grouped) {
var data = this.getData();
return (grouped && this.isGrouped()) ? data.averageByGroup(field) : data.average(field);
},
/**
* Runs the aggregate function for all the records in the store.
*
* When store is filtered, only items within the filter are aggregated.
*
* @param {Function} fn The function to execute. The function is called with a single parameter,
* an array of records for that group.
* @param {Object} [scope] The scope to execute the function in. Defaults to the store.
* @param {Boolean} [grouped] True to perform the operation for each group
* in the store. The value returned will be an object literal with the key being the group
* name and the group average being the value. The grouped parameter is only honored if
* the store has a groupField.
* @param {String} field The field to get the value from
* @return {Object} An object literal with the group names and their appropriate values.
*/
aggregate: function(fn, scope, grouped, field) {
var me = this,
groups, len, out, group, i;
if (grouped && me.isGrouped()) {
groups = me.getGroups().items;
len = groups.length;
out = {};
for (i = 0; i < len; ++i) {
group = groups[i];
out[group.getGroupKey()] = me.getAggregate(fn, scope || me, group.items, field);
}
return out;
}
else {
return me.getAggregate(fn, scope, me.getData().items, field);
}
},
getAggregate: function(fn, scope, records, field) {
var values = [],
len = records.length,
i;
// TODO EXTJSIV-12307 - not the right way to call fn
for (i = 0; i < len; ++i) {
values[i] = records[i].get(field);
}
return fn.call(scope || this, records, values);
},
addObserver: function(observer) {
var observers = this.observers;
if (!observers) {
this.observers = observers = new Ext.util.Collection();
}
observers.add(observer);
},
removeObserver: function(observer) {
var observers = this.observers;
if (observers) {
observers.remove(observer);
}
},
callObservers: function(action, args) {
var observers = this.observers,
len, items, i, methodName, item;
if (observers) {
items = observers.items;
if (args) {
args.unshift(this);
}
else {
args = [this];
}
for (i = 0, len = items.length; i < len; ++i) {
item = items[i];
methodName = 'onSource' + action;
if (item[methodName]) {
item[methodName].apply(item, args);
}
}
}
},
/**
* Query all the cached records in this Store using a filtering function. The specified function
* will be called with each record in this Store. If the function returns `true` the record is
* included in the results.
*
* This method is not affected by filtering, it will always search *all* records in the store
* regardless of filtering.
*
* @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.
* @param {Object} [scope] The scope (this reference) in which the function is executed
* Defaults to this Store.
* @return {Ext.data.Model[]} The matched records.
*
* @private
*/
queryRecordsBy: function(fn, scope) {
var data = this.getData(),
matches = [],
len, i, record;
data = (data.getSource() || data).items;
scope = scope || this;
for (i = 0, len = data.length; i < len; ++i) {
record = data[i];
if (fn.call(scope, record) === true) {
matches.push(record);
}
}
return matches;
},
/**
* Query all the cached records in this Store by field.
*
* This method is not affected by filtering, it will always search *all* records in the store
* regardless of filtering.
*
* @param {String} field The field from each record to use.
* @param {Object} value The value to match.
* @return {Ext.data.Model[]} The matched records.
*
* @private
*/
queryRecords: function(field, value) {
var data = this.getData(),
matches = [],
len, i, record;
data = (data.getSource() || data).items;
for (i = 0, len = data.length; i < len; ++i) {
record = data[i];
if (record.get(field) === value) {
matches.push(record);
}
}
return matches;
},
privates: {
isLast: function(record) {
return record === this.last();
},
makeInternalKeyCfg: function() {
return {
byInternalId: {
property: 'internalId',
rootProperty: ''
}
};
}
}
});