/* * Based on: * Ext JS Library 1.1.1 * Copyright(c) 2006-2007, Ext JS, LLC. * * Originally Released Under LGPL - original licence link has changed is not relivant. * * Fork - LGPL * <script type="text/javascript"> */ /** * @class Roo.data.Store * @extends Roo.util.Observable * The Store class encapsulates a client side cache of {@link Roo.data.Record} objects which provide input data * for widgets such as the Roo.grid.Grid, or the Roo.form.ComboBox.<br> * <p> * A Store object uses an implementation of {@link Roo.data.DataProxy} to access a data object unless you call loadData() directly and pass in your data. The Store object * has no knowledge of the format of the data returned by the Proxy.<br> * <p> * A Store object uses its configured implementation of {@link Roo.data.DataReader} to create {@link Roo.data.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. */ Roo.data.Store = function(config){ this.data = new Roo.util.MixedCollection(false); this.data.getKey = function(o){ return o.id; }; this.baseParams = {}; // private this.paramNames = { "start" : "start", "limit" : "limit", "sort" : "sort", "dir" : "dir", "multisort" : "_multisort" }; if(config && config.data){ this.inlineData = config.data; delete config.data; } Roo.apply(this, config); if(this.reader){ // reader passed this.reader = Roo.factory(this.reader, Roo.data); this.reader.xmodule = this.xmodule || false; if(!this.recordType){ this.recordType = this.reader.recordType; } if(this.reader.onMetaChange){ this.reader.onMetaChange = this.onMetaChange.createDelegate(this); } } if(this.recordType){ this.fields = this.recordType.prototype.fields; } this.modified = []; this.addEvents({ /** * @event datachanged * Fires when the data cache has changed, and a widget which is using this Store * as a Record cache should refresh its view. * @param {Store} this */ datachanged : true, /** * @event metachange * Fires when this store's reader provides new metadata (fields). This is currently only support for JsonReaders. * @param {Store} this * @param {Object} meta The JSON metadata */ metachange : true, /** * @event add * Fires when Records have been added to the Store * @param {Store} this * @param {Roo.data.Record[]} records The array of Records added * @param {Number} index The index at which the record(s) were added */ add : true, /** * @event remove * Fires when a Record has been removed from the Store * @param {Store} this * @param {Roo.data.Record} record The Record that was removed * @param {Number} index The index at which the record was removed */ remove : true, /** * @event update * Fires when a Record has been updated * @param {Store} this * @param {Roo.data.Record} record The Record that was updated * @param {String} operation The update operation being performed. Value may be one of: * <pre><code> Roo.data.Record.EDIT Roo.data.Record.REJECT Roo.data.Record.COMMIT * </code></pre> */ update : true, /** * @event clear * Fires when the data cache has been cleared. * @param {Store} this */ clear : true, /** * @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 : true, /** * @event beforeloadadd * Fires after a new set of Records has been loaded. * @param {Store} this * @param {Roo.data.Record[]} records The Records that were loaded * @param {Object} options The loading options that were specified (see {@link #load} for details) */ beforeloadadd : true, /** * @event load * Fires after a new set of Records has been loaded, before they are added to the store. * @param {Store} this * @param {Roo.data.Record[]} records The Records that were loaded * @param {Object} options The loading options that were specified (see {@link #load} for details) * @params {Object} return from reader */ load : true, /** * @event loadexception * Fires if an exception occurs in the Proxy during loading. * Called with the signature of the Proxy's "loadexception" event. * If you return Json { data: [] , success: false, .... } then this will be thrown with the following args * * @param {Proxy} * @param {Object} return from JsonData.reader() - success, totalRecords, records * @param {Object} load options * @param {Object} jsonData from your request (normally this contains the Exception) */ loadexception : true }); if(this.proxy){ this.proxy = Roo.factory(this.proxy, Roo.data); this.proxy.xmodule = this.xmodule || false; this.relayEvents(this.proxy, ["loadexception"]); } this.sortToggle = {}; this.sortOrder = []; // array of order of sorting - updated by grid if multisort is enabled. Roo.data.Store.superclass.constructor.call(this); if(this.inlineData){ this.loadData(this.inlineData); delete this.inlineData; } }; Roo.extend(Roo.data.Store, Roo.util.Observable, { /** * @cfg {boolean} isLocal flag if data is locally available (and can be always looked up * without a remote query - used by combo/forms at present. */ /** * @cfg {Roo.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 {Roo.data.Reader} reader The Reader object which processes the data object and returns * an Array of Roo.data.record objects which are cached keyed by their <em>id</em> property. */ /** * @cfg {Object} baseParams An object containing properties which are to be sent as parameters * on any HTTP request */ /** * @cfg {Object} sortInfo A config object in the format: {field: "fieldName", direction: "ASC|DESC"} */ /** * @cfg {Boolean} multiSort enable multi column sorting (sort is based on the order of columns, remote only at present) */ multiSort: false, /** * @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). */ 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, // private lastOptions : null, /** * Add Records to the Store and fires the add event. * @param {Roo.data.Record[]} records An Array of Roo.data.Record objects to add to the cache. */ add : function(records){ records = [].concat(records); for(var i = 0, len = records.length; i < len; i++){ records[i].join(this); } var index = this.data.length; this.data.addAll(records); this.fireEvent("add", this, records, index); }, /** * Remove a Record from the Store and fires the remove event. * @param {Ext.data.Record} record The Roo.data.Record object to remove from the cache. */ remove : function(record){ var index = this.data.indexOf(record); this.data.removeAt(index); if(this.pruneModifiedRecords){ this.modified.remove(record); } this.fireEvent("remove", this, record, index); }, /** * Remove all Records from the Store and fires the clear event. */ removeAll : function(){ this.data.clear(); if(this.pruneModifiedRecords){ this.modified = []; } this.fireEvent("clear", this); }, /** * Inserts Records to the Store at the given index and fires the add event. * @param {Number} index The start index at which to insert the passed Records. * @param {Roo.data.Record[]} records An Array of Roo.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 {Roo.data.Record} record The Roo.data.Record object to 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 {Roo.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 {Roo.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 {Roo.data.Record[]} An array of Records */ getRange : function(start, end){ return this.data.getRange(start, end); }, // private storeOptions : function(o){ o = Roo.apply({}, o); delete o.callback; delete o.scope; this.lastOptions = o; }, /** * Loads the Record cache from the configured Proxy using the configured Reader. * <p> * If using remote paging, then the first load call must specify the <em>start</em> * and <em>limit</em> 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. * <p> * <strong>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.</strong> * <p> * @param {Object} options An object containing properties which control loading options:<ul> * <li>params {Object} An object containing properties to pass as HTTP parameters to a remote data source.</li> * <li>callback {Function} A function to be called after the Records have been loaded. The callback is * passed the following arguments:<ul> * <li>r : Roo.data.Record[]</li> * <li>options: Options object from the load call</li> * <li>success: Boolean success indicator</li></ul></li> * <li>scope {Object} Scope with which to call the callback (defaults to the Store object)</li> * <li>add {Boolean} indicator to append loaded records rather than replace the current cache.</li> * </ul> */ load : function(options){ options = options || {}; if(this.fireEvent("beforeload", this, options) !== false){ this.storeOptions(options); var p = Roo.apply(options.params || {}, this.baseParams); // if meta was not loaded from remote source.. try requesting it. if (!this.reader.metaFromRemote) { p._requestMeta = 1; } if(this.sortInfo && this.remoteSort){ var pn = this.paramNames; p[pn["sort"]] = this.sortInfo.field; p[pn["dir"]] = this.sortInfo.direction; } if (this.multiSort) { var pn = this.paramNames; p[pn["multisort"]] = Roo.encode( { sort : this.sortToggle, order: this.sortOrder }); } this.proxy.load(p, this.reader, this.loadRecords, this, options); } }, /** * Reloads the Record cache from the configured Proxy using the configured Reader and * the options from the last load operation performed. * @param {Object} options (optional) An object containing properties 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(Roo.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, o); } if(options.callback){ options.callback.call(options.scope || this, [], options, false); } return; } // if data returned failure - throw an exception. if (o.success === false) { // show a message if no listener is registered. if (!this.hasListener('loadexception') && typeof(o.raw.errorMsg) != 'undefined') { Roo.MessageBox.alert("Error loading",o.raw.errorMsg); } // loadmask wil be hooked into this.. this.fireEvent("loadexception", this, o, options, o.raw.errorMsg); return; } var r = o.records, t = o.totalRecords || r.length; this.fireEvent("beforeloadadd", this, r, options, o); 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); } if(this.parent && !Roo.isIOS && !this.useNativeIOS && this.parent.emptyTitle.length) { var e = new Roo.data.Record({}); e.set(this.parent.displayField, this.parent.emptyTitle); e.set(this.parent.valueField, ''); this.insert(0, e); } this.fireEvent("load", this, r, options, o); if(options.callback){ options.callback.call(options.scope || this, r, options, true); } }, /** * Loads data from a passed data block. 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. */ loadData : function(o, append){ var r = this.reader.readRecords(o); this.loadRecords(r, {add: append}, true); }, /** * using 'cn' the nested child reader read the child array into it's child stores. * @param {Object} rec The record with a 'children array */ loadDataFromChildren : function(rec) { this.loadData(this.reader.toLoadData(rec)); }, /** * Gets the number of cached records. * <p> * <em>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 getTotalCount() function returns * the data set size</em> */ getCount : function(){ return this.data.length || 0; }, /** * Gets the total number of records in the dataset as returned by the server. * <p> * <em>If using paging, for this to be accurate, the data object used by the Reader must contain * the dataset size</em> */ getTotalCount : function(){ return this.totalLength || 0; }, /** * Returns the sort state of the Store as an object with two properties: * <pre><code> field {String} The name of the field by which the Records are sorted direction {String} The sort order, "ASC" or "DESC" * </code></pre> */ getSortState : function(){ return this.sortInfo; }, // private applySort : function(){ if(this.sortInfo && !this.remoteSort){ var s = this.sortInfo, f = s.field; 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(s.direction, fn); if(this.snapshot && this.snapshot != this.data){ this.snapshot.sort(s.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" (defaults to "ASC") */ setDefaultSort : function(field, dir){ this.sortInfo = {field: field, direction: dir ? dir.toUpperCase() : "ASC"}; }, /** * 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" (defaults to "ASC") */ sort : function(fieldName, dir){ var f = this.fields.get(fieldName); if(!dir){ this.sortToggle[f.name] = this.sortToggle[f.name] || f.sortDir; if(this.multiSort || (this.sortInfo && this.sortInfo.field == f.name) ){ // toggle sort dir dir = (this.sortToggle[f.name] || "ASC").toggle("ASC", "DESC"); }else{ dir = f.sortDir; } } this.sortToggle[f.name] = dir; this.sortInfo = {field: f.name, direction: dir}; if(!this.remoteSort){ this.applySort(); this.fireEvent("datachanged", this); }else{ this.load(this.lastOptions); } }, /** * 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 <em>false</em> 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 {Roo.data.Record[]} An array of Records containing outstanding modifications. */ getModifiedRecords : function(){ return this.modified; }, // private createFilterFn : function(property, value, anyMatch){ if(!value.exec){ // not a regex value = String(value); if(value.length == 0){ return false; } value = new RegExp((anyMatch === true ? '' : '^') + Roo.escapeRe(value), "i"); } return function(r){ return value.test(r.data[property]); }; }, /** * Sums the value of <i>property</i> 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 start with or a RegExp to test against the field * @param {Boolean} anyMatch True to match any part not just the beginning */ filter : function(property, value, anyMatch){ var fn = this.createFilterFn(property, value, anyMatch); return fn ? this.filterBy(fn) : this.clearFilter(); }, /** * Filter by a function. The specified function will be called with each * record in this data source. If the function returns true the record is included, * otherwise it is filtered. * @param {Function} fn The function to be called, it will receive 2 args (record, id) * @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 start with or a RegExp to test against the field * @param {Boolean} anyMatch True to match any part not just the beginning * @return {MixedCollection} Returns an Roo.util.MixedCollection of the matched records */ query : function(property, value, anyMatch){ var fn = this.createFilterFn(property, value, anyMatch); return fn ? this.queryBy(fn) : this.data.clone(); }, /** * Query by a function. The specified function will be called with each * record in this data source. If the function returns true the record is included * in the results. * @param {Function} fn The function to be called, it will receive 2 args (record, id) * @param {Object} scope (optional) The scope of the function (defaults to this) @return {MixedCollection} Returns an Roo.util.MixedCollection of the matched records **/ queryBy : function(fn, scope){ var data = this.snapshot || this.data; return data.filterBy(fn, scope||this); }, /** * 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 || !Roo.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.snapshot && this.snapshot != this.data){ this.data = this.snapshot; delete this.snapshot; if(suppressEvent !== true){ this.fireEvent("datachanged", this); } } }, // private afterEdit : function(record){ if(this.modified.indexOf(record) == -1){ this.modified.push(record); } this.fireEvent("update", this, record, Roo.data.Record.EDIT); }, // private afterReject : function(record){ this.modified.remove(record); this.fireEvent("update", this, record, Roo.data.Record.REJECT); }, // private afterCommit : function(record){ this.modified.remove(record); this.fireEvent("update", this, record, Roo.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 Roo.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(); } }, onMetaChange : function(meta, rtype, o){ this.recordType = rtype; this.fields = rtype.prototype.fields; delete this.snapshot; this.sortInfo = meta.sortInfo || this.sortInfo; this.modified = []; this.fireEvent('metachange', this, this.reader.meta); }, moveIndex : function(data, type) { var index = this.indexOf(data); var newIndex = index + type; this.remove(data); this.insert(newIndex, data); } });