X-Git-Url: http://git.ithinksw.org/extjs.git/blobdiff_plain/0494b8d9b9bb03ab6c22b34dae81261e3cd7e3e6..7a654f8d43fdb43d78b63d90528bed6e86b608cc:/src/data/Store.js diff --git a/src/data/Store.js b/src/data/Store.js index d6ae2398..be58ee56 100644 --- a/src/data/Store.js +++ b/src/data/Store.js @@ -1,1615 +1,1755 @@ -/*! - * Ext JS Library 3.3.1 - * Copyright(c) 2006-2010 Sencha Inc. - * licensing@sencha.com - * http://www.sencha.com/license - */ /** + * @author Ed Spencer * @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}.

- *

Retrieving Data

- *

A Store object may access a data object using:

- *

Reading Data

- *

A Store object has no inherent knowledge of the format of the data object (it could be - * an Array, XML, or JSON). A Store object uses an appropriate {@link #reader configured implementation} - * of a {@link Ext.data.DataReader DataReader} to create {@link Ext.data.Record Record} instances from the data - * object.

- *

Store Types

- *

There are several implementations of Store available which are customized for use with - * a specific DataReader implementation. Here is an example using an ArrayStore which implicitly - * creates a reader commensurate to an Array data object.

- *

-var myStore = new Ext.data.ArrayStore({
-    fields: ['fullname', 'first'],
-    idIndex: 0 // id for each record will be the first element
+ * @extends Ext.data.AbstractStore
+ *
+ * 

The Store class encapsulates a client side cache of {@link Ext.data.Model Model} objects. Stores load + * data via a {@link Ext.data.proxy.Proxy Proxy}, and also provide functions for {@link #sort sorting}, + * {@link #filter filtering} and querying the {@link Ext.data.Model model} instances contained within it.

+ * + *

Creating a Store is easy - we just tell it the Model and the Proxy to use to load and save its data:

+ * +

+// Set up a {@link Ext.data.Model model} to use in our Store
+Ext.define('User', {
+    extend: 'Ext.data.Model',
+    fields: [
+        {name: 'firstName', type: 'string'},
+        {name: 'lastName',  type: 'string'},
+        {name: 'age',       type: 'int'},
+        {name: 'eyeColor',  type: 'string'}
+    ]
 });
- * 
- *

For custom implementations create a basic {@link Ext.data.Store} configured as needed:

- *

-// create a {@link Ext.data.Record Record} constructor:
-var rt = Ext.data.Record.create([
-    {name: 'fullname'},
-    {name: 'first'}
-]);
+
 var myStore = new Ext.data.Store({
-    // explicitly create reader
-    reader: new Ext.data.ArrayReader(
+    model: 'User',
+    proxy: {
+        type: 'ajax',
+        url : '/users.json',
+        reader: {
+            type: 'json',
+            root: 'users'
+        }
+    },
+    autoLoad: true
+});
+
+ + *

In the example above we configured an AJAX proxy to load data from the url '/users.json'. We told our Proxy + * to use a {@link Ext.data.reader.Json JsonReader} to parse the response from the server into Model object - + * {@link Ext.data.reader.Json see the docs on JsonReader} for details.

+ * + *

Inline data

+ * + *

Stores can also load data inline. Internally, Store converts each of the objects we pass in as {@link #data} + * into Model instances:

+ * +

+new Ext.data.Store({
+    model: 'User',
+    data : [
+        {firstName: 'Ed',    lastName: 'Spencer'},
+        {firstName: 'Tommy', lastName: 'Maintz'},
+        {firstName: 'Aaron', lastName: 'Conran'},
+        {firstName: 'Jamie', lastName: 'Avins'}
+    ]
+});
+
+ * + *

Loading inline data using the method above is great if the data is in the correct format already (e.g. it doesn't need + * to be processed by a {@link Ext.data.reader.Reader reader}). If your inline data requires processing to decode the data structure, + * use a {@link Ext.data.proxy.Memory MemoryProxy} instead (see the {@link Ext.data.proxy.Memory MemoryProxy} docs for an example).

+ * + *

Additional data can also be loaded locally using {@link #add}.

+ * + *

Loading Nested Data

+ * + *

Applications often need to load sets of associated data - for example a CRM system might load a User and her Orders. + * Instead of issuing an AJAX request for the User and a series of additional AJAX requests for each Order, we can load a nested dataset + * and allow the Reader to automatically populate the associated models. Below is a brief example, see the {@link Ext.data.reader.Reader} intro + * docs for a full explanation:

+ * +

+var store = new Ext.data.Store({
+    autoLoad: true,
+    model: "User",
+    proxy: {
+        type: 'ajax',
+        url : 'users.json',
+        reader: {
+            type: 'json',
+            root: 'users'
+        }
+    }
+});
+
+ * + *

Which would consume a response like this:

+ * +

+{
+    "users": [
+        {
+            "id": 1,
+            "name": "Ed",
+            "orders": [
+                {
+                    "id": 10,
+                    "total": 10.76,
+                    "status": "invoiced"
+                },
+                {
+                    "id": 11,
+                    "total": 13.45,
+                    "status": "shipped"
+                }
+            ]
+        }
+    ]
+}
+
+ * + *

See the {@link Ext.data.reader.Reader} intro docs for a full explanation.

+ * + *

Filtering and Sorting

+ * + *

Stores can be sorted and filtered - in both cases either remotely or locally. The {@link #sorters} and {@link #filters} are + * held inside {@link Ext.util.MixedCollection MixedCollection} instances to make them easy to manage. Usually it is sufficient to + * either just specify sorters and filters in the Store configuration or call {@link #sort} or {@link #filter}: + * +


+var store = new Ext.data.Store({
+    model: 'User',
+    sorters: [
         {
-            idIndex: 0  // id for each record will be the first element
+            property : 'age',
+            direction: 'DESC'
         },
-        rt // recordType
-    )
+        {
+            property : 'firstName',
+            direction: 'ASC'
+        }
+    ],
+
+    filters: [
+        {
+            property: 'firstName',
+            value   : /Ed/
+        }
+    ]
 });
- * 
- *

Load some data into store (note the data object is an array which corresponds to the reader):

- *

-var myData = [
-    [1, 'Fred Flintstone', 'Fred'],  // note that id for the record is the first element
-    [2, 'Barney Rubble', 'Barney']
-];
-myStore.loadData(myData);
- * 
- *

Records are cached and made available through accessor functions. An example of adding - * a record to the store:

- *

-var defaultData = {
-    fullname: 'Full Name',
-    first: 'First Name'
-};
-var recId = 100; // provide unique id for the record
-var r = new myStore.recordType(defaultData, ++recId); // create new record
-myStore.{@link #insert}(0, r); // insert a new record into the store (also see {@link #add})
- * 
- *

Writing Data

- *

And new in Ext version 3, use the new {@link Ext.data.DataWriter DataWriter} to create an automated, Writable Store - * along with RESTful features. +

+ * + *

The new Store will keep the configured sorters and filters in the MixedCollection instances mentioned above. By default, sorting + * and filtering are both performed locally by the Store - see {@link #remoteSort} and {@link #remoteFilter} to allow the server to + * perform these operations instead.

+ * + *

Filtering and sorting after the Store has been instantiated is also easy. Calling {@link #filter} adds another filter to the Store + * and automatically filters the dataset (calling {@link #filter} with no arguments simply re-applies all existing filters). Note that by + * default {@link #sortOnFilter} is set to true, which means that your sorters are automatically reapplied if using local sorting.

+ * +

+store.filter('eyeColor', 'Brown');
+
+ * + *

Change the sorting at any time by calling {@link #sort}:

+ * +

+store.sort('height', 'ASC');
+
+ * + *

Note that all existing sorters will be removed in favor of the new sorter data (if {@link #sort} is called with no arguments, + * the existing sorters are just reapplied instead of being removed). To keep existing sorters and add new ones, just add them + * to the MixedCollection:

+ * +

+store.sorters.add(new Ext.util.Sorter({
+    property : 'shoeSize',
+    direction: 'ASC'
+}));
+
+store.sort();
+
+ * + *

Registering with StoreManager

+ * + *

Any Store that is instantiated with a {@link #storeId} will automatically be registed with the {@link Ext.data.StoreManager StoreManager}. + * This makes it easy to reuse the same store in multiple views:

+ * +

+//this store can be used several times
+new Ext.data.Store({
+    model: 'User',
+    storeId: 'usersStore'
+});
+
+new Ext.List({
+    store: 'usersStore',
+
+    //other config goes here
+});
+
+new Ext.view.View({
+    store: 'usersStore',
+
+    //other config goes here
+});
+
+ * + *

Further Reading

+ * + *

Stores are backed up by an ecosystem of classes that enables their operation. To gain a full understanding of these + * pieces and how they fit together, see:

+ * + * + * * @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. - * @xtype store + * @param {Object} config Optional config object */ -Ext.data.Store = Ext.extend(Ext.util.Observable, { +Ext.define('Ext.data.Store', { + extend: 'Ext.data.AbstractStore', + + alias: 'store.store', + + requires: ['Ext.ModelManager', 'Ext.data.Model', 'Ext.util.Grouper'], + uses: ['Ext.data.proxy.Memory'], + /** - * @cfg {String} storeId If passed, the id to use to register with the {@link Ext.StoreMgr StoreMgr}. - *

Note: if a (deprecated) {@link #id} is specified it will supersede the storeId - * assignment.

+ * @cfg {Boolean} remoteSort + * True to defer any sorting operation to the server. If false, sorting is done locally on the client. Defaults to false. */ + remoteSort: false, + /** - * @cfg {String} url If a {@link #proxy} is not specified the url will be used to - * implicitly configure a {@link Ext.data.HttpProxy HttpProxy} if an url is specified. - * Typically this option, or the {@link #data} option will be specified. + * @cfg {Boolean} remoteFilter + * True to defer any filtering operation to the server. If false, filtering is done locally on the client. Defaults to false. */ + remoteFilter: false, + /** - * @cfg {Boolean/Object} autoLoad If {@link #data} is not specified, and if autoLoad - * is true or an Object, this store's {@link #load} method is automatically called - * after creation. If the value of autoLoad is an Object, this Object will - * be passed to the store's {@link #load} method. + * @cfg {Boolean} remoteGroup + * True if the grouping should apply on the server side, false if it is local only (defaults to false). If the + * grouping is local, it can be applied immediately to the data. If it is remote, then it will simply act as a + * helper, automatically sending the grouping information to the server. */ + remoteGroup : false, + /** - * @cfg {Ext.data.DataProxy} proxy The {@link Ext.data.DataProxy DataProxy} object which provides - * access to a data object. See {@link #url}. + * @cfg {String/Ext.data.proxy.Proxy/Object} proxy The Proxy to use for this Store. This can be either a string, a config + * object or a Proxy instance - see {@link #setProxy} for details. */ + /** - * @cfg {Array} data An inline data object readable by the {@link #reader}. - * Typically this option, or the {@link #url} option will be specified. + * @cfg {Array} data Optional array of Model instances or data objects to load locally. See "Inline data" above for details. */ + /** - * @cfg {Ext.data.DataReader} reader The {@link Ext.data.DataReader Reader} object which processes the - * data object and returns an Array of {@link Ext.data.Record} objects which are cached keyed by their - * {@link Ext.data.Record#id id} property. + * @cfg {String} model The {@link Ext.data.Model} associated with this store */ - /** - * @cfg {Ext.data.DataWriter} writer - *

The {@link Ext.data.DataWriter Writer} object which processes a record object for being written - * to the server-side database.

- *

When a writer is installed into a Store the {@link #add}, {@link #remove}, and {@link #update} - * events on the store are monitored in order to remotely {@link #createRecords create records}, - * {@link #destroyRecord destroy records}, or {@link #updateRecord update records}.

- *

The proxy for this store will relay any {@link #writexception} events to this store.

- *

Sample implementation: - *


-var writer = new {@link Ext.data.JsonWriter}({
-    encode: true,
-    writeAllFields: true // write all fields, not just those that changed
-});
 
-// Typical Store collecting the Proxy, Reader and Writer together.
-var store = new Ext.data.Store({
-    storeId: 'user',
-    root: 'records',
-    proxy: proxy,
-    reader: reader,
-    writer: writer,     // <-- plug a DataWriter into the store just as you would a Reader
-    paramsAsHash: true,
-    autoSave: false    // <-- false to delay executing create, update, destroy requests
-                        //     until specifically told to do so.
-});
-     * 

- */ - writer : undefined, - /** - * @cfg {Object} baseParams - *

An object containing properties which are to be sent as parameters - * for every HTTP request.

- *

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

- *

Note: baseParams may be superseded by any params - * specified in a {@link #load} request, see {@link #load} - * for more details.

- * This property may be modified after creation using the {@link #setBaseParam} - * method. - * @property - */ - /** - * @cfg {Object} sortInfo A config object to specify the sort order in the request of a Store's - * {@link #load} operation. Note that for local sorting, the direction property is - * case-sensitive. See also {@link #remoteSort} and {@link #paramNames}. - * For example:

-sortInfo: {
-    field: 'fieldName',
-    direction: 'ASC' // or 'DESC' (case sensitive for local sorting)
-}
-
- */ /** - * @cfg {boolean} remoteSort true if sorting is to be handled by requesting the {@link #proxy 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 remoteSort is true, then clicking on a {@link Ext.grid.Column Grid Column}'s - * {@link Ext.grid.Column#header header} causes the current page to be requested from the server appending - * the following two parameters to the {@link #load params}:

+ * The (optional) 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 #sort}). Stores support a single + * level of grouping, and groups can be fetched via the {@link #getGroups} method. + * @property groupField + * @type String */ - remoteSort : false, + groupField: undefined, /** - * @cfg {Boolean} autoDestroy true to destroy the store when the component the store is bound - * to is destroyed (defaults to false). - *

Note: this should be set to true when using stores that are bound to only 1 component.

+ * The direction in which sorting should be applied when grouping. Defaults to "ASC" - the other supported value is "DESC" + * @property groupDir + * @type String */ - autoDestroy : false, + groupDir: "ASC", /** - * @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). See {@link #getModifiedRecords} - * for the accessor method to retrieve the modified records. + * The number of records considered to form a 'page'. This is used to power the built-in + * paging using the nextPage and previousPage functions. Defaults to 25. + * @property pageSize + * @type Number */ - pruneModifiedRecords : false, + pageSize: 25, /** - * Contains the last options object used as the parameter to the {@link #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 + * The page that the Store has most recently loaded (see {@link #loadPage}) + * @property currentPage + * @type Number */ - lastOptions : null, + currentPage: 1, /** - * @cfg {Boolean} autoSave - *

Defaults to true causing the store to automatically {@link #save} records to - * the server when a record is modified (ie: becomes 'dirty'). Specify false to manually call {@link #save} - * to send all modifiedRecords to the server.

- *

Note: each CRUD action will be sent as a separate request.

+ * @cfg {Boolean} clearOnPageLoad True to empty the store when loading another page via {@link #loadPage}, + * {@link #nextPage} or {@link #previousPage} (defaults to true). Setting to false keeps existing records, allowing + * large data sets to be loaded one page at a time but rendered all together. */ - autoSave : true, + clearOnPageLoad: true, /** - * @cfg {Boolean} batch - *

Defaults to true (unless {@link #restful}:true). Multiple - * requests for each CRUD action (CREATE, READ, UPDATE and DESTROY) will be combined - * and sent as one transaction. Only applies when {@link #autoSave} is set - * to false.

- *

If Store is RESTful, the DataProxy is also RESTful, and a unique transaction is - * generated for each record.

+ * True if the Store is currently loading via its Proxy + * @property loading + * @type Boolean + * @private */ - batch : true, + loading: false, /** - * @cfg {Boolean} restful - * Defaults to false. Set to true to have the Store and the set - * Proxy operate in a RESTful manner. The store will automatically generate GET, POST, - * PUT and DELETE requests to the server. The HTTP method used for any given CRUD - * action is described in {@link Ext.data.Api#restActions}. For additional information - * see {@link Ext.data.DataProxy#restful}. - *

Note: if {@link #restful}:true batch will - * internally be set to false.

+ * @cfg {Boolean} sortOnFilter For local filtering only, causes {@link #sort} to be called whenever {@link #filter} is called, + * causing the sorters to be reapplied after filtering. Defaults to true */ - restful: false, - + sortOnFilter: true, + /** - * @cfg {Object} paramNames - *

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 Store uses this property to determine - * the parameter names to use in its {@link #load requests}. + * @cfg {Boolean} buffered + * Allow the store to buffer and pre-fetch pages of records. This is to be used in conjunction with a view will + * tell the store to pre-fetch records ahead of a time. */ - paramNames : undefined, - + buffered: false, + /** - * @cfg {Object} defaultParamNames - * Provides the default values for the {@link #paramNames} property. To globally modify the parameters - * for all stores, this object should be changed on the store prototype. + * @cfg {Number} purgePageCount + * The number of pages to keep in the cache before purging additional records. A value of 0 indicates to never purge the prefetched data. + * This option is only relevant when the {@link #buffered} option is set to true. */ - defaultParamNames : { - start : 'start', - limit : 'limit', - sort : 'sort', - dir : 'dir' - }, - - isDestroyed: false, - hasMultiSort: false, - - // private - batchKey : '_ext_batch_', - - constructor : function(config){ - /** - * @property multiSort - * @type Boolean - * True if this store is currently sorted by more than one field/direction combination. - */ - - /** - * @property isDestroyed - * @type Boolean - * True if the store has been destroyed already. Read only - */ - - this.data = new Ext.util.MixedCollection(false); - this.data.getKey = function(o){ - return o.id; - }; - - - // temporary removed-records cache - this.removed = []; - - if(config && config.data){ - this.inlineData = config.data; - delete config.data; - } - - Ext.apply(this, config); - - /** - * See the {@link #baseParams corresponding configuration option} - * for a description of this property. - * To modify this property see {@link #setBaseParam}. - * @property - */ - this.baseParams = Ext.isObject(this.baseParams) ? this.baseParams : {}; + purgePageCount: 5, - this.paramNames = Ext.applyIf(this.paramNames || {}, this.defaultParamNames); + isStore: true, - if((this.url || this.api) && !this.proxy){ - this.proxy = new Ext.data.HttpProxy({url: this.url, api: this.api}); - } - // If Store is RESTful, so too is the DataProxy - if (this.restful === true && this.proxy) { - // When operating RESTfully, a unique transaction is generated for each record. - // TODO might want to allow implemention of faux REST where batch is possible using RESTful routes only. - this.batch = false; - Ext.data.Api.restify(this.proxy); - } - - if(this.reader){ // reader passed - if(!this.recordType){ - this.recordType = this.reader.recordType; - } - if(this.reader.onMetaChange){ - this.reader.onMetaChange = this.reader.onMetaChange.createSequence(this.onMetaChange, this); - } - if (this.writer) { // writer passed - if (this.writer instanceof(Ext.data.DataWriter) === false) { // <-- config-object instead of instance. - this.writer = this.buildWriter(this.writer); - } - this.writer.meta = this.reader.meta; - this.pruneModifiedRecords = true; - } - } - - /** - * The {@link Ext.data.Record Record} constructor as supplied to (or created by) the - * {@link Ext.data.DataReader Reader}. Read-only. - *

If the Reader was constructed by passing in an Array of {@link Ext.data.Field} definition objects, - * instead of a Record constructor, it will implicitly create a Record constructor from that Array (see - * {@link Ext.data.Record}.{@link Ext.data.Record#create create} for additional details).

- *

This property may be used to create new Records of the type held in this Store, for example:


-    // create the data store
-    var store = new Ext.data.ArrayStore({
-        autoDestroy: true,
-        fields: [
-           {name: 'company'},
-           {name: 'price', type: 'float'},
-           {name: 'change', type: 'float'},
-           {name: 'pctChange', type: 'float'},
-           {name: 'lastChange', type: 'date', dateFormat: 'n/j h:ia'}
-        ]
-    });
-    store.loadData(myData);
-
-    // create the Grid
-    var grid = new Ext.grid.EditorGridPanel({
-        store: store,
-        colModel: new Ext.grid.ColumnModel({
-            columns: [
-                {id:'company', header: 'Company', width: 160, dataIndex: 'company'},
-                {header: 'Price', renderer: 'usMoney', dataIndex: 'price'},
-                {header: 'Change', renderer: change, dataIndex: 'change'},
-                {header: '% Change', renderer: pctChange, dataIndex: 'pctChange'},
-                {header: 'Last Updated', width: 85,
-                    renderer: Ext.util.Format.dateRenderer('m/d/Y'),
-                    dataIndex: 'lastChange'}
-            ],
-            defaults: {
-                sortable: true,
-                width: 75
-            }
-        }),
-        autoExpandColumn: 'company', // match the id specified in the column model
-        height:350,
-        width:600,
-        title:'Array Grid',
-        tbar: [{
-            text: 'Add Record',
-            handler : function(){
-                var defaultData = {
-                    change: 0,
-                    company: 'New Company',
-                    lastChange: (new Date()).clearTime(),
-                    pctChange: 0,
-                    price: 10
-                };
-                var recId = 3; // provide unique id
-                var p = new store.recordType(defaultData, recId); // create new record
-                grid.stopEditing();
-                store.{@link #insert}(0, p); // insert a new record into the store (also see {@link #add})
-                grid.startEditing(0, 0);
-            }
-        }]
-    });
-         * 
- * @property recordType - * @type Function - */ + //documented above + constructor: function(config) { + config = config || {}; - if(this.recordType){ - /** - * A {@link Ext.util.MixedCollection MixedCollection} containing the defined {@link Ext.data.Field Field}s - * for the {@link Ext.data.Record Records} stored in this Store. Read-only. - * @property fields - * @type Ext.util.MixedCollection - */ - this.fields = this.recordType.prototype.fields; + var me = this, + groupers = config.groupers, + proxy, + data; + + if (config.buffered || me.buffered) { + me.prefetchData = Ext.create('Ext.util.MixedCollection', false, function(record) { + return record.index; + }); + me.pendingRequests = []; + me.pagesRequested = []; + + me.sortOnLoad = false; + me.filterOnLoad = false; } - 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 {@link #add}ed 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 {@link #remove}d 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 - * @param {Record[]} records The records that were cleared. - */ - 'clear', + + me.addEvents( /** - * @event exception - *

Fires if an exception occurs in the Proxy during a remote request. - * This event is relayed through the corresponding {@link Ext.data.DataProxy}. - * See {@link Ext.data.DataProxy}.{@link Ext.data.DataProxy#exception exception} - * for additional details. - * @param {misc} misc See {@link Ext.data.DataProxy}.{@link Ext.data.DataProxy#exception exception} - * for description. + * @event beforeprefetch + * Fires before a prefetch occurs. Return false to cancel. + * @param {Ext.data.store} this + * @param {Ext.data.Operation} operation The associated operation */ - 'exception', + 'beforeprefetch', /** - * @event beforeload - * Fires before a request is made for a new data object. If the beforeload handler returns - * false the {@link #load} action will be canceled. - * @param {Store} this - * @param {Object} options The loading options that were specified (see {@link #load} for details) + * @event groupchange + * Fired whenever the grouping in the grid changes + * @param {Ext.data.Store} store The store + * @param {Array} groupers The array of grouper objects */ - 'beforeload', + 'groupchange', /** * @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 - *

This event is deprecated in favor of the catch-all {@link #exception} - * event instead.

- *

This event is relayed through the corresponding {@link Ext.data.DataProxy}. - * See {@link Ext.data.DataProxy}.{@link Ext.data.DataProxy#loadexception loadexception} - * for additional details. - * @param {misc} misc See {@link Ext.data.DataProxy}.{@link Ext.data.DataProxy#loadexception loadexception} - * for description. + * Fires whenever records have been prefetched + * @param {Ext.data.store} this + * @param {Array} records An array of records + * @param {Boolean} successful True if the operation was successful. + * @param {Ext.data.Operation} operation The associated operation */ - 'loadexception', - /** - * @event beforewrite - * @param {Ext.data.Store} store - * @param {String} action [Ext.data.Api.actions.create|update|destroy] - * @param {Record/Record[]} rs The Record(s) being written. - * @param {Object} options The loading options that were specified. Edit options.params to add Http parameters to the request. (see {@link #save} for details) - * @param {Object} arg The callback's arg object passed to the {@link #request} function - */ - 'beforewrite', - /** - * @event write - * Fires if the server returns 200 after an Ext.data.Api.actions CRUD action. - * Success of the action is determined in the result['successProperty']property (NOTE for RESTful stores, - * a simple 20x response is sufficient for the actions "destroy" and "update". The "create" action should should return 200 along with a database pk). - * @param {Ext.data.Store} store - * @param {String} action [Ext.data.Api.actions.create|update|destroy] - * @param {Object} result The 'data' picked-out out of the response for convenience. - * @param {Ext.Direct.Transaction} res - * @param {Record/Record[]} rs Store's records, the subject(s) of the write-action - */ - 'write', - /** - * @event beforesave - * Fires before a save action is called. A save encompasses destroying records, updating records and creating records. - * @param {Ext.data.Store} store - * @param {Object} data An object containing the data that is to be saved. The object will contain a key for each appropriate action, - * with an array of records for each action. - */ - 'beforesave', - /** - * @event save - * Fires after a save is completed. A save encompasses destroying records, updating records and creating records. - * @param {Ext.data.Store} store - * @param {Number} batch The identifier for the batch that was saved. - * @param {Object} data An object containing the data that is to be saved. The object will contain a key for each appropriate action, - * with an array of records for each action. - */ - 'save' - + 'prefetch' ); + data = config.data || me.data; - if(this.proxy){ - // TODO remove deprecated loadexception with ext-3.0.1 - this.relayEvents(this.proxy, ['loadexception', 'exception']); - } - // With a writer set for the Store, we want to listen to add/remove events to remotely create/destroy records. - if (this.writer) { - this.on({ - scope: this, - add: this.createRecords, - remove: this.destroyRecord, - update: this.updateRecord, - clear: this.onClear - }); + /** + * The MixedCollection that holds this store's local cache of records + * @property data + * @type Ext.util.MixedCollection + */ + me.data = Ext.create('Ext.util.MixedCollection', false, function(record) { + return record.internalId; + }); + + if (data) { + me.inlineData = data; + delete config.data; + } + + if (!groupers && config.groupField) { + groupers = [{ + property : config.groupField, + direction: config.groupDir + }]; } + delete config.groupers; + + /** + * The collection of {@link Ext.util.Grouper Groupers} currently applied to this Store + * @property groupers + * @type Ext.util.MixedCollection + */ + me.groupers = Ext.create('Ext.util.MixedCollection'); + me.groupers.addAll(me.decodeGroupers(groupers)); - this.sortToggle = {}; - if(this.sortField){ - this.setDefaultSort(this.sortField, this.sortDir); - }else if(this.sortInfo){ - this.setDefaultSort(this.sortInfo.field, this.sortInfo.direction); + this.callParent([config]); + + if (me.groupers.items.length) { + me.sort(me.groupers.items, 'prepend', false); } - Ext.data.Store.superclass.constructor.call(this); + proxy = me.proxy; + data = me.inlineData; - if(this.id){ - this.storeId = this.id; - delete this.id; - } - if(this.storeId){ - 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]); + if (data) { + if (proxy instanceof Ext.data.proxy.Memory) { + proxy.data = data; + me.read(); + } else { + me.add.apply(me, data); + } + + me.sort(); + delete me.inlineData; + } else if (me.autoLoad) { + Ext.defer(me.load, 10, me, [typeof me.autoLoad === 'object' ? me.autoLoad: undefined]); + // Remove the defer call, we may need reinstate this at some point, but currently it's not obvious why it's here. + // this.load(typeof this.autoLoad == 'object' ? this.autoLoad : undefined); } - // used internally to uniquely identify a batch - this.batchCounter = 0; - this.batches = {}; }, - + + onBeforeSort: function() { + this.sort(this.groupers.items, 'prepend', false); + }, + /** - * builds a DataWriter instance when Store constructor is provided with a writer config-object instead of an instace. - * @param {Object} config Writer configuration - * @return {Ext.data.DataWriter} * @private - */ - buildWriter : function(config) { - var klass = undefined, - type = (config.format || 'json').toLowerCase(); - switch (type) { - case 'json': - klass = Ext.data.JsonWriter; - break; - case 'xml': - klass = Ext.data.XmlWriter; - break; - default: - klass = Ext.data.JsonWriter; + * Normalizes an array of grouper objects, ensuring that they are all Ext.util.Grouper instances + * @param {Array} groupers The groupers array + * @return {Array} Array of Ext.util.Grouper objects + */ + decodeGroupers: function(groupers) { + if (!Ext.isArray(groupers)) { + if (groupers === undefined) { + groupers = []; + } else { + groupers = [groupers]; + } } - return new klass(config); - }, - /** - * Destroys the store. - */ - destroy : function(){ - if(!this.isDestroyed){ - if(this.storeId){ - Ext.StoreMgr.unregister(this); + var length = groupers.length, + Grouper = Ext.util.Grouper, + config, i; + + for (i = 0; i < length; i++) { + config = groupers[i]; + + if (!(config instanceof Grouper)) { + if (Ext.isString(config)) { + config = { + property: config + }; + } + + Ext.applyIf(config, { + root : 'data', + direction: "ASC" + }); + + //support for 3.x style sorters where a function can be defined as 'fn' + if (config.fn) { + config.sorterFn = config.fn; + } + + //support a function to be passed as a sorter definition + if (typeof config == 'function') { + config = { + sorterFn: config + }; + } + + groupers[i] = new Grouper(config); } - this.clearData(); - this.data = null; - Ext.destroy(this.proxy); - this.reader = this.writer = null; - this.purgeListeners(); - this.isDestroyed = true; } - }, + return groupers; + }, + /** - * Add Records to the Store and fires the {@link #add} event. To add Records - * to the store from a remote source use {@link #load}({add:true}). - * See also {@link #recordType} and {@link #insert}. - * @param {Ext.data.Record[]} records An Array of Ext.data.Record objects - * to add to the cache. See {@link #recordType}. + * Group data in the store + * @param {String|Array} groupers Either a string name of one of the fields in this Store's configured {@link Ext.data.Model Model}, + * or an Array of grouper configurations. + * @param {String} direction The overall direction to group the data by. Defaults to "ASC". */ - add : function(records) { - var i, len, record, index; - - records = [].concat(records); - if (records.length < 1) { - return; - } - - for (i = 0, len = records.length; i < len; i++) { - record = records[i]; - - record.join(this); + group: function(groupers, direction) { + var me = this, + grouper, + newGroupers; - if (record.dirty || record.phantom) { - this.modified.push(record); + if (Ext.isArray(groupers)) { + newGroupers = groupers; + } else if (Ext.isObject(groupers)) { + newGroupers = [groupers]; + } else if (Ext.isString(groupers)) { + grouper = me.groupers.get(groupers); + + if (!grouper) { + grouper = { + property : groupers, + direction: direction + }; + newGroupers = [grouper]; + } else if (direction === undefined) { + grouper.toggle(); + } else { + grouper.setDirection(direction); } } - index = this.data.length; - this.data.addAll(records); - - if (this.snapshot) { - this.snapshot.addAll(records); + if (newGroupers && newGroupers.length) { + newGroupers = me.decodeGroupers(newGroupers); + me.groupers.clear(); + me.groupers.addAll(newGroupers); } - this.fireEvent('add', this, records, index); + if (me.remoteGroup) { + me.load({ + scope: me, + callback: me.fireGroupChange + }); + } else { + me.sort(); + me.fireEvent('groupchange', me, me.groupers); + } }, - + + /** + * Clear any groupers in the store + */ + clearGrouping: function(){ + var me = this; + // Clear any groupers we pushed on to the sorters + me.groupers.each(function(grouper){ + me.sorters.remove(grouper); + }); + me.groupers.clear(); + if (me.remoteGroup) { + me.load({ + scope: me, + callback: me.fireGroupChange + }); + } else { + me.sort(); + me.fireEvent('groupchange', me, me.groupers); + } + }, + /** - * (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 + * Checks if the store is currently grouped + * @return {Boolean} True if the store is grouped. */ - addSorted : function(record){ - var index = this.findInsertIndex(record); - this.insert(index, record); + isGrouped: function() { + return this.groupers.getCount() > 0; }, /** + * Fires the groupchange event. Abstracted out so we can use it + * as a callback * @private - * Update a record within the store with a new reference */ - doUpdate : function(rec){ - this.data.replace(rec.id, rec); - if(this.snapshot){ - this.snapshot.replace(rec.id, rec); - } - this.fireEvent('update', this, rec, Ext.data.Record.COMMIT); + fireGroupChange: function(){ + this.fireEvent('groupchange', this, this.groupers); }, /** - * Remove Records from the Store and fires the {@link #remove} event. - * @param {Ext.data.Record/Ext.data.Record[]} record The record object or array of records to remove from the cache. - */ - remove : function(record){ - if(Ext.isArray(record)){ - Ext.each(record, function(r){ - this.remove(r); - }, this); - return; - } - var index = this.data.indexOf(record); - if(index > -1){ - record.join(null); - this.data.removeAt(index); + * Returns an object containing the result of applying grouping to the records in this store. See {@link #groupField}, + * {@link #groupDir} and {@link #getGroupString}. Example for a store containing records with a color field: +


+var myStore = new Ext.data.Store({
+    groupField: 'color',
+    groupDir  : 'DESC'
+});
+
+myStore.getGroups(); //returns:
+[
+    {
+        name: 'yellow',
+        children: [
+            //all records where the color field is 'yellow'
+        ]
+    },
+    {
+        name: 'red',
+        children: [
+            //all records where the color field is 'red'
+        ]
+    }
+]
+
+ * @param {String} groupName (Optional) Pass in an optional groupName argument to access a specific group as defined by {@link #getGroupString} + * @return {Array} The grouped data + */ + getGroups: function(requestGroupString) { + var records = this.data.items, + length = records.length, + groups = [], + pointers = {}, + record, + groupStr, + group, + i; + + for (i = 0; i < length; i++) { + record = records[i]; + groupStr = this.getGroupString(record); + group = pointers[groupStr]; + + if (group === undefined) { + group = { + name: groupStr, + children: [] + }; + + groups.push(group); + pointers[groupStr] = group; + } + + group.children.push(record); } - if(this.pruneModifiedRecords){ - this.modified.remove(record); + + return requestGroupString ? pointers[requestGroupString] : groups; + }, + + /** + * @private + * For a given set of records and a Grouper, returns an array of arrays - each of which is the set of records + * matching a certain group. + */ + getGroupsForGrouper: function(records, grouper) { + var length = records.length, + groups = [], + oldValue, + newValue, + record, + group, + i; + + for (i = 0; i < length; i++) { + record = records[i]; + newValue = grouper.getGroupString(record); + + if (newValue !== oldValue) { + group = { + name: newValue, + grouper: grouper, + records: [] + }; + groups.push(group); + } + + group.records.push(record); + + oldValue = newValue; } - if(this.snapshot){ - this.snapshot.remove(record); + + return groups; + }, + + /** + * @private + * This is used recursively to gather the records into the configured Groupers. The data MUST have been sorted for + * this to work properly (see {@link #getGroupData} and {@link #getGroupsForGrouper}) Most of the work is done by + * {@link #getGroupsForGrouper} - this function largely just handles the recursion. + * @param {Array} records The set or subset of records to group + * @param {Number} grouperIndex The grouper index to retrieve + * @return {Array} The grouped records + */ + getGroupsForGrouperIndex: function(records, grouperIndex) { + var me = this, + groupers = me.groupers, + grouper = groupers.getAt(grouperIndex), + groups = me.getGroupsForGrouper(records, grouper), + length = groups.length, + i; + + if (grouperIndex + 1 < groupers.length) { + for (i = 0; i < length; i++) { + groups[i].children = me.getGroupsForGrouperIndex(groups[i].records, grouperIndex + 1); + } } - if(index > -1){ - this.fireEvent('remove', this, record, index); + + for (i = 0; i < length; i++) { + groups[i].depth = grouperIndex; } + + return groups; }, /** - * 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. + * @private + *

Returns records grouped by the configured {@link #groupers grouper} configuration. Sample return value (in + * this case grouping by genre and then author in a fictional books dataset):

+

+[
+    {
+        name: 'Fantasy',
+        depth: 0,
+        records: [
+            //book1, book2, book3, book4
+        ],
+        children: [
+            {
+                name: 'Rowling',
+                depth: 1,
+                records: [
+                    //book1, book2
+                ]
+            },
+            {
+                name: 'Tolkein',
+                depth: 1,
+                records: [
+                    //book3, book4
+                ]
+            }
+        ]
+    }
+]
+
+ * @param {Boolean} sort True to call {@link #sort} before finding groups. Sorting is required to make grouping + * function correctly so this should only be set to false if the Store is known to already be sorted correctly + * (defaults to true) + * @return {Array} The group data */ - removeAt : function(index){ - this.remove(this.getAt(index)); + getGroupData: function(sort) { + var me = this; + if (sort !== false) { + me.sort(); + } + + return me.getGroupsForGrouperIndex(me.data.items, 0); }, /** - * Remove all Records from the Store and fires the {@link #clear} event. - * @param {Boolean} silent [false] Defaults to false. Set true to not fire clear event. + *

Returns the string to group on for a given model instance. The default implementation of this method returns + * the model's {@link #groupField}, but this can be overridden to group by an arbitrary string. For example, to + * group by the first letter of a model's 'name' field, use the following code:

+

+new Ext.data.Store({
+    groupDir: 'ASC',
+    getGroupString: function(instance) {
+        return instance.get('name')[0];
+    }
+});
+
+ * @param {Ext.data.Model} instance The model instance + * @return {String} The string to compare when forming groups */ - removeAll : function(silent){ - var items = []; - this.each(function(rec){ - items.push(rec); - }); - this.clearData(); - if(this.snapshot){ - this.snapshot.clear(); - } - if(this.pruneModifiedRecords){ - this.modified = []; - } - if (silent !== true) { // <-- prevents write-actions when we just want to clear a store. - this.fireEvent('clear', this, items); + getGroupString: function(instance) { + var group = this.groupers.first(); + if (group) { + return instance.get(group.property); } + return ''; }, - - // private - onClear: function(store, records){ - Ext.each(records, function(rec, index){ - this.destroyRecord(this, rec, index); - }, this); - }, - /** - * Inserts Records into the Store at the given index and fires the {@link #add} event. - * See also {@link #add} and {@link #addSorted}. + * Inserts Model instances into the Store at the given index and fires the {@link #add} event. + * See also {@link #add}. * @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. + * @param {Ext.data.Model[]} records An Array of Ext.data.Model objects to add to the cache. */ - insert : function(index, records) { - var i, len, record; - + insert: function(index, records) { + var me = this, + sync = false, + i, + record, + len; + records = [].concat(records); for (i = 0, len = records.length; i < len; i++) { - record = records[i]; + record = me.createModel(records[i]); + record.set(me.modelDefaults); + // reassign the model in the array in case it wasn't created yet + records[i] = record; - this.data.insert(index + i, record); - record.join(this); - - if (record.dirty || record.phantom) { - this.modified.push(record); - } + me.data.insert(index + i, record); + record.join(me); + + sync = sync || record.phantom === true; } - - if (this.snapshot) { - this.snapshot.addAll(records); + + if (me.snapshot) { + me.snapshot.addAll(records); } - - 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); + me.fireEvent('add', me, records, index); + me.fireEvent('datachanged', me); + if (me.autoSync && sync) { + me.sync(); + } }, /** - * 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. + * Adds Model instances to the Store by instantiating them based on a JavaScript object. When adding already- + * instantiated Models, use {@link #insert} instead. The instances will be added at the end of the existing collection. + * This method accepts either a single argument array of Model instances or any number of model instance arguments. + * Sample usage: + * +

+myStore.add({some: 'data'}, {some: 'other data'});
+
+ * + * @param {Object} data The data for each model + * @return {Array} The array of newly created model instances */ - indexOfId : function(id){ - return this.data.indexOfKey(id); - }, + add: function(records) { + //accept both a single-argument array of records, or any number of record arguments + if (!Ext.isArray(records)) { + records = Array.prototype.slice.apply(arguments); + } - /** - * 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.snapshot || this.data).key(id); - }, + var me = this, + i = 0, + length = records.length, + record; - /** - * 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); + for (; i < length; i++) { + record = me.createModel(records[i]); + // reassign the model in the array in case it wasn't created yet + records[i] = record; + } + + me.insert(me.data.length, records); + + return records; }, /** - * 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 + * Converts a literal to a model, if it's not a model already + * @private + * @param record {Ext.data.Model/Object} The record to create + * @return {Ext.data.Model} */ - 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; - }, + createModel: function(record) { + if (!record.isModel) { + record = Ext.ModelManager.create(record, this.model); + } - // private - clearData: function(){ - this.data.each(function(rec) { - rec.join(null); - }); - this.data.clear(); + return record; }, /** - *

Loads the Record cache from the configured {@link #proxy} using the configured {@link #reader}.

- *

Notes:

- * @param {Object} options An object containing properties which control loading options: - * @return {Boolean} If the developer provided {@link #beforeload} event handler returns - * false, the load call will abort and will return false; otherwise will return true. - */ - load : function(options) { - options = Ext.apply({}, options); - this.storeOptions(options); - if(this.sortInfo && this.remoteSort){ - var pn = this.paramNames; - options.params = Ext.apply({}, options.params); - options.params[pn.sort] = this.sortInfo.field; - options.params[pn.dir] = this.sortInfo.direction; - } - try { - return this.execute('read', null, options); // <-- null represents rs. No rs for load actions. - } catch(e) { - this.handleException(e); - return false; - } + * Calls the specified function for each of the {@link Ext.data.Model Records} in the cache. + * @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 (optional) The scope (this reference) in which the function is executed. + * Defaults to the current {@link Ext.data.Model Record} in the iteration. + */ + each: function(fn, scope) { + this.data.each(fn, scope); }, /** - * updateRecord Should not be used directly. This method will be called automatically if a Writer is set. - * Listens to 'update' event. - * @param {Object} store - * @param {Object} record - * @param {Object} action - * @private + * Removes the given record from the Store, firing the 'remove' event for each instance that is removed, plus a single + * 'datachanged' event after removal. + * @param {Ext.data.Model/Array} records The Ext.data.Model instance or array of instances to remove */ - updateRecord : function(store, record, action) { - if (action == Ext.data.Record.EDIT && this.autoSave === true && (!record.phantom || (record.phantom && record.isValid()))) { - this.save(); + remove: function(records, /* private */ isMove) { + if (!Ext.isArray(records)) { + records = [records]; } - }, - /** - * @private - * Should not be used directly. Store#add will call this automatically if a Writer is set - * @param {Object} store - * @param {Object} records - * @param {Object} index - */ - createRecords : function(store, records, index) { - var modified = this.modified, - length = records.length, - record, i; - - for (i = 0; i < length; i++) { + /* + * Pass the isMove parameter if we know we're going to be re-inserting this record + */ + isMove = isMove === true; + var me = this, + sync = false, + i = 0, + length = records.length, + isPhantom, + index, + record; + + for (; i < length; i++) { record = records[i]; + index = me.data.indexOf(record); - if (record.phantom && record.isValid()) { - record.markDirty(); // <-- Mark new records dirty (Ed: why?) - - if (modified.indexOf(record) == -1) { - modified.push(record); + if (me.snapshot) { + me.snapshot.remove(record); + } + + if (index > -1) { + isPhantom = record.phantom === true; + if (!isMove && !isPhantom) { + // don't push phantom records onto removed + me.removed.push(record); } + + record.unjoin(me); + me.data.remove(record); + sync = sync || !isPhantom; + + me.fireEvent('remove', me, record, index); } } - if (this.autoSave === true) { - this.save(); + + me.fireEvent('datachanged', me); + if (!isMove && me.autoSync && sync) { + me.sync(); } }, /** - * Destroys a Record. Should not be used directly. It's called by Store#remove if a Writer is set. - * @param {Store} store this - * @param {Ext.data.Record} record - * @param {Number} index - * @private + * Removes the model instance at the given index + * @param {Number} index The record index */ - destroyRecord : function(store, record, index) { - if (this.modified.indexOf(record) != -1) { // <-- handled already if @cfg pruneModifiedRecords == true - this.modified.remove(record); + removeAt: function(index) { + var record = this.getAt(index); + + if (record) { + this.remove(record); } - if (!record.phantom) { - this.removed.push(record); + }, - // since the record has already been removed from the store but the server request has not yet been executed, - // must keep track of the last known index this record existed. If a server error occurs, the record can be - // put back into the store. @see Store#createCallback where the record is returned when response status === false - record.lastIndex = index; + /** + *

Loads data into the Store via the configured {@link #proxy}. This uses the Proxy to make an + * asynchronous call to whatever storage backend the Proxy uses, automatically adding the retrieved + * instances into the Store and calling an optional callback if required. Example usage:

+ * +

+store.load({
+    scope   : this,
+    callback: function(records, operation, success) {
+        //the {@link Ext.data.Operation operation} object contains all of the details of the load operation
+        console.log(records);
+    }
+});
+
+ * + *

If the callback scope does not need to be set, a function can simply be passed:

+ * +

+store.load(function(records, operation, success) {
+    console.log('loaded records');
+});
+
+ * + * @param {Object/Function} options Optional config object, passed into the Ext.data.Operation object before loading. + */ + load: function(options) { + var me = this; + + options = options || {}; - if (this.autoSave === true) { - this.save(); - } + if (Ext.isFunction(options)) { + options = { + callback: options + }; } + + Ext.applyIf(options, { + groupers: me.groupers.items, + page: me.currentPage, + start: (me.currentPage - 1) * me.pageSize, + limit: me.pageSize, + addRecords: false + }); + + return me.callParent([options]); }, /** - * This method should generally not be used directly. This method is called internally - * by {@link #load}, or if a Writer is set will be called automatically when {@link #add}, - * {@link #remove}, or {@link #update} events fire. - * @param {String} action Action name ('read', 'create', 'update', or 'destroy') - * @param {Record/Record[]} rs - * @param {Object} options - * @throws Error * @private + * Called internally when a Proxy has completed a load request */ - execute : function(action, rs, options, /* private */ batch) { - // blow up if action not Ext.data.CREATE, READ, UPDATE, DESTROY - if (!Ext.data.Api.isAction(action)) { - throw new Ext.data.Api.Error('execute', action); + onProxyLoad: function(operation) { + var me = this, + resultSet = operation.getResultSet(), + records = operation.getRecords(), + successful = operation.wasSuccessful(); + + if (resultSet) { + me.totalCount = resultSet.total; } - // make sure options has a fresh, new params hash - options = Ext.applyIf(options||{}, { - params: {} - }); - if(batch !== undefined){ - this.addToBatch(batch); - } - // have to separate before-events since load has a different signature than create,destroy and save events since load does not - // include the rs (record resultset) parameter. Capture return values from the beforeaction into doRequest flag. - var doRequest = true; - - if (action === 'read') { - doRequest = this.fireEvent('beforeload', this, options); - Ext.applyIf(options.params, this.baseParams); - } - else { - // if Writer is configured as listful, force single-record rs to be [{}] instead of {} - // TODO Move listful rendering into DataWriter where the @cfg is defined. Should be easy now. - if (this.writer.listful === true && this.restful !== true) { - rs = (Ext.isArray(rs)) ? rs : [rs]; - } - // if rs has just a single record, shift it off so that Writer writes data as '{}' rather than '[{}]' - else if (Ext.isArray(rs) && rs.length == 1) { - rs = rs.shift(); - } - // Write the action to options.params - if ((doRequest = this.fireEvent('beforewrite', this, action, rs, options)) !== false) { - this.writer.apply(options.params, this.baseParams, action, rs); - } + + if (successful) { + me.loadRecords(records, operation); } - if (doRequest !== false) { - // Send request to proxy. - if (this.writer && this.proxy.url && !this.proxy.restful && !Ext.data.Api.hasUniqueUrl(this.proxy, action)) { - options.params.xaction = action; // <-- really old, probaby unecessary. - } - // Note: Up until this point we've been dealing with 'action' as a key from Ext.data.Api.actions. - // We'll flip it now and send the value into DataProxy#request, since it's the value which maps to - // the user's configured DataProxy#api - // TODO Refactor all Proxies to accept an instance of Ext.data.Request (not yet defined) instead of this looooooong list - // of params. This method is an artifact from Ext2. - this.proxy.request(Ext.data.Api.actions[action], rs, options.params, this.reader, this.createCallback(action, rs, batch), this, options); - } - return doRequest; - }, - - /** - * Saves all pending changes to the store. If the commensurate Ext.data.Api.actions action is not configured, then - * the configured {@link #url} will be used. - *
-     * change            url
-     * ---------------   --------------------
-     * removed records   Ext.data.Api.actions.destroy
-     * phantom records   Ext.data.Api.actions.create
-     * {@link #getModifiedRecords modified records}  Ext.data.Api.actions.update
-     * 
- * @TODO: Create extensions of Error class and send associated Record with thrown exceptions. - * e.g.: Ext.data.DataReader.Error or Ext.data.Error or Ext.data.DataProxy.Error, etc. - * @return {Number} batch Returns a number to uniquely identify the "batch" of saves occurring. -1 will be returned - * if there are no items to save or the save was cancelled. - */ - save : function() { - if (!this.writer) { - throw new Ext.data.Store.Error('writer-undefined'); - } - - var queue = [], - len, - trans, - batch, - data = {}, - i; - // DESTROY: First check for removed records. Records in this.removed are guaranteed non-phantoms. @see Store#remove - if(this.removed.length){ - queue.push(['destroy', this.removed]); - } - - // Check for modified records. Use a copy so Store#rejectChanges will work if server returns error. - var rs = [].concat(this.getModifiedRecords()); - if(rs.length){ - // CREATE: Next check for phantoms within rs. splice-off and execute create. - var phantoms = []; - for(i = rs.length-1; i >= 0; i--){ - if(rs[i].phantom === true){ - var rec = rs.splice(i, 1).shift(); - if(rec.isValid()){ - phantoms.push(rec); + + me.loading = false; + me.fireEvent('load', me, records, successful); + + //TODO: deprecate this event, it should always have been 'load' instead. 'load' is now documented, 'read' is not. + //People are definitely using this so can't deprecate safely until 2.x + me.fireEvent('read', me, records, operation.wasSuccessful()); + + //this is a callback that would have been passed to the 'read' function and is optional + Ext.callback(operation.callback, operation.scope || me, [records, operation, successful]); + }, + + /** + * Create any new records when a write is returned from the server. + * @private + * @param {Array} records The array of new records + * @param {Ext.data.Operation} operation The operation that just completed + * @param {Boolean} success True if the operation was successful + */ + onCreateRecords: function(records, operation, success) { + if (success) { + var i = 0, + data = this.data, + snapshot = this.snapshot, + length = records.length, + originalRecords = operation.records, + record, + original, + index; + + /** + * Loop over each record returned from the server. Assume they are + * returned in order of how they were sent. If we find a matching + * record, replace it with the newly created one. + */ + for (; i < length; ++i) { + record = records[i]; + original = originalRecords[i]; + if (original) { + index = data.indexOf(original); + if (index > -1) { + data.removeAt(index); + data.insert(index, record); + } + if (snapshot) { + index = snapshot.indexOf(original); + if (index > -1) { + snapshot.removeAt(index); + snapshot.insert(index, record); + } } - }else if(!rs[i].isValid()){ // <-- while we're here, splice-off any !isValid real records - rs.splice(i,1); + record.phantom = false; + record.join(this); } } - // If we have valid phantoms, create them... - if(phantoms.length){ - queue.push(['create', phantoms]); - } - - // UPDATE: And finally, if we're still here after splicing-off phantoms and !isValid real records, update the rest... - if(rs.length){ - queue.push(['update', rs]); - } } - len = queue.length; - if(len){ - batch = ++this.batchCounter; - for(i = 0; i < len; ++i){ - trans = queue[i]; - data[trans[0]] = trans[1]; - } - if(this.fireEvent('beforesave', this, data) !== false){ - for(i = 0; i < len; ++i){ - trans = queue[i]; - this.doTransaction(trans[0], trans[1], batch); + }, + + /** + * Update any records when a write is returned from the server. + * @private + * @param {Array} records The array of updated records + * @param {Ext.data.Operation} operation The operation that just completed + * @param {Boolean} success True if the operation was successful + */ + onUpdateRecords: function(records, operation, success){ + if (success) { + var i = 0, + length = records.length, + data = this.data, + snapshot = this.snapshot, + record; + + for (; i < length; ++i) { + record = records[i]; + data.replace(record); + if (snapshot) { + snapshot.replace(record); } - return batch; + record.join(this); } } - return -1; }, - // private. Simply wraps call to Store#execute in try/catch. Defers to Store#handleException on error. Loops if batch: false - doTransaction : function(action, rs, batch) { - function transaction(records) { - try{ - this.execute(action, records, undefined, batch); - }catch (e){ - this.handleException(e); - } - } - if(this.batch === false){ - for(var i = 0, len = rs.length; i < len; i++){ - transaction.call(this, rs[i]); + /** + * Remove any records when a write is returned from the server. + * @private + * @param {Array} records The array of removed records + * @param {Ext.data.Operation} operation The operation that just completed + * @param {Boolean} success True if the operation was successful + */ + onDestroyRecords: function(records, operation, success){ + if (success) { + var me = this, + i = 0, + length = records.length, + data = me.data, + snapshot = me.snapshot, + record; + + for (; i < length; ++i) { + record = records[i]; + record.unjoin(me); + data.remove(record); + if (snapshot) { + snapshot.remove(record); + } } - }else{ - transaction.call(this, rs); + me.removed = []; } }, - // private - addToBatch : function(batch){ - var b = this.batches, - key = this.batchKey + batch, - o = b[key]; - - if(!o){ - b[key] = o = { - id: batch, - count: 0, - data: {} + //inherit docs + getNewRecords: function() { + return this.data.filterBy(this.filterNew).items; + }, + + //inherit docs + getUpdatedRecords: function() { + return this.data.filterBy(this.filterUpdated).items; + }, + + /** + * Filters the loaded set of records by a given set of filters. + * @param {Mixed} filters The set of filters to apply to the data. These are stored internally on the store, + * but the filtering itself is done on the Store's {@link Ext.util.MixedCollection MixedCollection}. See + * MixedCollection's {@link Ext.util.MixedCollection#filter filter} method for filter syntax. Alternatively, + * pass in a property string + * @param {String} value Optional value to filter by (only if using a property string as the first argument) + */ + filter: function(filters, value) { + if (Ext.isString(filters)) { + filters = { + property: filters, + value: value }; } - ++o.count; - }, - removeFromBatch : function(batch, action, data){ - var b = this.batches, - key = this.batchKey + batch, - o = b[key], - arr; + var me = this, + decoded = me.decodeFilters(filters), + i = 0, + doLocalSort = me.sortOnFilter && !me.remoteSort, + length = decoded.length; + + for (; i < length; i++) { + me.filters.replace(decoded[i]); + } + if (me.remoteFilter) { + //the load function will pick up the new filters and request the filtered data from the proxy + me.load(); + } else { + /** + * A pristine (unfiltered) collection of the records in this store. This is used to reinstate + * records when a filter is removed or changed + * @property snapshot + * @type Ext.util.MixedCollection + */ + if (me.filters.getCount()) { + me.snapshot = me.snapshot || me.data.clone(); + me.data = me.data.filter(me.filters.items); - if(o){ - arr = o.data[action] || []; - o.data[action] = arr.concat(data); - if(o.count === 1){ - data = o.data; - delete b[key]; - this.fireEvent('save', this, batch, data); - }else{ - --o.count; + if (doLocalSort) { + me.sort(); + } + // fire datachanged event if it hasn't already been fired by doSort + if (!doLocalSort || me.sorters.length < 1) { + me.fireEvent('datachanged', me); + } } } }, - // @private callback-handler for remote CRUD actions - // Do not override -- override loadRecords, onCreateRecords, onDestroyRecords and onUpdateRecords instead. - createCallback : function(action, rs, batch) { - var actions = Ext.data.Api.actions; - return (action == 'read') ? this.loadRecords : function(data, response, success) { - // calls: onCreateRecords | onUpdateRecords | onDestroyRecords - this['on' + Ext.util.Format.capitalize(action) + 'Records'](success, rs, [].concat(data)); - // If success === false here, exception will have been called in DataProxy - if (success === true) { - this.fireEvent('write', this, action, data, response, rs); - } - this.removeFromBatch(batch, action, data); - }; - }, + /** + * Revert to a view of the Record cache with no filtering applied. + * @param {Boolean} suppressEvent If true the filter is cleared silently without firing the + * {@link #datachanged} event. + */ + clearFilter: function(suppressEvent) { + var me = this; + + me.filters.clear(); - // Clears records from modified array after an exception event. - // NOTE: records are left marked dirty. Do we want to commit them even though they were not updated/realized? - // TODO remove this method? - clearModified : function(rs) { - if (Ext.isArray(rs)) { - for (var n=rs.length-1;n>=0;n--) { - this.modified.splice(this.modified.indexOf(rs[n]), 1); + if (me.remoteFilter) { + me.load(); + } else if (me.isFiltered()) { + me.data = me.snapshot.clone(); + delete me.snapshot; + + if (suppressEvent !== true) { + me.fireEvent('datachanged', me); } - } else { - this.modified.splice(this.modified.indexOf(rs), 1); } }, - // remap record ids in MixedCollection after records have been realized. @see Store#onCreateRecords, @see DataReader#realize - reMap : function(record) { - if (Ext.isArray(record)) { - for (var i = 0, len = record.length; i < len; i++) { - this.reMap(record[i]); - } - } else { - delete this.data.map[record._phid]; - this.data.map[record.id] = record; - var index = this.data.keys.indexOf(record._phid); - this.data.keys.splice(index, 1, record.id); - delete record._phid; - } + /** + * Returns true if this store is currently filtered + * @return {Boolean} + */ + isFiltered: function() { + var snapshot = this.snapshot; + return !! snapshot && snapshot !== this.data; }, - // @protected onCreateRecord proxy callback for create action - onCreateRecords : function(success, rs, data) { - if (success === true) { - try { - this.reader.realize(rs, data); - this.reMap(rs); - } - catch (e) { - this.handleException(e); - if (Ext.isArray(rs)) { - // Recurse to run back into the try {}. DataReader#realize splices-off the rs until empty. - this.onCreateRecords(success, rs, data); - } - } - } + /** + * 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 (this reference) in which the function is executed. Defaults to this Store. + */ + filterBy: function(fn, scope) { + var me = this; + + me.snapshot = me.snapshot || me.data.clone(); + me.data = me.queryBy(fn, scope || me); + me.fireEvent('datachanged', me); }, - // @protected, onUpdateRecords proxy callback for update action - onUpdateRecords : function(success, rs, data) { - if (success === true) { - try { - this.reader.update(rs, data); - } catch (e) { - this.handleException(e); - if (Ext.isArray(rs)) { - // Recurse to run back into the try {}. DataReader#update splices-off the rs until empty. - this.onUpdateRecords(success, rs, data); - } - } - } + /** + * 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 (this reference) in which the function is executed. Defaults to this Store. + * @return {MixedCollection} Returns an Ext.util.MixedCollection of the matched records + **/ + queryBy: function(fn, scope) { + var me = this, + data = me.snapshot || me.data; + return data.filterBy(fn, scope || me); }, - // @protected onDestroyRecords proxy callback for destroy action - onDestroyRecords : function(success, rs, data) { - // splice each rec out of this.removed - rs = (rs instanceof Ext.data.Record) ? [rs] : [].concat(rs); - for (var i=0,len=rs.length;i=0;i--) { - this.insert(rs[i].lastIndex, rs[i]); // <-- lastIndex set in Store#destroyRecord + /** + * Loads an array of data straight into the Store + * @param {Array} data Array of data to load. Any non-model instances will be cast into model instances first + * @param {Boolean} append True to add the records to the existing records in the store, false to remove the old ones first + */ + loadData: function(data, append) { + var model = this.model, + length = data.length, + i, + record; + + //make sure each data element is an Ext.data.Model instance + for (i = 0; i < length; i++) { + record = data[i]; + + if (! (record instanceof Ext.data.Model)) { + data[i] = Ext.ModelManager.create(record, model); } } - }, - // protected handleException. Possibly temporary until Ext framework has an exception-handler. - handleException : function(e) { - // @see core/Error.js - Ext.handleError(e); + this.loadRecords(data, {addRecords: append}); }, /** - *

Reloads the Record cache from the configured Proxy using the configured - * {@link Ext.data.Reader Reader} and the options from the last load operation - * performed.

- *

Note: see the Important note in {@link #load}.

- * @param {Object} options

(optional) An Object containing - * {@link #load loading options} which may override the {@link #lastOptions options} - * used in the last {@link #load} operation. See {@link #load} for details - * (defaults to null, in which case the {@link #lastOptions} are - * used).

- *

To add new params to the existing params:


-lastOptions = myStore.lastOptions;
-Ext.apply(lastOptions.params, {
-    myNewParam: true
-});
-myStore.reload(lastOptions);
-     * 
+ * Loads an array of {@Ext.data.Model model} instances into the store, fires the datachanged event. This should only usually + * be called internally when loading from the {@link Ext.data.proxy.Proxy Proxy}, when adding records manually use {@link #add} instead + * @param {Array} records The array of records to load + * @param {Object} options {addRecords: true} to add these records to the existing records, false to remove the Store's existing records first */ - reload : function(options){ - this.load(Ext.applyIf(options||{}, this.lastOptions)); - }, + loadRecords: function(records, options) { + var me = this, + i = 0, + length = records.length; - // private - // Called as a callback by the Reader during a load operation. - loadRecords : function(o, options, success){ - var i, len; - - if (this.isDestroyed === true) { - return; + options = options || {}; + + + if (!options.addRecords) { + delete me.snapshot; + me.data.clear(); } - if(!o || success === false){ - if(success !== false){ - this.fireEvent('load', this, [], options); - } - if(options.callback){ - options.callback.call(options.scope || this, [], options, false, o); + + me.data.addAll(records); + + //FIXME: this is not a good solution. Ed Spencer is totally responsible for this and should be forced to fix it immediately. + for (; i < length; i++) { + if (options.start !== undefined) { + records[i].index = options.start + i; + } - return; + records[i].join(me); } - var r = o.records, t = o.totalRecords || r.length; - if(!options || options.add !== true){ - if(this.pruneModifiedRecords){ - this.modified = []; - } - for(i = 0, len = r.length; i < len; i++){ - r[i].join(this); - } - if(this.snapshot){ - this.data = this.snapshot; - delete this.snapshot; - } - this.clearData(); - this.data.addAll(r); - this.totalLength = t; - this.applySort(); - this.fireEvent('datachanged', this); - }else{ - var toAdd = [], - rec, - cnt = 0; - for(i = 0, len = r.length; i < len; ++i){ - rec = r[i]; - if(this.indexOfId(rec.id) > -1){ - this.doUpdate(rec); - }else{ - toAdd.push(rec); - ++cnt; - } - } - this.totalLength = Math.max(t, this.data.length + cnt); - this.add(toAdd); + + /* + * this rather inelegant suspension and resumption of events is required because both the filter and sort functions + * fire an additional datachanged event, which is not wanted. Ideally we would do this a different way. The first + * datachanged event is fired by the call to this.add, above. + */ + me.suspendEvents(); + + if (me.filterOnLoad && !me.remoteFilter) { + me.filter(); } - this.fireEvent('load', this, r, options); - if(options.callback){ - options.callback.call(options.scope || this, r, options, true); + + if (me.sortOnLoad && !me.remoteSort) { + me.sort(); } + + me.resumeEvents(); + me.fireEvent('datachanged', me, records); }, + // PAGING METHODS /** - * Loads data from a passed data block and fires the {@link #load} event. A {@link Ext.data.Reader 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 {@link Ext.data.Reader Reader} that is configured and should correspond to - * that {@link Ext.data.Reader Reader}'s {@link Ext.data.Reader#readRecords} parameter. - * @param {Boolean} append (Optional) true to append the new Records rather the default to replace - * the existing cache. - * Note: 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. Only Records with - * new, unique ids will be added. + * Loads a given 'page' of data by setting the start and limit values appropriately. Internally this just causes a normal + * load operation, passing in calculated 'start' and 'limit' params + * @param {Number} page The number of the page to load */ - loadData : function(o, append){ - var r = this.reader.readRecords(o); - this.loadRecords(r, {add: append}, true); + loadPage: function(page) { + var me = this; + + me.currentPage = page; + + me.read({ + page: page, + start: (page - 1) * me.pageSize, + limit: me.pageSize, + addRecords: !me.clearOnPageLoad + }); }, /** - * 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. Note: see the Important note in {@link #load}.

- * @return {Number} The number of Records in the Store's cache. + * Loads the next 'page' in the current data set */ - getCount : function(){ - return this.data.length || 0; + nextPage: function() { + this.loadPage(this.currentPage + 1); }, /** - * 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 {@link #reader Reader} - * must contain the dataset size. For remote data sources, the value for this property - * (totalProperty for {@link Ext.data.JsonReader JsonReader}, - * totalRecords for {@link Ext.data.XmlReader XmlReader}) shall be returned by a query on the server. - * Note: see the Important note in {@link #load}.

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

Note: this value is not updated when changing the contents of the Store locally.

+ * Loads the previous 'page' in the current data set */ - getTotalCount : function(){ - return this.totalLength || 0; + previousPage: function() { + this.loadPage(this.currentPage - 1); }, + // private + clearData: function() { + this.data.each(function(record) { + record.unjoin(); + }); + + this.data.clear(); + }, + + // Buffering /** - * Returns an object describing the current sort state of this Store. - * @return {Object} The sort state of the Store. An object with two properties: - * See {@link #sortInfo} for additional details. + * Prefetches data the Store using its configured {@link #proxy}. + * @param {Object} options Optional config object, passed into the Ext.data.Operation object before loading. + * See {@link #load} */ - getSortState : function(){ - return this.sortInfo; - }, + prefetch: function(options) { + var me = this, + operation, + requestId = me.getRequestId(); + options = options || {}; + + Ext.applyIf(options, { + action : 'read', + filters: me.filters.items, + sorters: me.sorters.items, + requestId: requestId + }); + me.pendingRequests.push(requestId); + + operation = Ext.create('Ext.data.Operation', options); + + // HACK to implement loadMask support. + //if (operation.blocking) { + // me.fireEvent('beforeload', me, operation); + //} + if (me.fireEvent('beforeprefetch', me, operation) !== false) { + me.loading = true; + me.proxy.read(operation, me.onProxyPrefetch, me); + } + + return me; + }, + + /** + * Prefetches a page of data. + * @param {Number} page The page to prefetch + * @param {Object} options Optional config object, passed into the Ext.data.Operation object before loading. + * See {@link #load} + * @param + */ + prefetchPage: function(page, options) { + var me = this, + pageSize = me.pageSize, + start = (page - 1) * me.pageSize, + end = start + pageSize; + + // Currently not requesting this page and range isn't already satisified + if (Ext.Array.indexOf(me.pagesRequested, page) === -1 && !me.rangeSatisfied(start, end)) { + options = options || {}; + me.pagesRequested.push(page); + Ext.applyIf(options, { + page : page, + start: start, + limit: pageSize, + callback: me.onWaitForGuarantee, + scope: me + }); + + me.prefetch(options); + } + + }, + /** + * Returns a unique requestId to track requests. * @private - * Invokes sortData if we have sortInfo to sort on and are not sorting remotely */ - applySort : function(){ - if ((this.sortInfo || this.multiSortInfo) && !this.remoteSort) { - this.sortData(); - } + getRequestId: function() { + this.requestSeed = this.requestSeed || 1; + return this.requestSeed++; }, - + /** + * Handles a success pre-fetch * @private - * Performs the actual sorting of data. This checks to see if we currently have a multi sort or not. It applies - * each sorter field/direction pair in turn by building an OR'ed master sorting function and running it against - * the full dataset + * @param {Ext.data.Operation} operation The operation that completed */ - sortData : function() { - var sortInfo = this.hasMultiSort ? this.multiSortInfo : this.sortInfo, - direction = sortInfo.direction || "ASC", - sorters = sortInfo.sorters, - sortFns = []; - - //if we just have a single sorter, pretend it's the first in an array - if (!this.hasMultiSort) { - sorters = [{direction: direction, field: sortInfo.field}]; + onProxyPrefetch: function(operation) { + var me = this, + resultSet = operation.getResultSet(), + records = operation.getRecords(), + + successful = operation.wasSuccessful(); + + if (resultSet) { + me.totalCount = resultSet.total; + me.fireEvent('totalcountchange', me.totalCount); } - - //create a sorter function for each sorter field/direction combo - for (var i=0, j = sorters.length; i < j; i++) { - sortFns.push(this.createSortFunction(sorters[i].field, sorters[i].direction)); + + if (successful) { + me.cacheRecords(records, operation); } + Ext.Array.remove(me.pendingRequests, operation.requestId); + if (operation.page) { + Ext.Array.remove(me.pagesRequested, operation.page); + } + + me.loading = false; + me.fireEvent('prefetch', me, records, successful, operation); - if (sortFns.length == 0) { - return; + // HACK to support loadMask + if (operation.blocking) { + me.fireEvent('load', me, records, successful); } - //the direction modifier is multiplied with the result of the sorting functions to provide overall sort direction - //(as opposed to direction per field) - var directionModifier = direction.toUpperCase() == "DESC" ? -1 : 1; - - //create a function which ORs each sorter together to enable multi-sort - var fn = function(r1, r2) { - var result = sortFns[0].call(this, r1, r2); - - //if we have more than one sorter, OR any additional sorter functions together - if (sortFns.length > 1) { - for (var i=1, j = sortFns.length; i < j; i++) { - result = result || sortFns[i].call(this, r1, r2); - } - } - - return directionModifier * result; - }; - - //sort the data - this.data.sort(direction, fn); - if (this.snapshot && this.snapshot != this.data) { - this.snapshot.sort(direction, fn); + //this is a callback that would have been passed to the 'read' function and is optional + Ext.callback(operation.callback, operation.scope || me, [records, operation, successful]); + }, + + /** + * Caches the records in the prefetch and stripes them with their server-side + * index. + * @private + * @param {Array} records The records to cache + * @param {Ext.data.Operation} The associated operation + */ + cacheRecords: function(records, operation) { + var me = this, + i = 0, + length = records.length, + start = operation ? operation.start : 0; + + if (!Ext.isDefined(me.totalCount)) { + me.totalCount = records.length; + me.fireEvent('totalcountchange', me.totalCount); + } + + for (; i < length; i++) { + // this is the true index, not the viewIndex + records[i].index = start + i; + } + + me.prefetchData.addAll(records); + if (me.purgePageCount) { + me.purgeRecords(); } + }, + + + /** + * Purge the least recently used records in the prefetch if the purgeCount + * has been exceeded. + */ + purgeRecords: function() { + var me = this, + prefetchCount = me.prefetchData.getCount(), + purgeCount = me.purgePageCount * me.pageSize, + numRecordsToPurge = prefetchCount - purgeCount - 1, + i = 0; + for (; i <= numRecordsToPurge; i++) { + me.prefetchData.removeAt(0); + } + }, + /** + * Determines if the range has already been satisfied in the prefetchData. * @private - * Creates and returns a function which sorts an array by the given field and direction - * @param {String} field The field to create the sorter for - * @param {String} direction The direction to sort by (defaults to "ASC") - * @return {Function} A function which sorts by the field/direction combination provided + * @param {Number} start The start index + * @param {Number} end The end index in the range + */ + rangeSatisfied: function(start, end) { + var me = this, + i = start, + satisfied = true; + + for (; i < end; i++) { + if (!me.prefetchData.getByKey(i)) { + satisfied = false; + // + if (end - i > me.pageSize) { + Ext.Error.raise("A single page prefetch could never satisfy this request."); + } + // + break; + } + } + return satisfied; + }, + + /** + * Determines the page from a record index + * @param {Number} index The record index + * @return {Number} The page the record belongs to */ - createSortFunction: function(field, direction) { - direction = direction || "ASC"; - var directionModifier = direction.toUpperCase() == "DESC" ? -1 : 1; - - var sortType = this.fields.get(field).sortType; - - //create a comparison function. Takes 2 records, returns 1 if record 1 is greater, - //-1 if record 2 is greater or 0 if they are equal - return function(r1, r2) { - var v1 = sortType(r1.data[field]), - v2 = sortType(r2.data[field]); - - return directionModifier * (v1 > v2 ? 1 : (v1 < v2 ? -1 : 0)); - }; + getPageFromRecordIndex: function(index) { + return Math.floor(index / this.pageSize) + 1; }, - + + /** + * Handles a guaranteed range being loaded + * @private + */ + onGuaranteedRange: function() { + var me = this, + totalCount = me.getTotalCount(), + start = me.requestStart, + end = ((totalCount - 1) < me.requestEnd) ? totalCount - 1 : me.requestEnd, + range = [], + record, + i = start; + + // + if (start > end) { + Ext.Error.raise("Start (" + start + ") was greater than end (" + end + ")"); + } + // + + if (start !== me.guaranteedStart && end !== me.guaranteedEnd) { + me.guaranteedStart = start; + me.guaranteedEnd = end; + + for (; i <= end; i++) { + record = me.prefetchData.getByKey(i); + // + if (!record) { + Ext.Error.raise("Record was not found and store said it was guaranteed"); + } + // + range.push(record); + } + me.fireEvent('guaranteedrange', range, start, end); + if (me.cb) { + me.cb.call(me.scope || me, range); + } + } + + me.unmask(); + }, + + // hack to support loadmask + mask: function() { + this.masked = true; + this.fireEvent('beforeload'); + }, + + // hack to support loadmask + unmask: function() { + if (this.masked) { + this.fireEvent('load'); + } + }, + /** - * Sets the default sort column and order to be used by the next {@link #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. See also {@link #remoteSort} and {@link #paramNames}. - * This function accepts two call signatures - pass in a field name as the first argument to sort on a single - * field, or pass in an array of sort configuration objects to sort by multiple fields. - * Single sort example: - * store.sort('name', 'ASC'); - * Multi sort example: - * store.sort([ - * { - * field : 'name', - * direction: 'ASC' - * }, - * { - * field : 'salary', - * direction: 'DESC' - * } - * ], 'ASC'); - * In this second form, the sort configs are applied in order, with later sorters sorting within earlier sorters' results. - * For example, if two records with the same name are present they will also be sorted by salary if given the sort configs - * above. Any number of sort configs can be added. - * @param {String/Array} fieldName The name of the field to sort by, or an array of ordered sort configs - * @param {String} dir (optional) The sort order, 'ASC' or 'DESC' (case-sensitive, defaults to 'ASC') - */ - sort : function(fieldName, dir) { - if (Ext.isArray(arguments[0])) { - return this.multiSort.call(this, fieldName, dir); - } else { - return this.singleSort(fieldName, dir); + * Returns the number of pending requests out. + */ + hasPendingRequests: function() { + return this.pendingRequests.length; + }, + + + // wait until all requests finish, until guaranteeing the range. + onWaitForGuarantee: function() { + if (!this.hasPendingRequests()) { + this.onGuaranteedRange(); } }, - + /** - * Sorts the store contents by a single field and direction. This is called internally by {@link sort} and would - * not usually be called manually - * @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') - */ - singleSort: function(fieldName, dir) { - var field = this.fields.get(fieldName); - if (!field) { - return false; + * Guarantee a specific range, this will load the store with a range (that + * must be the pageSize or smaller) and take care of any loading that may + * be necessary. + */ + guaranteeRange: function(start, end, cb, scope) { + // + if (start && end) { + if (end - start > this.pageSize) { + Ext.Error.raise({ + start: start, + end: end, + pageSize: this.pageSize, + msg: "Requested a bigger range than the specified pageSize" + }); + } } - - var name = field.name, - sortInfo = this.sortInfo || null, - sortToggle = this.sortToggle ? this.sortToggle[name] : null; - - if (!dir) { - if (sortInfo && sortInfo.field == name) { // toggle sort dir - dir = (this.sortToggle[name] || 'ASC').toggle('ASC', 'DESC'); + // + + end = (end > this.totalCount) ? this.totalCount - 1 : end; + + var me = this, + i = start, + prefetchData = me.prefetchData, + range = [], + startLoaded = !!prefetchData.getByKey(start), + endLoaded = !!prefetchData.getByKey(end), + startPage = me.getPageFromRecordIndex(start), + endPage = me.getPageFromRecordIndex(end); + + me.cb = cb; + me.scope = scope; + + me.requestStart = start; + me.requestEnd = end; + // neither beginning or end are loaded + if (!startLoaded || !endLoaded) { + // same page, lets load it + if (startPage === endPage) { + me.mask(); + me.prefetchPage(startPage, { + //blocking: true, + callback: me.onWaitForGuarantee, + scope: me + }); + // need to load two pages } else { - dir = field.sortDir; + me.mask(); + me.prefetchPage(startPage, { + //blocking: true, + callback: me.onWaitForGuarantee, + scope: me + }); + me.prefetchPage(endPage, { + //blocking: true, + callback: me.onWaitForGuarantee, + scope: me + }); } + // Request was already satisfied via the prefetch + } else { + me.onGuaranteedRange(); } - - this.sortToggle[name] = dir; - this.sortInfo = {field: name, direction: dir}; - this.hasMultiSort = false; - - if (this.remoteSort) { - if (!this.load(this.lastOptions)) { - if (sortToggle) { - this.sortToggle[name] = sortToggle; - } - if (sortInfo) { - this.sortInfo = sortInfo; + }, + + // because prefetchData is stored by index + // this invalidates all of the prefetchedData + sort: function() { + var me = this, + prefetchData = me.prefetchData, + sorters, + start, + end, + range; + + if (me.buffered) { + if (me.remoteSort) { + prefetchData.clear(); + me.callParent(arguments); + } else { + sorters = me.getSorters(); + start = me.guaranteedStart; + end = me.guaranteedEnd; + range; + + if (sorters.length) { + prefetchData.sort(sorters); + range = prefetchData.getRange(); + prefetchData.clear(); + me.cacheRecords(range); + delete me.guaranteedStart; + delete me.guaranteedEnd; + me.guaranteeRange(start, end); } + me.callParent(arguments); } } else { - this.applySort(); - this.fireEvent('datachanged', this); + me.callParent(arguments); } - return true; }, - /** - * Sorts the contents of this store by multiple field/direction sorters. This is called internally by {@link sort} - * and would not usually be called manually. - * Multi sorting only currently applies to local datasets - multiple sort data is not currently sent to a proxy - * if remoteSort is used. - * @param {Array} sorters Array of sorter objects (field and direction) - * @param {String} direction Overall direction to sort the ordered results by (defaults to "ASC") - */ - multiSort: function(sorters, direction) { - this.hasMultiSort = true; - direction = direction || "ASC"; - - //toggle sort direction - if (this.multiSortInfo && direction == this.multiSortInfo.direction) { - direction = direction.toggle("ASC", "DESC"); - } - - /** - * Object containing overall sort direction and an ordered array of sorter configs used when sorting on multiple fields - * @property multiSortInfo - * @type Object - */ - this.multiSortInfo = { - sorters : sorters, - direction: direction - }; - - if (this.remoteSort) { - this.singleSort(sorters[0].field, sorters[0].direction); - + // overriden to provide striping of the indexes as sorting occurs. + // this cannot be done inside of sort because datachanged has already + // fired and will trigger a repaint of the bound view. + doSort: function(sorterFn) { + var me = this; + if (me.remoteSort) { + //the load function will pick up the new sorters and request the sorted data from the proxy + me.load(); } else { - this.applySort(); - this.fireEvent('datachanged', this); + me.data.sortBy(sorterFn); + if (!me.buffered) { + var range = me.getRange(), + ln = range.length, + i = 0; + for (; i < ln; i++) { + range[i].index = i; + } + } + me.fireEvent('datachanged', me); } }, - - /** - * Calls the specified function for each of the {@link Ext.data.Record Records} in the cache. - * @param {Function} fn The function to call. The {@link Ext.data.Record Record} is passed as the first parameter. - * Returning false aborts and exits the iteration. - * @param {Object} scope (optional) The scope (this reference) in which the function is executed. - * Defaults to the current {@link Ext.data.Record Record} in the iteration. - */ - each : function(fn, scope){ - this.data.each(fn, scope); - }, - + /** - * Gets all {@link Ext.data.Record records} modified since the last commit. Modified records are - * persisted across load operations (e.g., during paging). Note: deleted records are not - * included. See also {@link #pruneModifiedRecords} and - * {@link Ext.data.Record}{@link Ext.data.Record#markDirty markDirty}.. - * @return {Ext.data.Record[]} An array of {@link Ext.data.Record Records} containing outstanding - * modifications. To obtain modified fields within a modified record see - *{@link Ext.data.Record}{@link Ext.data.Record#modified modified}.. + * Finds the index of the first matching Record in this store by a specific field value. + * @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 (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 + * @param {Boolean} exactMatch True to force exact match (^ and $ characters added to the regex). Defaults to false. + * @return {Number} The matched index or -1 */ - getModifiedRecords : function(){ - return this.modified; + find: function(property, value, start, anyMatch, caseSensitive, exactMatch) { + var fn = this.createFilterFn(property, value, anyMatch, caseSensitive, exactMatch); + return fn ? this.data.findIndexBy(fn, null, start) : -1; }, /** - * Sums the value of property for each {@link Ext.data.Record record} between start - * and end and returns the result. - * @param {String} property A field in each record - * @param {Number} start (optional) The record index to start at (defaults to 0) - * @param {Number} end (optional) The last record index to include (defaults to length - 1) - * @return {Number} The sum + * Finds the first matching Record in this store by a specific field value. + * @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 (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 + * @param {Boolean} exactMatch True to force exact match (^ and $ characters added to the regex). Defaults to false. + * @return {Ext.data.Model} The matched record or null */ - 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; + findRecord: function() { + var me = this, + index = me.find.apply(me, arguments); + return index !== -1 ? me.getAt(index) : null; }, /** @@ -1620,10 +1760,11 @@ myStore.reload(lastOptions); * @param {String/RegExp} value The string/regex to compare the property value to * @param {Boolean} anyMatch True if we don't care if the filter value is not the full value (defaults to false) * @param {Boolean} caseSensitive True to create a case-sensitive regex (defaults to false) - * @param {Boolean} exactMatch True to force exact match (^ and $ characters added to the regex). Defaults to false. Ignored if anyMatch is true. + * @param {Boolean} exactMatch True to force exact match (^ and $ characters added to the regex). Defaults to false. + * Ignored if anyMatch is true. */ - createFilterFn : function(property, value, anyMatch, caseSensitive, exactMatch){ - if(Ext.isEmpty(value, false)){ + createFilterFn: function(property, value, anyMatch, caseSensitive, exactMatch) { + if (Ext.isEmpty(value)) { return false; } value = this.data.createValueMatcher(value, anyMatch, caseSensitive, exactMatch); @@ -1633,345 +1774,381 @@ myStore.reload(lastOptions); }, /** - * @private - * Given an array of filter functions (each with optional scope), constructs and returns a single function that returns - * the result of all of the filters ANDed together - * @param {Array} filters The array of filter objects (each object should contain an 'fn' and optional scope) - * @return {Function} The multiple filter function + * Finds the index of the first matching Record in this store by a specific field value. + * @param {String} fieldName The name of the Record field to test. + * @param {Mixed} value The value to match the field against. + * @param {Number} startIndex (optional) The index to start searching at + * @return {Number} The matched index or -1 */ - createMultipleFilterFn: function(filters) { - return function(record) { - var isMatch = true; - - for (var i=0, j = filters.length; i < j; i++) { - var filter = filters[i], - fn = filter.fn, - scope = filter.scope; - - isMatch = isMatch && fn.call(scope, record); - } - - return isMatch; - }; + findExact: function(property, value, start) { + return this.data.findIndexBy(function(rec) { + return rec.get(property) === value; + }, + this, start); }, /** - * Filter the {@link Ext.data.Record records} by a specified property. Alternatively, pass an array of filter - * options to filter by more than one property. - * Single filter example: - * store.filter('name', 'Ed', true, true); //finds all records containing the substring 'Ed' - * Multiple filter example: - *

-     * store.filter([
-     *   {
-     *     property     : 'name',
-     *     value        : 'Ed',
-     *     anyMatch     : true, //optional, defaults to true
-     *     caseSensitive: true  //optional, defaults to true
-     *   },
-     *
-     *   //filter functions can also be passed
-     *   {
-     *     fn   : function(record) {
-     *       return record.get('age') == 24
-     *     },
-     *     scope: this
-     *   }
-     * ]);
-     * 
- * @param {String|Array} field A field on your records, or an array containing multiple filter options - * @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 - * @param {Boolean} exactMatch True to force exact match (^ and $ characters added to the regex). Defaults to false. Ignored if anyMatch is true. - */ - filter : function(property, value, anyMatch, caseSensitive, exactMatch){ - var fn; - //we can accept an array of filter objects, or a single filter object - normalize them here - if (Ext.isObject(property)) { - property = [property]; - } - - if (Ext.isArray(property)) { - var filters = []; - - //normalize the filters passed into an array of filter functions - for (var i=0, j = property.length; i < j; i++) { - var filter = property[i], - func = filter.fn, - scope = filter.scope || this; - - //if we weren't given a filter function, construct one now - if (!Ext.isFunction(func)) { - func = this.createFilterFn(filter.property, filter.value, filter.anyMatch, filter.caseSensitive, filter.exactMatch); - } - - filters.push({fn: func, scope: scope}); - } + * 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:
    + *
  • record : Ext.data.Model

    The {@link Ext.data.Model record} + * to test for filtering. Access field values using {@link Ext.data.Model#get}.

  • + *
  • id : Object

    The ID of the Record passed.

  • + *
+ * @param {Object} scope (optional) The scope (this reference) in which the function is executed. Defaults to this Store. + * @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); + }, - fn = this.createMultipleFilterFn(filters); - } else { - //classic single property filter - fn = this.createFilterFn(property, value, anyMatch, caseSensitive, exactMatch); - } + /** + * 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 me = this, + data = (bypassFilter === true && me.snapshot) ? me.snapshot: me.data; - return fn ? this.filterBy(fn) : this.clearFilter(); + return data.collect(dataIndex, 'data', allowNull); }, /** - * 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:
    - *
  • record : Ext.data.Record

    The {@link Ext.data.Record record} - * to test for filtering. Access field values using {@link Ext.data.Record#get}.

  • - *
  • id : Object

    The ID of the Record passed.

  • - *
- * @param {Object} scope (optional) The scope (this reference) in which the function is executed. Defaults to this Store. + * 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. Note: see the Important note in {@link #load}.

+ * @return {Number} The number of Records in the Store's cache. */ - filterBy : function(fn, scope){ - this.snapshot = this.snapshot || this.data; - this.data = this.queryBy(fn, scope || this); - this.fireEvent('datachanged', this); + getCount: function() { + return this.data.length || 0; }, /** - * Revert to a view of the Record cache with no filtering applied. - * @param {Boolean} suppressEvent If true the filter is cleared silently without firing the - * {@link #datachanged} event. + * Returns the total number of {@link Ext.data.Model Model} instances that the {@link Ext.data.proxy.Proxy Proxy} + * indicates exist. This will usually differ from {@link #getCount} when using paging - getCount returns the + * number of records loaded into the Store at the moment, getTotalCount returns the number of records that + * could be loaded into the Store if the Store contained all data + * @return {Number} The total number of Model instances available via the Proxy */ - clearFilter : function(suppressEvent){ - if(this.isFiltered()){ - this.data = this.snapshot; - delete this.snapshot; - if(suppressEvent !== true){ - this.fireEvent('datachanged', this); - } - } + getTotalCount: function() { + return this.totalCount; }, /** - * Returns true if this store is currently filtered - * @return {Boolean} + * Get the Record at the specified index. + * @param {Number} index The index of the Record to find. + * @return {Ext.data.Model} The Record at the passed index. Returns undefined if not found. */ - isFiltered : function(){ - return !!this.snapshot && this.snapshot != this.data; + getAt: function(index) { + return this.data.getAt(index); }, /** - * 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 + * 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.Model[]} An array of Records */ - query : function(property, value, anyMatch, caseSensitive){ - var fn = this.createFilterFn(property, value, anyMatch, caseSensitive); - return fn ? this.queryBy(fn) : this.data.clone(); + getRange: function(start, end) { + return this.data.getRange(start, end); }, /** - * 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:
    - *
  • record : Ext.data.Record

    The {@link Ext.data.Record record} - * to test for filtering. Access field values using {@link Ext.data.Record#get}.

  • - *
  • id : Object

    The ID of the Record passed.

  • - *
- * @param {Object} scope (optional) The scope (this reference) in which the function is executed. Defaults to this Store. - * @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); + * Get the Record with the specified id. + * @param {String} id The id of the Record to find. + * @return {Ext.data.Model} The Record with the passed id. Returns undefined if not found. + */ + getById: function(id) { + return (this.snapshot || this.data).findBy(function(record) { + return record.getId() === id; + }); }, /** - * Finds the index of the first matching Record in this store by a specific field value. - * @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 (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 + * Get the index within the cache of the passed Record. + * @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. */ - find : function(property, value, start, anyMatch, caseSensitive){ - var fn = this.createFilterFn(property, value, anyMatch, caseSensitive); - return fn ? this.data.findIndexBy(fn, null, start) : -1; + indexOf: function(record) { + return this.data.indexOf(record); }, + /** - * Finds the index of the first matching Record in this store by a specific field value. - * @param {String} fieldName The name of the Record field to test. - * @param {Mixed} value The value to match the field against. - * @param {Number} startIndex (optional) The index to start searching at - * @return {Number} The matched index or -1 + * Get the index within the entire dataset. From 0 to the totalCount. + * @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. */ - findExact: function(property, value, start){ - return this.data.findIndexBy(function(rec){ - return rec.get(property) === value; - }, this, start); + indexOfTotal: function(record) { + return record.index || this.indexOf(record); }, /** - * 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:
    - *
  • record : Ext.data.Record

    The {@link Ext.data.Record record} - * to test for filtering. Access field values using {@link Ext.data.Record#get}.

  • - *
  • id : Object

    The ID of the Record passed.

  • - *
- * @param {Object} scope (optional) The scope (this reference) in which the function is executed. Defaults to this Store. - * @param {Number} startIndex (optional) The index to start searching at - * @return {Number} The matched index or -1 + * 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. */ - findBy : function(fn, scope, start){ - return this.data.findIndexBy(fn, scope, start); + indexOfId: function(id) { + return this.data.indexOfKey(id); }, + + /** + * Remove all items from the store. + * @param {Boolean} silent Prevent the `clear` event from being fired. + */ + removeAll: function(silent) { + var me = this; + + me.clearData(); + if (me.snapshot) { + me.snapshot.clear(); + } + if (silent !== true) { + me.fireEvent('clear', me); + } + }, + + /* + * Aggregation methods + */ /** - * 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; - } + * Convenience function for getting the first model instance in the store + * @param {Boolean} grouped (Optional) 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) { + var me = this; + + if (grouped && me.isGrouped()) { + return me.aggregate(function(records) { + return records.length ? records[0] : undefined; + }, me, true); + } else { + return me.data.first(); } - return r; }, - // private - afterEdit : function(record){ - if(this.modified.indexOf(record) == -1){ - this.modified.push(record); + /** + * Convenience function for getting the last model instance in the store + * @param {Boolean} grouped (Optional) 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) { + var me = this; + + if (grouped && me.isGrouped()) { + return me.aggregate(function(records) { + var len = records.length; + return len ? records[len - 1] : undefined; + }, me, true); + } else { + return me.data.last(); } - 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); + /** + * Sums the value of property for each {@link Ext.data.Model record} between start + * and end and returns the result. + * @param {String} field A field in each record + * @param {Boolean} grouped (Optional) 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 me = this; + + if (grouped && me.isGrouped()) { + return me.aggregate(me.getSum, me, true, [field]); + } else { + return me.getSum(me.data.items, field); + } }, - // private - afterCommit : function(record){ - this.modified.remove(record); - this.fireEvent('update', this, record, Ext.data.Record.COMMIT); + // @private, see sum + getSum: function(records, field) { + var total = 0, + i = 0, + len = records.length; + + for (; i < len; ++i) { + total += records[i].get(field); + } + + return total; }, /** - * Commit all Records with {@link #getModifiedRecords outstanding changes}. To handle updates for changes, - * subscribe to the Store's {@link #update update event}, and perform updating when the third parameter is - * Ext.data.Record.COMMIT. + * Gets the count of items in the store. + * @param {Boolean} grouped (Optional) 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 */ - commitChanges : function(){ - var modified = this.modified.slice(0), - length = modified.length, - i; - - for (i = 0; i < length; i++){ - modified[i].commit(); + count: function(grouped) { + var me = this; + + if (grouped && me.isGrouped()) { + return me.aggregate(function(records) { + return records.length; + }, me, true); + } else { + return me.getCount(); } - - this.modified = []; - this.removed = []; }, /** - * {@link Ext.data.Record#reject Reject} outstanding changes on all {@link #getModifiedRecords modified records}. + * Gets the minimum value in the store. + * @param {String} field The field in each record + * @param {Boolean} grouped (Optional) 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 {Mixed/undefined} The minimum value, if no items exist, undefined. */ - rejectChanges : function() { - var modified = this.modified.slice(0), - removed = this.removed.slice(0).reverse(), - mLength = modified.length, - rLength = removed.length, - i; - - for (i = 0; i < mLength; i++) { - modified[i].reject(); - } - - for (i = 0; i < rLength; i++) { - this.insert(removed[i].lastIndex || 0, removed[i]); - removed[i].reject(); + min: function(field, grouped) { + var me = this; + + if (grouped && me.isGrouped()) { + return me.aggregate(me.getMin, me, true, [field]); + } else { + return me.getMin(me.data.items, field); } - - this.modified = []; - this.removed = []; }, - // private - onMetaChange : function(meta){ - this.recordType = this.reader.recordType; - this.fields = this.recordType.prototype.fields; - delete this.snapshot; - if(this.reader.meta.sortInfo){ - this.sortInfo = this.reader.meta.sortInfo; - }else if(this.sortInfo && !this.fields.get(this.sortInfo.field)){ - delete this.sortInfo; + // @private, see min + getMin: function(records, field){ + var i = 1, + len = records.length, + value, min; + + if (len > 0) { + min = records[0].get(field); } - if(this.writer){ - this.writer.meta = this.reader.meta; + + for (; i < len; ++i) { + value = records[i].get(field); + if (value < min) { + min = value; + } } - this.modified = []; - this.fireEvent('metachange', this, this.reader.meta); + return min; }, - // 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; + /** + * Gets the maximum value in the store. + * @param {String} field The field in each record + * @param {Boolean} grouped (Optional) 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 {Mixed/undefined} The maximum value, if no items exist, undefined. + */ + max: function(field, grouped) { + var me = this; + + if (grouped && me.isGrouped()) { + return me.aggregate(me.getMax, me, true, [field]); + } else { + return me.getMax(me.data.items, field); + } + }, + + // @private, see max + getMax: function(records, field) { + var i = 1, + len = records.length, + value, + max; + + if (len > 0) { + max = records[0].get(field); + } + + for (; i < len; ++i) { + value = records[i].get(field); + if (value > max) { + max = value; + } + } + return max; }, /** - * Set the value for a property name in this store's {@link #baseParams}. Usage:


-myStore.setBaseParam('foo', {bar:3});
-
- * @param {String} name Name of the property to assign - * @param {Mixed} value Value to assign the named property - **/ - setBaseParam : function (name, value){ - this.baseParams = this.baseParams || {}; - this.baseParams[name] = value; - } -}); + * Gets the average value in the store. + * @param {String} field The field in each record + * @param {Boolean} grouped (Optional) 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 {Mixed/undefined} The average value, if no items exist, 0. + */ + average: function(field, grouped) { + var me = this; + if (grouped && me.isGrouped()) { + return me.aggregate(me.getAverage, me, true, [field]); + } else { + return me.getAverage(me.data.items, field); + } + }, -Ext.reg('store', Ext.data.Store); + // @private, see average + getAverage: function(records, field) { + var i = 0, + len = records.length, + sum = 0; -/** - * @class Ext.data.Store.Error - * @extends Ext.Error - * Store Error extension. - * @param {String} name - */ -Ext.data.Store.Error = Ext.extend(Ext.Error, { - name: 'Ext.data.Store' -}); -Ext.apply(Ext.data.Store.Error.prototype, { - lang: { - 'writer-undefined' : 'Attempted to execute a write-action without a DataWriter installed.' + if (records.length > 0) { + for (; i < len; ++i) { + sum += records[i].get(field); + } + return sum / len; + } + return 0; + }, + + /** + * Runs the aggregate function for all the records in the store. + * @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 (optional) The scope to execute the function in. Defaults to the store. + * @param {Boolean} grouped (Optional) 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 {Array} args (optional) Any arguments to append to the function call + * @return {Object} An object literal with the group names and their appropriate values. + */ + aggregate: function(fn, scope, grouped, args) { + args = args || []; + if (grouped && this.isGrouped()) { + var groups = this.getGroups(), + i = 0, + len = groups.length, + out = {}, + group; + + for (; i < len; ++i) { + group = groups[i]; + out[group.name] = fn.apply(scope || this, [group.children].concat(args)); + } + return out; + } else { + return fn.apply(scope || this, [this.data.items].concat(args)); + } } });