/* * Ext JS Library 2.3.0 * Copyright(c) 2006-2009, Ext JS, LLC. * licensing@extjs.com * * http://extjs.com/license */ /** * @class Ext.data.Store * @extends Ext.util.Observable * The Store class encapsulates a client side cache of {@link Ext.data.Record Record} * objects which provide input data for Components such as the {@link Ext.grid.GridPanel GridPanel}, * the {@link Ext.form.ComboBox ComboBox}, or the {@link Ext.DataView DataView}

*

A Store object uses its {@link #proxy configured} implementation of {@link Ext.data.DataProxy DataProxy} * to access a data object unless you call {@link #loadData} directly and pass in your data.

*

A Store object has no knowledge of the format of the data returned by the Proxy.

*

A Store object uses its {@link #reader configured} implementation of {@link Ext.data.DataReader DataReader} * to create {@link Ext.data.Record Record} instances from the data object. These Records * are cached and made available through accessor functions.

* @constructor * Creates a new Store. * @param {Object} config A config object containing the objects needed for the Store to access data, * and read the data into Records. */ Ext.data.Store = function(config){ this.data = new Ext.util.MixedCollection(false); this.data.getKey = function(o){ return o.id; }; /** * An object containing properties which are used as parameters on any HTTP request. * This property can be changed after creating the Store to send different parameters. * @property */ this.baseParams = {}; /** *

An object containing properties which specify the names of the paging and * sorting parameters passed to remote servers when loading blocks of data. By default, this * object takes the following form:


{
    start : "start",    // The parameter name which specifies the start row
    limit : "limit",    // The parameter name which specifies number of rows to return
    sort : "sort",      // The parameter name which specifies the column to sort on
    dir : "dir"         // The parameter name which specifies the sort direction
}
*

The server must produce the requested data block upon receipt of these parameter names. * If different parameter names are required, this property can be overriden using a configuration * property.

*

A {@link Ext.PagingToolbar PagingToolbar} bound to this grid uses this property to determine * the parameter names to use in its requests. * @property */ this.paramNames = { "start" : "start", "limit" : "limit", "sort" : "sort", "dir" : "dir" }; if(config && config.data){ this.inlineData = config.data; delete config.data; } Ext.apply(this, config); if(this.url && !this.proxy){ this.proxy = new Ext.data.HttpProxy({url: this.url}); } if(this.reader){ // reader passed if(!this.recordType){ this.recordType = this.reader.recordType; } if(this.reader.onMetaChange){ this.reader.onMetaChange = this.onMetaChange.createDelegate(this); } } /** * The {@link Ext.data.Record Record} constructor as supplied to (or created by) the {@link Ext.data.Reader#Reader Reader}. Read-only. *

If the Reader was constructed by passing in an Array of field definition objects, instead of a created * Record constructor it will have {@link Ext.data.Record#create created a constructor} from that Array.

*

This property may be used to create new Records of the type held in this Store.

* @property recordType * @type Function */ if(this.recordType){ /** * A MixedCollection containing the defined {@link Ext.data.Field Field}s for the Records stored in this Store. Read-only. * @property fields * @type Ext.util.MixedCollection */ this.fields = this.recordType.prototype.fields; } this.modified = []; this.addEvents( /** * @event datachanged * 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 {Store} this */ 'datachanged', /** * @event metachange * Fires when this store's reader provides new metadata (fields). This is currently only supported for JsonReaders. * @param {Store} this * @param {Object} meta The JSON metadata */ 'metachange', /** * @event add * Fires when Records have been added to the Store * @param {Store} this * @param {Ext.data.Record[]} records The array of Records added * @param {Number} index The index at which the record(s) were added */ 'add', /** * @event remove * Fires when a Record has been removed from the Store * @param {Store} this * @param {Ext.data.Record} record The Record that was removed * @param {Number} index The index at which the record was removed */ 'remove', /** * @event update * Fires when a Record has been updated * @param {Store} this * @param {Ext.data.Record} record The Record that was updated * @param {String} operation The update operation being performed. Value may be one of: *

 Ext.data.Record.EDIT
 Ext.data.Record.REJECT
 Ext.data.Record.COMMIT
         * 
*/ 'update', /** * @event clear * Fires when the data cache has been cleared. * @param {Store} this */ 'clear', /** * @event beforeload * Fires before a request is made for a new data object. If the beforeload handler returns false * the load action will be canceled. * @param {Store} this * @param {Object} options The loading options that were specified (see {@link #load} for details) */ 'beforeload', /** * @event load * Fires after a new set of Records has been loaded. * @param {Store} this * @param {Ext.data.Record[]} records The Records that were loaded * @param {Object} options The loading options that were specified (see {@link #load} for details) */ 'load', /** * @event loadexception * Fires if an exception occurs in the Proxy during loading. * Called with the signature of the Proxy's "loadexception" event. */ 'loadexception' ); if(this.proxy){ this.relayEvents(this.proxy, ["loadexception"]); } this.sortToggle = {}; if(this.sortInfo){ this.setDefaultSort(this.sortInfo.field, this.sortInfo.direction); } Ext.data.Store.superclass.constructor.call(this); if(this.storeId || this.id){ Ext.StoreMgr.register(this); } if(this.inlineData){ this.loadData(this.inlineData); delete this.inlineData; }else if(this.autoLoad){ this.load.defer(10, this, [ typeof this.autoLoad == 'object' ? this.autoLoad : undefined]); } }; Ext.extend(Ext.data.Store, Ext.util.Observable, { /** * @cfg {String} storeId If passed, the id to use to register with the StoreMgr */ /** * @cfg {String} url If passed, an HttpProxy is created for the passed URL */ /** * @cfg {Boolean/Object} autoLoad If passed, this store's load method is automatically called after creation with the autoLoad object */ /** * @cfg {Ext.data.DataProxy} proxy The Proxy object which provides access to a data object. */ /** * @cfg {Array} data Inline data to be loaded when the store is initialized. */ /** * @cfg {Ext.data.DataReader} reader The DataReader object which processes the data object and returns * an Array of Ext.data.Record objects which are cached keyed by their id property. */ /** * @cfg {Object} baseParams

An object containing properties which are to be sent as parameters.

*

Parameters are encoded as standard HTTP parameters using {@link Ext#urlEncode}.

* on any HTTP request */ /** * @cfg {Object} sortInfo A config object in the format: {field: "fieldName", direction: "ASC|DESC"} to * specify the sort order in the request of a remote Store's {@link #load} operation. Note that for * local sorting, the direction property is case-sensitive. */ /** * @cfg {boolean} remoteSort True if sorting is to be handled by requesting the * Proxy to provide a refreshed version of the data object in sorted order, as * opposed to sorting the Record cache in place (defaults to false). *

If remote sorting is specified, then clicking on a column header causes the * current page to be requested from the server with the addition of the following * two parameters: *

*/ remoteSort : false, /** * @cfg {boolean} pruneModifiedRecords True to clear all modified record information each time the store is * loaded or when a record is removed. (defaults to false). */ pruneModifiedRecords : false, /** * Contains the last options object used as the parameter to the load method. See {@link #load} * for the details of what this may contain. This may be useful for accessing any params which * were used to load the current Record cache. * @property */ lastOptions : null, destroy : function(){ if(this.storeId || this.id){ Ext.StoreMgr.unregister(this); } this.data = null; Ext.destroy(this.proxy); this.reader = null; this.purgeListeners(); }, /** * Add Records to the Store and fires the {@link #add} event. * @param {Ext.data.Record[]} records An Array of Ext.data.Record objects to add to the cache. */ add : function(records){ records = [].concat(records); if(records.length < 1){ return; } for(var i = 0, len = records.length; i < len; i++){ records[i].join(this); } var index = this.data.length; this.data.addAll(records); if(this.snapshot){ this.snapshot.addAll(records); } this.fireEvent("add", this, records, index); }, /** * (Local sort only) Inserts the passed Record into the Store at the index where it * should go based on the current sort information. * @param {Ext.data.Record} record */ addSorted : function(record){ var index = this.findInsertIndex(record); this.insert(index, record); }, /** * Remove a Record from the Store and fires the {@link #remove} event. * @param {Ext.data.Record} record The Ext.data.Record object to remove from the cache. */ remove : function(record){ var index = this.data.indexOf(record); if(index > -1){ this.data.removeAt(index); if(this.pruneModifiedRecords){ this.modified.remove(record); } if(this.snapshot){ this.snapshot.remove(record); } this.fireEvent("remove", this, record, index); } }, /** * Remove a Record from the Store at the specified index. Fires the {@link #remove} event. * @param {Number} index The index of the record to remove. */ removeAt : function(index){ this.remove(this.getAt(index)); }, /** * Remove all Records from the Store and fires the {@link #clear} event. */ removeAll : function(){ this.data.clear(); if(this.snapshot){ this.snapshot.clear(); } if(this.pruneModifiedRecords){ this.modified = []; } this.fireEvent("clear", this); }, /** * Inserts Records into the Store at the given index and fires the {@link #add} event. * @param {Number} index The start index at which to insert the passed Records. * @param {Ext.data.Record[]} records An Array of Ext.data.Record objects to add to the cache. */ insert : function(index, records){ records = [].concat(records); for(var i = 0, len = records.length; i < len; i++){ this.data.insert(index, records[i]); records[i].join(this); } this.fireEvent("add", this, records, index); }, /** * Get the index within the cache of the passed Record. * @param {Ext.data.Record} record The Ext.data.Record object to find. * @return {Number} The index of the passed Record. Returns -1 if not found. */ indexOf : function(record){ return this.data.indexOf(record); }, /** * Get the index within the cache of the Record with the passed id. * @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.data.indexOfKey(id); }, /** * Get the Record with the specified id. * @param {String} id The id of the Record to find. * @return {Ext.data.Record} The Record with the passed id. Returns undefined if not found. */ getById : function(id){ return this.data.key(id); }, /** * Get the Record at the specified index. * @param {Number} index The index of the Record to find. * @return {Ext.data.Record} The Record at the passed index. Returns undefined if not found. */ getAt : function(index){ return this.data.itemAt(index); }, /** * Returns a range of Records between specified indices. * @param {Number} startIndex (optional) The starting index (defaults to 0) * @param {Number} endIndex (optional) The ending index (defaults to the last Record in the Store) * @return {Ext.data.Record[]} An array of Records */ getRange : function(start, end){ return this.data.getRange(start, end); }, // private storeOptions : function(o){ o = Ext.apply({}, o); delete o.callback; delete o.scope; this.lastOptions = o; }, /** * Loads the Record cache from the configured Proxy using the configured Reader. *

If using remote paging, then the first load call must specify the start * and limit properties in the options.params property to establish the initial * position within the dataset, and the number of Records to cache on each read from the Proxy.

*

It is important to note that for remote data sources, loading is asynchronous, * and this call will return before the new data has been loaded. Perform any post-processing * in a callback function, or in a "load" event handler.

* @param {Object} options An object containing properties which control loading options: * @return {Boolean} Whether the load fired (if beforeload failed). */ load : function(options){ options = options || {}; if(this.fireEvent("beforeload", this, options) !== false){ this.storeOptions(options); var p = Ext.apply(options.params || {}, this.baseParams); if(this.sortInfo && this.remoteSort){ var pn = this.paramNames; p[pn["sort"]] = this.sortInfo.field; p[pn["dir"]] = this.sortInfo.direction; } this.proxy.load(p, this.reader, this.loadRecords, this, options); return true; } else { return false; } }, /** *

Reloads the Record cache from the configured Proxy using the configured Reader and * the options from the last load operation performed.

*

It is important to note that for remote data sources, loading is asynchronous, * and this call will return before the new data has been loaded. Perform any post-processing * in a callback function, or in a "load" event handler.

* @param {Object} options (optional) An object containing loading options which may override the options * used in the last load operation. See {@link #load} for details (defaults to null, in which case * the most recently used options are reused). */ reload : function(options){ this.load(Ext.applyIf(options||{}, this.lastOptions)); }, // private // Called as a callback by the Reader during a load operation. loadRecords : function(o, options, success){ if(!o || success === false){ if(success !== false){ this.fireEvent("load", this, [], options); } if(options.callback){ options.callback.call(options.scope || this, [], options, false); } return; } var r = o.records, t = o.totalRecords || r.length; if(!options || options.add !== true){ if(this.pruneModifiedRecords){ this.modified = []; } for(var i = 0, len = r.length; i < len; i++){ r[i].join(this); } if(this.snapshot){ this.data = this.snapshot; delete this.snapshot; } this.data.clear(); this.data.addAll(r); this.totalLength = t; this.applySort(); this.fireEvent("datachanged", this); }else{ this.totalLength = Math.max(t, this.data.length+r.length); this.add(r); } this.fireEvent("load", this, r, options); if(options.callback){ options.callback.call(options.scope || this, r, options, true); } }, /** * Loads data from a passed data block and fires the {@link #load} event. A Reader which understands the format of the data * must have been configured in the constructor. * @param {Object} data The data block from which to read the Records. The format of the data expected * is dependent on the type of Reader that is configured and should correspond to that Reader's readRecords parameter. * @param {Boolean} append (Optional) True to append the new Records rather than replace the existing cache. Remember that * Records in a Store are keyed by their {@link Ext.data.Record#id id}, so added Records with ids which are already present in * the Store will replace existing Records. Records with new, unique ids will be added. */ loadData : function(o, append){ var r = this.reader.readRecords(o); this.loadRecords(r, {add: append}, true); }, /** * Gets the number of cached records. *

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 #getTotalCount} function returns * the dataset size.

* @return {Number} The number of Records in the Store's cache. */ getCount : function(){ return this.data.length || 0; }, /** * Gets the total number of records in the dataset as returned by the server. *

If using paging, for this to be accurate, the data object used by the Reader must contain * the dataset size. For remote data sources, this is provided by a query on the server.

* @return {Number} The number of Records as specified in the data object passed to the Reader * by the Proxy *

This value is not updated when changing the contents of the Store locally.

*/ getTotalCount : function(){ return this.totalLength || 0; }, /** * Returns an object describing the current sort state of this Store. * @return {Object} The sort state of the Store. An object with two properties: */ getSortState : function(){ return this.sortInfo; }, // private applySort : function(){ if(this.sortInfo && !this.remoteSort){ var s = this.sortInfo, f = s.field; this.sortData(f, s.direction); } }, // private sortData : function(f, direction){ direction = direction || 'ASC'; var st = this.fields.get(f).sortType; var fn = function(r1, r2){ var v1 = st(r1.data[f]), v2 = st(r2.data[f]); return v1 > v2 ? 1 : (v1 < v2 ? -1 : 0); }; this.data.sort(direction, fn); if(this.snapshot && this.snapshot != this.data){ this.snapshot.sort(direction, fn); } }, /** * Sets the default sort column and order to be used by the next load operation. * @param {String} fieldName The name of the field to sort by. * @param {String} dir (optional) The sort order, "ASC" or "DESC" (case-sensitive, defaults to "ASC") */ setDefaultSort : function(field, dir){ dir = dir ? dir.toUpperCase() : "ASC"; this.sortInfo = {field: field, direction: dir}; this.sortToggle[field] = dir; }, /** * Sort the Records. * If remote sorting is used, the sort is performed on the server, and the cache is * reloaded. If local sorting is used, the cache is sorted internally. * @param {String} fieldName The name of the field to sort by. * @param {String} dir (optional) The sort order, "ASC" or "DESC" (case-sensitive, defaults to "ASC") */ sort : function(fieldName, dir){ var f = this.fields.get(fieldName); if(!f){ return false; } if(!dir){ if(this.sortInfo && this.sortInfo.field == f.name){ // toggle sort dir dir = (this.sortToggle[f.name] || "ASC").toggle("ASC", "DESC"); }else{ dir = f.sortDir; } } var st = (this.sortToggle) ? this.sortToggle[f.name] : null; var si = (this.sortInfo) ? this.sortInfo : null; this.sortToggle[f.name] = dir; this.sortInfo = {field: f.name, direction: dir}; if(!this.remoteSort){ this.applySort(); this.fireEvent("datachanged", this); }else{ if (!this.load(this.lastOptions)) { if (st) { this.sortToggle[f.name] = st; } if (si) { this.sortInfo = si; } } } }, /** * Calls the specified function for each of the Records in the cache. * @param {Function} fn The function to call. The Record is passed as the first parameter. * Returning false aborts and exits the iteration. * @param {Object} scope (optional) The scope in which to call the function (defaults to the Record). */ each : function(fn, scope){ this.data.each(fn, scope); }, /** * Gets all records modified since the last commit. Modified records are persisted across load operations * (e.g., during paging). * @return {Ext.data.Record[]} An array of Records containing outstanding modifications. */ getModifiedRecords : function(){ return this.modified; }, // private createFilterFn : function(property, value, anyMatch, caseSensitive){ if(Ext.isEmpty(value, false)){ return false; } value = this.data.createValueMatcher(value, anyMatch, caseSensitive); return function(r){ return value.test(r.data[property]); }; }, /** * Sums the value of property for each record between start and end and returns the result. * @param {String} property A field on your records * @param {Number} start The record index to start at (defaults to 0) * @param {Number} end The last record index to include (defaults to length - 1) * @return {Number} The sum */ sum : function(property, start, end){ var rs = this.data.items, v = 0; start = start || 0; end = (end || end === 0) ? end : rs.length-1; for(var i = start; i <= end; i++){ v += (rs[i].data[property] || 0); } return v; }, /** * Filter the records by a specified property. * @param {String} field A field on your records * @param {String/RegExp} value Either a string that the field * should begin with, or a RegExp to test against the field. * @param {Boolean} anyMatch (optional) True to match any part not just the beginning * @param {Boolean} caseSensitive (optional) True for case sensitive comparison */ filter : function(property, value, anyMatch, caseSensitive){ var fn = this.createFilterFn(property, value, anyMatch, caseSensitive); return fn ? this.filterBy(fn) : this.clearFilter(); }, /** * Filter 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. * @param {Function} fn The function to be called. It will be passed the following parameters: * @param {Object} scope (optional) The scope of the function (defaults to this) */ filterBy : function(fn, scope){ this.snapshot = this.snapshot || this.data; this.data = this.queryBy(fn, scope||this); this.fireEvent("datachanged", this); }, /** * Query the records by a specified property. * @param {String} field A field on your records * @param {String/RegExp} value Either a string that the field * should begin with, or a RegExp to test against the field. * @param {Boolean} anyMatch (optional) True to match any part not just the beginning * @param {Boolean} caseSensitive (optional) True for case sensitive comparison * @return {MixedCollection} Returns an Ext.util.MixedCollection of the matched records */ query : function(property, value, anyMatch, caseSensitive){ var fn = this.createFilterFn(property, value, anyMatch, caseSensitive); return fn ? this.queryBy(fn) : this.data.clone(); }, /** * Query 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. * @param {Function} fn The function to be called. It will be passed the following parameters: * @param {Object} scope (optional) The scope of the function (defaults to this) * @return {MixedCollection} Returns an Ext.util.MixedCollection of the matched records **/ queryBy : function(fn, scope){ var data = this.snapshot || this.data; return data.filterBy(fn, scope||this); }, /** * Finds the index of the first matching record in this store by a specific property/value. * @param {String} property A property on your objects * @param {String/RegExp} value Either a string that the property value * should begin with, or a RegExp to test against the property. * @param {Number} startIndex (optional) The index to start searching at * @param {Boolean} anyMatch (optional) True to match any part of the string, not just the beginning * @param {Boolean} caseSensitive (optional) True for case sensitive comparison * @return {Number} The matched index or -1 */ find : function(property, value, start, anyMatch, caseSensitive){ var fn = this.createFilterFn(property, value, anyMatch, caseSensitive); return fn ? this.data.findIndexBy(fn, null, start) : -1; }, /** * Find the index of the first matching Record in this Store by a function. * If the function returns true it is considered a match. * @param {Function} fn The function to be called. It will be passed the following parameters: * @param {Object} scope (optional) The scope of the function (defaults to this) * @param {Number} startIndex (optional) The index to start searching at * @return {Number} The matched index or -1 */ findBy : function(fn, scope, start){ return this.data.findIndexBy(fn, scope, start); }, /** * Collects unique values for a particular dataIndex from this store. * @param {String} dataIndex The property to collect * @param {Boolean} allowNull (optional) Pass true to allow null, undefined or empty string values * @param {Boolean} bypassFilter (optional) Pass true to collect from all records, even ones which are filtered * @return {Array} An array of the unique values **/ collect : function(dataIndex, allowNull, bypassFilter){ var d = (bypassFilter === true && this.snapshot) ? this.snapshot.items : this.data.items; var v, sv, r = [], l = {}; for(var i = 0, len = d.length; i < len; i++){ v = d[i].data[dataIndex]; sv = String(v); if((allowNull || !Ext.isEmpty(v)) && !l[sv]){ l[sv] = true; r[r.length] = v; } } return r; }, /** * Revert to a view of the Record cache with no filtering applied. * @param {Boolean} suppressEvent If true the filter is cleared silently without notifying listeners */ clearFilter : function(suppressEvent){ if(this.isFiltered()){ this.data = this.snapshot; delete this.snapshot; if(suppressEvent !== true){ this.fireEvent("datachanged", this); } } }, /** * Returns true if this store is currently filtered * @return {Boolean} */ isFiltered : function(){ return this.snapshot && this.snapshot != this.data; }, // private afterEdit : function(record){ if(this.modified.indexOf(record) == -1){ this.modified.push(record); } this.fireEvent("update", this, record, Ext.data.Record.EDIT); }, // private afterReject : function(record){ this.modified.remove(record); this.fireEvent("update", this, record, Ext.data.Record.REJECT); }, // private afterCommit : function(record){ this.modified.remove(record); this.fireEvent("update", this, record, Ext.data.Record.COMMIT); }, /** * Commit all Records with outstanding changes. To handle updates for changes, subscribe to the * Store's "update" event, and perform updating when the third parameter is Ext.data.Record.COMMIT. */ commitChanges : function(){ var m = this.modified.slice(0); this.modified = []; for(var i = 0, len = m.length; i < len; i++){ m[i].commit(); } }, /** * Cancel outstanding changes on all changed records. */ rejectChanges : function(){ var m = this.modified.slice(0); this.modified = []; for(var i = 0, len = m.length; i < len; i++){ m[i].reject(); } }, // private onMetaChange : function(meta, rtype, o){ this.recordType = rtype; this.fields = rtype.prototype.fields; delete this.snapshot; if(meta.sortInfo){ this.sortInfo = meta.sortInfo; }else if(this.sortInfo && !this.fields.get(this.sortInfo.field)){ delete this.sortInfo; } this.modified = []; this.fireEvent('metachange', this, this.reader.meta); }, // private findInsertIndex : function(record){ this.suspendEvents(); var data = this.data.clone(); this.data.add(record); this.applySort(); var index = this.data.indexOf(record); this.data = data; this.resumeEvents(); return index; } });