/** * 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 #method-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 for loading and saving * its data: * * // Set up a 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'} * ] * }); * * var myStore = Ext.create('Ext.data.Store', { * model: 'User', * proxy: { * type: 'ajax', * url: '/users.json', * reader: { * type: 'json', * rootProperty: '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 #cfg-data} into Model instances: * * Ext.create('Ext.data.Store', { * model: 'User', * data : [ * {firstName: 'Peter', lastName: 'Venkman'}, * {firstName: 'Egon', lastName: 'Spengler'}, * {firstName: 'Ray', lastName: 'Stantz'}, * {firstName: 'Winston', lastName: 'Zeddemore'} * ] * }); * * 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 #method-add}. * * ## Dynamic Loading * * Stores can be dynamically updated by calling the {@link #method-load} method: * * store.load({ * params: { * group: 3, * type: 'user' * }, * callback: function(records, operation, success) { * // do something after the load finishes * }, * scope: this * }); * * Here a bunch of arbitrary parameters is passed along with the load request and a callback * function is set up to do something after the loading is over. * * ## 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 = Ext.create('Ext.data.Store', { * autoLoad: true, * model: "User", * proxy: { * type: 'ajax', * url: 'users.json', * reader: { * type: 'json', * rootProperty: 'users' * } * } * }); * * Which would consume a response like this: * * { * "users": [{ * "id": 1, * "name": "Peter", * "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 #cfg-sorters} and {@link #cfg-filters} are held inside * {@link Ext.util.Collection Collection} 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 #method-sort} or {@link #filter}: * * var store = Ext.create('Ext.data.Store', { * model: 'User', * sorters: [{ * property: 'age', * direction: 'DESC' * }, { * property: 'firstName', * direction: 'ASC' * }], * * filters: [{ * property: 'firstName', * value: /Peter/ * }] * }); * * The new Store will keep the configured sorters and filters in the Collection 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). * * store.filter('eyeColor', 'Brown'); * * Change the sorting at any time by calling {@link #method-sort}: * * store.sort('height', 'ASC'); * * Note that all existing sorters will be removed in favor of the new sorter data (if * {@link #method-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 * Collection: * * 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 registered 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 * Ext.create('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: * * - {@link Ext.data.proxy.Proxy Proxy} - overview of what Proxies are and how they are used * - {@link Ext.data.Model Model} - the core class in the data package * - {@link Ext.data.reader.Reader Reader} - used by any subclass of * {@link Ext.data.proxy.Server ServerProxy} to read a response */Ext.define('Ext.data.Store', { extend: 'Ext.data.ProxyStore', alias: 'store.store', mixins: [ 'Ext.data.LocalStore' ], // Required classes must be loaded before the definition callback runs // The class definition callback creates a dummy Store which requires that // all the classes below have been loaded. requires: [ 'Ext.data.Model', 'Ext.data.proxy.Ajax', 'Ext.data.reader.Json', 'Ext.data.writer.Json', // This ensures that we have Ext.util.Collection and all of its requirements. 'Ext.util.GroupCollection', 'Ext.util.DelayedTask' ], uses: [ 'Ext.data.StoreManager', 'Ext.util.Grouper' ], config: { /** * @cfg {Object[]/Ext.data.Model[]} data * Array of Model instances or data objects to load locally. See "Inline data" * above for details. */ data: undefined, // undefined so the applier is always called /** * @cfg {Boolean} [clearRemovedOnLoad=true] * `true` to clear anything in the {@link #removed} record collection when the store loads. */ clearRemovedOnLoad: true, /** * @cfg {Boolean} [clearOnPageLoad=true] * True to empty the store when loading another page via {@link #loadPage}, * {@link #nextPage} or {@link #previousPage}. Setting to false keeps existing records, * allowing large data sets to be loaded one page at a time but rendered all together. */ clearOnPageLoad: true, /** * @cfg {Ext.data.Model} [associatedEntity] * The owner of this store if the store is used as part of an association. * * @private */ associatedEntity: null, /** * @cfg {Ext.data.schema.Role} [role] * The role for the {@link #associatedEntity}. * * @private */ role: null, /** * @cfg {Ext.data.Session} session * The session for this store. By specifying a session, it ensures any records that are * added to this store are also included in the session. This store does not become a member * of the session itself. * * @since 5.0.0 */ session: null }, /** * @property {Ext.util.Collection} data * The `data` property is a `Collection` which holds this store's local cache of records. * @private * @readonly */ /** * @private * Used as a parameter to loadRecords */ addRecordsOptions: { addRecords: true }, /** * @property {Number} loadCount * The number of times records have been loaded into the store. This includes loads via * {@link #loadData} & {@link #loadRecords}. * @readonly */ loadCount: 0, /** * `true` once the store has loaded data from the server. * @property {Boolean} complete * * @private */ complete: false, moveMapCount: 0, /** * Creates the store. * @param {Object} [config] Config object. */ constructor: function(config) { var me = this, data; if (config) { if (config.buffered) { //<debug> if (this.self !== Ext.data.Store) { Ext.raise('buffered config not supported on derived Store classes. ' + 'Please derive from Ext.data.BufferedStore.'); } //</debug> // Hide this from Cmd /* eslint-disable-next-line dot-notation */ return new Ext.data['BufferedStore'](config); } //<debug> if (config.remoteGroup) { Ext.log.warn('Ext.data.Store: remoteGroup has been removed. ' + 'Use remoteSort instead.'); } //</debug> } /** * @event beforeprefetch * Fires before a prefetch occurs. Return `false` to cancel. * @param {Ext.data.Store} this * @param {Ext.data.operation.Operation} operation The associated operation. */ /** * @event groupchange * Fired whenever the grouping in the grid changes. * @param {Ext.data.Store} store The store. * @param {Ext.util.Grouper} grouper The grouper object. */ /** * @event groupschange * Fired whenever the multi grouping in the grid changes. * @param {Ext.data.Store} store The store. * @param {Ext.util.GrouperCollection} groupers The groupers collection. */ /** * @event prefetch * Fires whenever records have been prefetched. * @param {Ext.data.Store} this * @param {Ext.data.Model[]} records An array of records. * @param {Boolean} successful `true` if the operation was successful. * @param {Ext.data.operation.Operation} operation The associated operation. */ /** * @event filterchange * Fired whenever the filter set changes. * @param {Ext.data.Store} store The store. * @param {Ext.util.Filter[]} filters The array of Filter objects. */ me.callParent([config]); // See applyData for the details. data = me.inlineData; if (data) { delete me.inlineData; me.loadInlineData(data); } }, /** * @method getData * Returns the store's records. * * **Note:** If your store has been filtered, getData() will return a filtered * collection. Use `getData().{@link Ext.util.Collection#getSource getSource()}` to * fetch all unfiltered records. * * @return {Ext.util.Collection} An Ext.util.Collection of records * (an empty Collection if no records are held by the store). */ /** * @method setData * Loads an array of data directly into the Store. * * setData() is ideal if your data's format is already in its appropriate format (e.g. it * doesn't need to be processed by a reader). If your data's structure requires processing, * use a {@link Ext.data.proxy.Memory MemoryProxy} or {@link #loadRawData}. * * Use {@link #loadData}, {@link #method-add}, or {@link #insert} if records need to be * appended to the current recordset. * * @param {Ext.data.Model[]/Object[]} data Array of data to load. Any non-model instances * will be cast into model instances first. */ applyData: function(data, dataCollection) { // We bring up the Collection for records which forms the bottom of the config // dependency graph. The appliers for "filters" and "sorters" depend on "data" // and "remoteFilter" and "remoteSort" depend on both in their updaters. var me = this; // Ensure that we process our Model config first. me.getFields(); me.getModel(); // We might be configured with a Collection instance if (data && data.isCollection) { data.setRootProperty('data'); dataCollection = data; dataCollection.addObserver(this); // Perform a load postprocess if the incoming collection is loaded. if (data.getCount()) { me.afterLoadRecords(data.items); // This is not fired by afterLoadRecords because loadRecords // which calls afterLoadRecords is a public API which simply adds some // records. This situation here though, is anaologous to a load. if (me.hasListeners.load) { me.fireEvent('load', me, data.items, true); } } } else { if (!dataCollection) { dataCollection = me.constructDataCollection(); } if (data) { if (me.isInitializing) { // When data is configured on the instance of a Store we must wait for // all the things to initialize (sorters, filters, groupers) so that we // can properly process that data. All of those appliers, however, depend // on the dataCollection (us) to get booted up first so we must defer // this back to after initConfig. In previous versions this was hacked // at by the constructor via "config.data" but "data" can also be set on // the Ext.define level so best to pick it up here and store aside to be // finished in the constructor. me.inlineData = data; } else { // If we are not constructing the Store than a setData call needs to be // equivalent to the legacy loadData method with respect to events that fire, // etc. me.loadData(data); } } } return dataCollection; }, loadInlineData: function(data) { var me = this, proxy = me.getProxy(); if (proxy && proxy.isMemoryProxy) { proxy.setData(data); // Allow a memory proxy to trigger a load initially me.suspendEvents(); me.read(); me.resumeEvents(); } else { // We make it silent because we don't want to fire a refresh event me.removeAll(true); // We don't want to fire addrecords event since we will be firing // a refresh event later which will already take care of updating // any views bound to this store me.suspendEvents(); me.loadData(data); me.resumeEvents(); } }, /** * @method insert * @inheritdoc Ext.data.LocalStore#insert */ onCollectionAdd: function(collection, info) { this.loadCount = this.loadCount || 1; this.onCollectionAddItems(collection, info.items, info); }, onCollectionFilterAdd: function(collection, items) { this.onCollectionAddItems(collection, items); }, onCollectionAddItems: function(collection, records, info) { var me = this, len = records.length, lastChunk = info ? !info.next : false, // Must use class-specific removed property. // Regular Stores add to the "removed" property on remove. // TreeStores are having records removed all the time; node collapse removes. // TreeStores add to the "removedNodes" property onNodeRemove removed = me.removed, ignoreAdd = me.ignoreCollectionAdd, session = me.getSession(), replaced = info && info.replaced, i, sync, record, replacedItems; // Collection add changes the items reference of the collection, and that array // object if directly referenced by Ranges. The ranges have to refresh themselves // upon add. if (me.activeRanges) { me.syncActiveRanges(); } for (i = 0; i < len; ++i) { record = records[i]; if (session) { session.adopt(record); } // If ignoring, we don't want to do anything other than pull // the added records into the session if (!ignoreAdd) { record.join(me); if (removed && removed.length) { Ext.Array.remove(removed, record); } sync = sync || record.phantom || record.dirty; } } if (ignoreAdd) { return; } if (replaced) { replacedItems = []; do { Ext.Array.push(replacedItems, replaced.items); replaced = replaced.next; } while (replaced); me.setMoving(replacedItems, true); } if (info) { // If this is a replacement operation, there will have been a // previous call to onCollectionRemove which will have fired no // events in anticipation of a final refresh event. // Here is where we inform interested parties of all the changes. if (info.replaced) { if (lastChunk) { me.fireEvent('datachanged', me); me.fireEvent('refresh', me); } } else { me.fireEvent('add', me, records, info.at); // If there is a next property, that means there is another range that needs // to be removed after this. Wait until everything is gone before firing // datachanged since it should be a bulk operation if (lastChunk) { me.fireEvent('datachanged', me); } } } if (replacedItems) { me.setMoving(replacedItems, false); } // Addition means a sync is needed. me.needsSync = me.needsSync || sync; }, onCollectionBeforeItemChange: function(collection, info) { var record = info.item, modifiedFieldNames = info.modified || null, type = info.meta; // This is currently intended to be private this.fireEvent('beforeupdate', this, record, type, modifiedFieldNames, info); }, // If our source collection informs us that a filtered out item has changed, we must still // fire the events... onCollectionFilteredItemChange: function() { this.onCollectionItemChange.apply(this, arguments); }, onCollectionItemChange: function(collection, info) { var me = this, record = info.item, modifiedFieldNames = info.modified || null, type = info.meta; if (me.fireChangeEvent(record)) { // Inform any interested parties that a record has been mutated. // This will be invoked on TreeStores in which the invoking record // is an descendant of a collapsed node, and so *will not be contained by this store me.onUpdate(record, type, modifiedFieldNames, info); me.fireEvent('update', me, record, type, modifiedFieldNames, info); me.fireEvent('datachanged', me); } }, afterChange: function(record, modifiedFieldNames, type) { this.getData().itemChanged(record, modifiedFieldNames || null, undefined, type); }, afterCommit: function(record, modifiedFieldNames) { this.afterChange(record, modifiedFieldNames, Ext.data.Model.COMMIT); }, afterEdit: function(record, modifiedFieldNames) { this.needsSync = this.needsSync || record.dirty; this.afterChange(record, modifiedFieldNames, Ext.data.Model.EDIT); }, afterReject: function(record) { this.afterChange(record, null, Ext.data.Model.REJECT); }, afterDrop: function(record) { this.getData().remove(record); }, afterErase: function(record) { this.removeFromRemoved(record); }, /** * @method add * @inheritdoc Ext.data.LocalStore#add */ /** * (Local sort only) Inserts the passed Record into the Store at the index where it * should go based on the current sort information. * * @param {Ext.data.Record} record */ addSorted: function(record) { var me = this, remote = me.getRemoteSort(), data = me.getData(), index; if (remote) { data.setSorters(me.getSorters()); } index = data.findInsertionIndex(record); if (remote) { data.setSorters(null); } return me.insert(index, record); }, /** * Removes the specified record(s) from the Store, firing the {@link #event-remove} * event for the removed records. * * After all records have been removed a single `datachanged` is fired. * * @param {Ext.data.Model/Ext.data.Model[]/Number/Number[]} records Model instance or * array of instances to remove or an array of indices from which to remove records. * @param isMove (private) * @param silent (private) */ remove: function(records, isMove, silent) { var me = this, data = me.getDataSource(), len, i, toRemove, record; if (records) { if (records.isModel) { if (data.indexOf(records) > -1) { toRemove = [records]; len = 1; } else { len = 0; } } else { toRemove = []; for (i = 0, len = records.length; i < len; ++i) { record = records[i]; if (record && record.isEntity) { if (!data.contains(record)) { continue; } } else if (!(record = data.getAt(record))) { // an index continue; } toRemove.push(record); } len = toRemove.length; } } if (!len) { return []; } me.removeIsMove = isMove === true; me.removeIsSilent = silent; data.remove(toRemove); me.removeIsSilent = false; return toRemove; }, onCollectionRemove: function(collection, info) { var me = this, // Must use class-specific removed property. // Regular Stores add to the "removed" property on remove. // TreeStores are having records removed all the time; node collapse removes. // TreeStores add to the "removedNodes" property onNodeRemove removed = me.removed, records = info.items, len = records.length, index = info.at, replacement = info.replacement, /* eslint-disable-next-line max-len */ isMove = me.removeIsMove || (replacement && Ext.Array.equals(records, replacement.items)), silent = me.removeIsSilent, lastChunk = !info.next, data = me.getDataSource(), i, record; if (me.ignoreCollectionRemove) { return; } if (replacement) { me.setMoving(replacement.items, true); } for (i = len - 1; i >= 0; i--) { record = records[i]; // If the data contains the record, that means the record is filtered out, so // it's not being removed, nor should it be unjoined if (!data.contains(record)) { // Don't push interally moving, or phantom (client side only), // erasing (informing server through its own proxy) records if (removed && !isMove && !record.phantom && !record.erasing) { // Store the index the record was removed from so that rejectChanges can // re-insert at the correct place. // The record's index property won't do, as that is the index in the overall // dataset when Store is buffered. record.removedFrom = index + i; removed.push(record); // Removal of a non-phantom record which is NOT erasing (informing the server // through its own proxy) requires that the store be synced at some point. me.needsSync = true; } else { // Only unjoin if we're not being pushed into the removed collection. We still // have an interest in that record otherwise. record.unjoin(me); } } } if (!silent) { // If this removal is just the first part of a replacement operation, // do not fire the events now. // // onCollectionAddItems will fire a refresh event, and convert multiple // remove and add operations to an atomic refresh event. // This will provide a better UI update. // Also, focus can only be preserved around one operation, so // editing a field which is the sorted field could result in // incorrect focus.. if (!replacement || !replacement.items.length) { me.fireEvent('remove', me, records, index, isMove); // If there is a next property, that means there is another range that needs // to be removed after this. Wait until everything is gone before firing datachanged // since it should be a bulk operation if (lastChunk) { me.fireEvent('datachanged', me); } } } if (replacement) { me.setMoving(replacement.items, false); } }, onFilterEndUpdate: function() { var me = this; if (me.destroying || me.destroyed) { return; } // Filtering changes the items reference of the collection, and that array // object if directly referenced by Ranges. The ranges have to refresh themselves // upon add. if (me.activeRanges) { me.syncActiveRanges(); } me.callParent(arguments); me.callObservers('Filter'); }, /** * Removes the model instance(s) at the given index * @param {Number} index The record index * @param {Number} [count=1] The number of records to delete */ removeAt: function(index, count) { var data = this.getData(); // Sanity check input. index = Math.max(index, 0); if (index < data.length) { if (arguments.length === 1) { count = 1; } else if (!count) { return; } data.removeAt(index, count); } }, /** * Removes all unfiltered items from the store. Filtered records will not be removed. * Individual record `{@link #event-remove}` events are not fired by this method. * * @param {Boolean} [silent=false] Pass `true` to prevent the `{@link #event-clear}` event * from being fired. * @return {Ext.data.Model[]} The removed records. */ removeAll: function(silent) { var me = this, data = me.getData(), records = data.getRange(); // We want to remove and mute any events here if (data.length) { // Explicit true here, we never want to fire remove events me.removeIsSilent = true; me.callObservers('BeforeRemoveAll'); data.removeAll(); me.removeIsSilent = false; if (!silent) { me.fireEvent('clear', me, records); me.fireEvent('datachanged', me); } me.callObservers('AfterRemoveAll', [!!silent]); } return records; }, /** * Make a set of records be current in the store. This means that unneeded records * will be removed and new records will be added. * @param {Ext.data.Model[]} records The records to be current in the store. * * @private */ setRecords: function(records) { var count = this.getCount(); ++this.loadCount; if (count) { this.getData().splice(0, count, records); } else { this.add(records); } }, /** * This method is basically the same as the JavaScript Array splice method. * * Negative indexes are interpreted starting at the end of the collection. That is, * a value of -1 indicates the last item, or equivalent to `length - 1`. * * @param {Number} index The index at which to add or remove items. * @param {Number/Object[]} toRemove The number of items to remove or an array of the * items to remove. * @param {Object[]} [toAdd] The items to insert at the given `index`. * @private */ splice: function(index, toRemove, toAdd) { return this.getData().splice(index, toRemove, toAdd); }, /** * @protected * Called internally when a Proxy has completed a load request */ onProxyLoad: function(operation) { var me = this, resultSet = operation.getResultSet(), records = operation.getRecords(), successful = operation.wasSuccessful(); if (me.destroyed) { return; } if (resultSet) { me.totalCount = resultSet.getTotal(); } if (successful) { records = me.processAssociation(records); me.loadRecords(records, operation.getAddRecords() ? { addRecords: true } : undefined); me.attachSummaryRecord(resultSet); } else { me.loading = false; } if (me.hasListeners.load) { me.fireEvent('load', me, records, successful, operation); } me.callObservers('AfterLoad', [records, successful, operation]); }, onProxyWrite: function(operation) { if (operation.wasSuccessful()) { this.attachSummaryRecord(operation.getResultSet()); } this.callParent([operation]); }, // private filterDataSource: function(fn) { var source = this.getDataSource(), items = source.items, len = items.length, ret = [], i; for (i = 0; i < len; i++) { if (fn.call(source, items[i])) { ret.push(items[i]); } } return ret; }, getNewRecords: function() { return this.filterDataSource(this.filterNew); }, getRejectRecords: function() { return this.filterDataSource(this.filterRejects); }, getUpdatedRecords: function() { return this.filterDataSource(this.filterUpdated); }, /** * Loads an array of data straight into the Store. * * Using this method is great if the data is in the correct format already (e.g. it doesn't * need to be processed by a reader). If your data requires processing to decode the data * structure, use a {@link Ext.data.proxy.Memory MemoryProxy} or {@link #loadRawData}. * * @param {Ext.data.Model[]/Object[]} data Array of data to load. Any non-model instances will * be cast into model instances first. * @param {Boolean} [append=false] `true` to add the records to the existing records in the * store, `false` to remove the old ones first. */ loadData: function(data, append) { var me = this, length = data.length, newData = [], i; // make sure each data element is an Ext.data.Model instance for (i = 0; i < length; i++) { newData.push(me.createModel(data[i])); } newData = me.processAssociation(newData); me.loadRecords(newData, append ? me.addRecordsOptions : undefined); }, /** * Loads data via the bound Proxy's reader * * Use this method if you are attempting to load data and want to utilize the configured data * reader. * * As of 4.2, this method will no longer fire the {@link #event-load} event. * * @param {Object[]} data The full JSON object you'd like to load into the Data store. * @param {Boolean} [append=false] `true` to add the records to the existing records in the * store, `false` to remove the old ones first. * * @return {Boolean} `true` if the reader processed the records correctly. See * {@link Ext.data.reader.Reader#successProperty}. If the reader did not process the records, * nothing will be added. */ loadRawData: function(data, append) { var me = this, session = me.getSession(), result, records, success; /* eslint-disable-next-line max-len */ result = me.getProxy().getReader().read(data, session ? { recordCreator: session.recordCreator } : undefined); records = result.getRecords(); success = result.getSuccess(); if (success) { me.totalCount = result.getTotal(); me.loadRecords(records, append ? me.addRecordsOptions : undefined); } return success; }, /** * Loads an array of {@link 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 #method-add} * instead * @param {Ext.data.Model[]} records The array of records to load * @param {Object} options * @param {Boolean} [options.addRecords=false] Pass `true` to add these records to the existing * records, `false` to remove the Store's existing records first. */ loadRecords: function(records, options) { var me = this, data = me.getData(), addRecords, skipSort; if (options) { addRecords = options.addRecords; } if (!me.getRemoteSort() && !me.getSortOnLoad()) { skipSort = true; data.setAutoSort(false); } if (!addRecords) { me.clearData(true); } // Clear the flag AFTER the stores collection has been cleared down so that // observers of that collection know that it was due to a load, and a refresh is imminent. me.loading = false; me.ignoreCollectionAdd = true; me.callObservers('BeforePopulate'); data.add(records); me.ignoreCollectionAdd = false; if (skipSort) { data.setAutoSort(true); } me.afterLoadRecords(records); }, afterLoadRecords: function(records) { var me = this, length = records.length, i; for (i = 0; i < length; i++) { records[i].join(me); } if (!me.isEmptyStore) { ++me.loadCount; me.complete = true; } if (me.hasListeners.datachanged) { me.fireEvent('datachanged', me); } if (me.hasListeners.refresh) { me.fireEvent('refresh', me); } me.callObservers('AfterPopulate'); }, // PAGING METHODS /** * 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. * @param {Object} [options] See options for {@link #method-load}. */ loadPage: function(page, options) { var me = this, size = me.getPageSize(); me.currentPage = page; // Copy options into a new object so as not to mutate passed in objects options = Ext.apply({ page: page, start: (page - 1) * size, limit: size, addRecords: !me.getClearOnPageLoad() }, options); me.read(options); }, /** * Loads the next 'page' in the current data set * @param {Object} options See options for {@link #method-load} */ nextPage: function(options) { this.loadPage(this.currentPage + 1, options); }, /** * Loads the previous 'page' in the current data set * @param {Object} options See options for {@link #method-load} */ previousPage: function(options) { this.loadPage(this.currentPage - 1, options); }, /** * @private */ clearData: function(isLoad) { var me = this, removed = me.removed, data = me.getDataSource(), clearRemovedOnLoad = me.getClearRemovedOnLoad(), needsUnjoinCheck = removed && isLoad && !clearRemovedOnLoad, records, record, i, len; // We only have to do the unjoining if not buffered. PageMap will unjoin its records when // it clears itself. // There is a potential for a race condition in stores configured with autoDestroy: true; // if loading was initiated but didn't complete by the time the store is destroyed, // the data MC may not have been created yet so we have to check for its existence // here and below. if (data) { records = data.items; for (i = 0, len = records.length; i < len; ++i) { record = records[i]; if (needsUnjoinCheck && Ext.Array.contains(removed, record)) { continue; } record.unjoin(me); } me.ignoreCollectionRemove = true; me.callObservers('BeforeClear'); data.removeAll(); me.ignoreCollectionRemove = false; me.callObservers('AfterClear'); } if (removed && (!isLoad || clearRemovedOnLoad)) { removed.length = 0; } }, onIdChanged: function(rec, oldId, newId) { this.getData().updateKey(rec, oldId); // This event is used internally this.fireEvent('idchanged', this, rec, oldId, newId); }, /** * Commits all Records with {@link #getModifiedRecords outstanding changes}. To handle updates * for changes, subscribe to the Store's {@link #event-update update event}, and perform * updating when the third parameter is Ext.data.Record.COMMIT. */ commitChanges: function() { var me = this, recs = me.getModifiedRecords(), len = recs.length, i = 0; Ext.suspendLayouts(); me.beginUpdate(); for (; i < len; i++) { recs[i].commit(); } me.cleanRemoved(); me.endUpdate(); Ext.resumeLayouts(true); /** * @private * @event commit * Fired when all changes were committed and the Store is clean. * * **Note** Used internally. * * @param {Ext.data.Store} store The Store object */ me.fireEvent('commit', me); }, filterNewOnly: function(item) { return item.phantom === true; }, filterRejects: function(item) { return item.phantom || item.dirty; }, /** * {@link Ext.data.Model#reject Rejects} outstanding changes on all {@link #getModifiedRecords * modified records} and re-insert any records that were removed locally. Any phantom records * will be removed. */ rejectChanges: function() { var me = this, recs = me.getRejectRecords(), len = recs.length, i, rec, toRemove, sorted, data, currentAutoSort; Ext.suspendLayouts(); me.beginUpdate(); for (i = 0; i < len; i++) { rec = recs[i]; if (rec.phantom) { toRemove = toRemove || []; toRemove.push(rec); } else { rec.reject(); } } if (toRemove) { me.remove(toRemove); for (i = 0, len = toRemove.length; i < len; ++i) { toRemove[i].reject(); } } // Restore removed records back to their original positions. recs = me.getRawRemovedRecords(); if (recs) { len = recs.length; sorted = !me.getRemoteSort() && me.isSorted(); if (sorted) { // Temporarily turn off sorting so .reject() doesn't attempt to sort the record. // It would throw b/c the record isn't yet in its collection. data = me.getData(); currentAutoSort = data.getAutoSort(); data.setAutoSort(false); } for (i = len - 1; i >= 0; i--) { rec = recs[i]; rec.reject(); if (!sorted) { me.insert(rec.removedFrom || 0, rec); } } if (sorted) { // Turn sorting back on so the collection is auto-sorted when added. data.setAutoSort(currentAutoSort); me.add(recs); } // Don't need to call cleanRemoved because we've re-added everything, don't // need to unjoin the store recs.length = 0; } me.endUpdate(); Ext.resumeLayouts(true); /** * @private * @event reject * Fired when all changes were rejected and the Store is clean. * * **Note** Used internally. * * @param {Ext.data.Store} store The Store object */ me.fireEvent('reject', me); }, doDestroy: function() { var me = this, task = me.loadTask, data = me.getData(), source = data.getSource(); // clearData ensures everything is unjoined me.clearData(); me.setSession(null); me.observers = null; if (task) { task.cancel(); me.loadTask = null; } if (source) { source.destroy(); } me.callParent(); }, /** * Change summary functions on multiple fields on the store model * * @param {Object} values Object where keys are field names and values are summary types */ setFieldsSummaries: function(values) { var me = this, model = me.model.getSummaryModel(), key; if (!model || me.isDestroyed) { return; } for (key in values) { model.setSummaryField(key, values[key]); } if (me.getRemoteSummary()) { me.reload(); } else { me.getSummaryRecord().calculateSummary(me.getData().items); me.recalculateSummaries(me.getGroups()); me.fireEvent('summarieschanged', me); me.fireEvent('datachanged', me); } }, /** * Change summary function on the specified field of the store model * * @param {String} field * @param {String/Ext.data.summary.Base} summary */ setFieldSummary: function(field, summary) { var me = this, model = me.model.getSummaryModel(); if (!model || me.isDestroyed) { return; } model.setSummaryField(field, summary); if (me.getRemoteSummary()) { me.reload(); } else { me.getSummaryRecord().calculateSummary(me.getData().items); me.recalculateSummaries(me.getGroups()); me.fireEvent('summarieschanged', me); me.fireEvent('datachanged', me); } }, recalculateSummaries: function(groups) { var groupCount = groups ? groups.length : 0, i, group; for (i = 0; i < groupCount; i++) { group = groups.items[i]; group.recalculateSummaries(); this.recalculateSummaries(group.getGroups()); } }, privates: { commitOptions: { commit: true }, attachSummaryRecord: function(resultSet) { if (!resultSet) { return; } /* eslint-disable-next-line vars-on-top */ var me = this, summary = resultSet.getSummaryData(), groupers = me.getGroupers(), current = me.summaryRecord, commitOptions = me.commitOptions, changed = false, groups, len, i, rec, group, children, child; if (summary) { changed = true; if (current) { // we need to clear the data object otherwise fields // that are populated on the client side but are not // calculated on the server may not be removed. current.data = {}; current.set(summary.data, commitOptions); } else { me.summaryRecord = summary; summary.isRemote = true; } } if (groupers && groupers.length) { changed = true; summary = resultSet.getGroupData(); if (summary) { groups = me.getGroups(); for (i = 0, len = summary.length; i < len; ++i) { rec = summary[i]; // The summaries are in fact records. For multiple groupers the records // should only fill in the dataIndex of all groupers above it. group = groups.getItemGroup(rec); if (group) { children = group.getGroups(); while (children) { child = children.getItemGroup(rec); if (child) { // if the remote summary record is undefined on the grouper // property then this child group is not the right one if (Ext.isDefined(rec.data[child.getGrouper().getProperty()])) { group = child; children = group.getGroups(); } else { children = null; } } else { children = null; } } delete(rec.data.id); current = group.getGroupRecord(); current.set(rec.data, commitOptions); current.isRemote = true; current = group.getSummaryRecord(); current.set(rec.data, commitOptions); current.isRemote = true; } } } } if (changed) { if (me.hasListeners.remotesummarieschanged) { me.fireEvent('remotesummarieschanged', me); } } }, /** * Similar to a load, however no records are added to the store. This is useful * in allowing the developer to decide what to do with the new records. * @param {Object} [options] See {@link #method-load load options}. * * @private */ fetch: function(options) { var operation; options = Ext.apply({}, options); this.setLoadOptions(options); operation = this.createOperation('read', options); operation.execute(); }, fireChangeEvent: function(record) { return this.getDataSource().contains(record); }, onBeforeLoad: function(operation) { this.callObservers('BeforeLoad', [operation]); }, onRemoteFilterSet: function(filters, remoteFilter) { if (filters) { this.getData().setFilters(remoteFilter ? null : filters); } this.callParent([filters, remoteFilter]); }, onRemoteSortSet: function(sorters, remoteSort) { var data = this.getData(); if (sorters) { data.setSorters(remoteSort ? null : sorters); } data.setAutoGroup(!remoteSort); this.callParent([sorters, remoteSort]); }, /** * Checks whether records are being moved within the store. This can be used in conjunction * with the {@link #event-add} and {@link #event-remove} events to determine whether * the records are being removed/added or just having the position changed. * @param {Ext.data.Model[]/Ext.data.Model} [records] The record(s). * @param {Object} [getMap] (private) * @return {Number} The number of records being moved. `0` if no records are moving. * If records are passed the number will refer to how many of the passed records are moving. * * @private */ isMoving: function(records, getMap) { var map = this.moveMap, moving = 0, len, i; if (map) { if (records) { if (Ext.isArray(records)) { for (i = 0, len = records.length; i < len; ++i) { moving += map[records[i].id] ? 1 : 0; } } else if (map[records.id]) { ++moving; } } else { moving = getMap ? map : this.moveMapCount; } } return moving; }, setLoadOptions: function(options) { // Only add grouping options if grouping is remote var me = this, pageSize = me.getPageSize(), summaries = [], groupers = me.getGroupers(false), session, grouper, model, fields, field, len, i; if (me.getRemoteSort() || me.getRemoteSummary()) { if (!options.grouper) { grouper = me.getGrouper(); if (grouper) { options.grouper = grouper; } } else if (!options.groupers) { if (groupers && groupers.length) { options.groupers = groupers.getRange(); } } } if (me.getRemoteSummary()) { // extract summary fields from the model model = me.getModel().getSummaryModel(); fields = model.getFields(); len = fields.length; for (i = 0; i < len; i++) { // fields that are sent as groupers should not be sent as summaries // otherwise when the response comes back we can't link groups to results field = fields[i]; if (groupers && !groupers.get(field.name)) { summaries.push(field); } } options.summaries = summaries; } if (pageSize || 'start' in options || 'limit' in options || 'page' in options) { options.page = options.page != null ? options.page : me.currentPage; options.start = (options.start !== undefined) ? options.start : (options.page - 1) * pageSize; options.limit = options.limit != null ? options.limit : pageSize; me.currentPage = options.page; } options.addRecords = options.addRecords || false; if (!options.recordCreator) { session = me.getSession(); if (session) { options.recordCreator = session.recordCreator; } } me.callParent([options]); }, setMoving: function(records, isMoving) { var me = this, map = me.moveMap || (me.moveMap = {}), len = records.length, i, id; for (i = 0; i < len; ++i) { id = records[i].id; if (isMoving) { if (map[id]) { ++map[id]; } else { map[id] = 1; ++me.moveMapCount; } } else { if (--map[id] === 0) { delete map[id]; --me.moveMapCount; } } } if (me.moveMapCount === 0) { me.moveMap = null; } }, processAssociation: function(records) { var me = this, associatedEntity = me.getAssociatedEntity(); if (associatedEntity) { records = me.getRole().processLoad(me, associatedEntity, records, me.getSession()); } return records; } } // Provides docs from the mixin /** * @method each * @inheritdoc Ext.data.LocalStore#each */ /** * @method collect * @inheritdoc Ext.data.LocalStore#collect */ /** * @method getById * @inheritdoc Ext.data.LocalStore#getById */ /** * @method getByInternalId * @inheritdoc Ext.data.LocalStore#getByInternalId */ /** * @method indexOf * @inheritdoc Ext.data.LocalStore#indexOf */ /** * @method indexOfId * @inheritdoc Ext.data.LocalStore#indexOfId */ /** * @method queryBy * @inheritdoc Ext.data.LocalStore#queryBy */ /** * @method query * @inheritdoc Ext.data.LocalStore#query */ /** * @method first * @inheritdoc Ext.data.LocalStore#first */ /** * @method last * @inheritdoc Ext.data.LocalStore#last */ /** * @method sum * @inheritdoc Ext.data.LocalStore#sum */ /** * @method count * @inheritdoc Ext.data.LocalStore#count */ /** * @method min * @inheritdoc Ext.data.LocalStore#min */ /** * @method max * @inheritdoc Ext.data.LocalStore#max */ /** * @method average * @inheritdoc Ext.data.LocalStore#average */ /** * @method aggregate * @inheritdoc Ext.data.LocalStore#aggregate */});