]> git.proxmox.com Git - sencha-touch.git/blob - src/src/data/Store.js
import Sencha Touch 2.4.2 source
[sencha-touch.git] / src / src / data / Store.js
1 /**
2 * @author Ed Spencer
3 *
4 * The Store class encapsulates a client side cache of {@link Ext.data.Model Model} objects. Stores load
5 * data via a {@link Ext.data.proxy.Proxy Proxy}, and also provide functions for {@link #sort sorting},
6 * {@link #filter filtering} and querying the {@link Ext.data.Model model} instances contained within it.
7 *
8 * Creating a Store is easy - we just tell it the Model and the Proxy to use to load and save its data:
9 *
10 * // Set up a {@link Ext.data.Model model} to use in our Store
11 * Ext.define("User", {
12 * extend: "Ext.data.Model",
13 * config: {
14 * fields: [
15 * {name: "firstName", type: "string"},
16 * {name: "lastName", type: "string"},
17 * {name: "age", type: "int"},
18 * {name: "eyeColor", type: "string"}
19 * ]
20 * }
21 * });
22 *
23 * var myStore = Ext.create("Ext.data.Store", {
24 * model: "User",
25 * proxy: {
26 * type: "ajax",
27 * url : "/users.json",
28 * reader: {
29 * type: "json",
30 * rootProperty: "users"
31 * }
32 * },
33 * autoLoad: true
34 * });
35 *
36 * Ext.create("Ext.List", {
37 * fullscreen: true,
38 * store: myStore,
39 * itemTpl: "{lastName}, {firstName} ({age})"
40 * });
41 *
42 * In the example above we configured an AJAX proxy to load data from the url '/users.json'. We told our Proxy
43 * to use a {@link Ext.data.reader.Json JsonReader} to parse the response from the server into Model object -
44 * {@link Ext.data.reader.Json see the docs on JsonReader} for details.
45 *
46 * The external data file, _/users.json_, is as follows:
47 *
48 * {
49 * "success": true,
50 * "users": [
51 * {
52 * "firstName": "Tommy",
53 * "lastName": "Maintz",
54 * "age": 24,
55 * "eyeColor": "green"
56 * },
57 * {
58 * "firstName": "Aaron",
59 * "lastName": "Conran",
60 * "age": 26,
61 * "eyeColor": "blue"
62 * },
63 * {
64 * "firstName": "Jamie",
65 * "lastName": "Avins",
66 * "age": 37,
67 * "eyeColor": "brown"
68 * }
69 * ]
70 * }
71 *
72 * ## Inline data
73 *
74 * Stores can also load data inline. Internally, Store converts each of the objects we pass in as {@link #cfg-data}
75 * into Model instances:
76 *
77 * @example
78 * // Set up a model to use in our Store
79 * Ext.define('User', {
80 * extend: 'Ext.data.Model',
81 * config: {
82 * fields: [
83 * {name: 'firstName', type: 'string'},
84 * {name: 'lastName', type: 'string'},
85 * {name: 'age', type: 'int'},
86 * {name: 'eyeColor', type: 'string'}
87 * ]
88 * }
89 * });
90 *
91 * Ext.create("Ext.data.Store", {
92 * storeId: "usersStore",
93 * model: "User",
94 * data : [
95 * {firstName: "Ed", lastName: "Spencer"},
96 * {firstName: "Tommy", lastName: "Maintz"},
97 * {firstName: "Aaron", lastName: "Conran"},
98 * {firstName: "Jamie", lastName: "Avins"}
99 * ]
100 * });
101 *
102 * Ext.create("Ext.List", {
103 * fullscreen: true,
104 * store: "usersStore",
105 * itemTpl: "{lastName}, {firstName}"
106 * });
107 *
108 * Loading inline data using the method above is great if the data is in the correct format already (e.g. it doesn't need
109 * to be processed by a {@link Ext.data.reader.Reader reader}). If your inline data requires processing to decode the data structure,
110 * use a {@link Ext.data.proxy.Memory MemoryProxy} instead (see the {@link Ext.data.proxy.Memory MemoryProxy} docs for an example).
111 *
112 * Additional data can also be loaded locally using {@link #method-add}.
113 *
114 * ## Loading Nested Data
115 *
116 * Applications often need to load sets of associated data - for example a CRM system might load a User and her Orders.
117 * 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
118 * and allow the Reader to automatically populate the associated models. Below is a brief example, see the {@link Ext.data.reader.Reader} intro
119 * documentation for a full explanation:
120 *
121 * // Set up a model to use in our Store
122 * Ext.define('User', {
123 * extend: 'Ext.data.Model',
124 * config: {
125 * fields: [
126 * {name: 'name', type: 'string'},
127 * {name: 'id', type: 'int'}
128 * ]
129 * }
130 * });
131 *
132 * var store = Ext.create('Ext.data.Store', {
133 * autoLoad: true,
134 * model: "User",
135 * proxy: {
136 * type: 'ajax',
137 * url : 'users.json',
138 * reader: {
139 * type: 'json',
140 * rootProperty: 'users'
141 * }
142 * }
143 * });
144 *
145 * Ext.create("Ext.List", {
146 * fullscreen: true,
147 * store: store,
148 * itemTpl: "{name} (id: {id})"
149 * });
150 *
151 * Which would consume a response like this:
152 *
153 * {
154 * "users": [
155 * {
156 * "id": 1,
157 * "name": "Ed",
158 * "orders": [
159 * {
160 * "id": 10,
161 * "total": 10.76,
162 * "status": "invoiced"
163 * },
164 * {
165 * "id": 11,
166 * "total": 13.45,
167 * "status": "shipped"
168 * }
169 * ]
170 * },
171 * {
172 * "id": 3,
173 * "name": "Tommy",
174 * "orders": [
175 * ]
176 * },
177 * {
178 * "id": 4,
179 * "name": "Jamie",
180 * "orders": [
181 * {
182 * "id": 12,
183 * "total": 17.76,
184 * "status": "shipped"
185 * }
186 * ]
187 * }
188 * ]
189 * }
190 *
191 * See the {@link Ext.data.reader.Reader} intro docs for a full explanation.
192 *
193 * ## Filtering and Sorting
194 *
195 * Stores can be sorted and filtered - in both cases either remotely or locally. The {@link #sorters} and {@link #filters} are
196 * held inside {@link Ext.util.MixedCollection MixedCollection} instances to make them easy to manage. Usually it is sufficient to
197 * either just specify sorters and filters in the Store configuration or call {@link #sort} or {@link #filter}:
198 *
199 * // Set up a model to use in our Store
200 * Ext.define('User', {
201 * extend: 'Ext.data.Model',
202 * config: {
203 * fields: [
204 * {name: 'firstName', type: 'string'},
205 * {name: 'lastName', type: 'string'},
206 * {name: 'age', type: 'int'}
207 * ]
208 * }
209 * });
210 *
211 * var store = Ext.create("Ext.data.Store", {
212 * autoLoad: true,
213 * model: "User",
214 * proxy: {
215 * type: "ajax",
216 * url : "users.json",
217 * reader: {
218 * type: "json",
219 * rootProperty: "users"
220 * }
221 * },
222 * sorters: [
223 * {
224 * property : "age",
225 * direction: "DESC"
226 * },
227 * {
228 * property : "firstName",
229 * direction: "ASC"
230 * }
231 * ],
232 * filters: [
233 * {
234 * property: "firstName",
235 * value: /Jamie/
236 * }
237 * ]
238 * });
239 *
240 * Ext.create("Ext.List", {
241 * fullscreen: true,
242 * store: store,
243 * itemTpl: "{lastName}, {firstName} ({age})"
244 * });
245 *
246 * And the data file, _users.json_, is as follows:
247 *
248 * {
249 * "success": true,
250 * "users": [
251 * {
252 * "firstName": "Tommy",
253 * "lastName": "Maintz",
254 * "age": 24
255 * },
256 * {
257 * "firstName": "Aaron",
258 * "lastName": "Conran",
259 * "age": 26
260 * },
261 * {
262 * "firstName": "Jamie",
263 * "lastName": "Avins",
264 * "age": 37
265 * }
266 * ]
267 * }
268 *
269 * The new Store will keep the configured sorters and filters in the MixedCollection instances mentioned above. By default, sorting
270 * and filtering are both performed locally by the Store - see {@link #remoteSort} and {@link #remoteFilter} to allow the server to
271 * perform these operations instead.
272 *
273 * Filtering and sorting after the Store has been instantiated is also easy. Calling {@link #filter} adds another filter to the Store
274 * and automatically filters the dataset (calling {@link #filter} with no arguments simply re-applies all existing filters). Note that by
275 * default your sorters are automatically reapplied if using local sorting.
276 *
277 * store.filter('eyeColor', 'Brown');
278 *
279 * Change the sorting at any time by calling {@link #sort}:
280 *
281 * store.sort('height', 'ASC');
282 *
283 * Note that all existing sorters will be removed in favor of the new sorter data (if {@link #sort} is called with no arguments,
284 * the existing sorters are just reapplied instead of being removed). To keep existing sorters and add new ones, just add them
285 * to the MixedCollection:
286 *
287 * store.sorters.add(new Ext.util.Sorter({
288 * property : 'shoeSize',
289 * direction: 'ASC'
290 * }));
291 *
292 * store.sort();
293 *
294 * ## Registering with StoreManager
295 *
296 * Any Store that is instantiated with a {@link #storeId} will automatically be registered with the {@link Ext.data.StoreManager StoreManager}.
297 * This makes it easy to reuse the same store in multiple views:
298 *
299 * // this store can be used several times
300 * Ext.create('Ext.data.Store', {
301 * model: 'User',
302 * storeId: 'usersStore'
303 * });
304 *
305 * Ext.create('Ext.List', {
306 * store: 'usersStore'
307 * // other config goes here
308 * });
309 *
310 * Ext.create('Ext.view.View', {
311 * store: 'usersStore'
312 * // other config goes here
313 * });
314 *
315 * ## Further Reading
316 *
317 * Stores are backed up by an ecosystem of classes that enables their operation. To gain a full understanding of these
318 * pieces and how they fit together, see:
319 *
320 * - {@link Ext.data.proxy.Proxy Proxy} - overview of what Proxies are and how they are used
321 * - {@link Ext.data.Model Model} - the core class in the data package
322 * - {@link Ext.data.reader.Reader Reader} - used by any subclass of {@link Ext.data.proxy.Server ServerProxy} to read a response
323 */
324 Ext.define('Ext.data.Store', {
325 alias: 'store.store',
326
327 extend: 'Ext.Evented',
328
329 requires: [
330 'Ext.util.Collection',
331 'Ext.data.Operation',
332 'Ext.data.proxy.Memory',
333 'Ext.data.Model',
334 'Ext.data.StoreManager',
335 'Ext.util.Grouper'
336 ],
337
338 /**
339 * @event addrecords
340 * Fired when one or more new Model instances have been added to this Store. You should listen
341 * for this event if you have to update a representation of the records in this store in your UI.
342 * If you need the indices of the records that were added please use the store.indexOf(record) method.
343 * @param {Ext.data.Store} store The store
344 * @param {Ext.data.Model[]} records The Model instances that were added
345 */
346
347 /**
348 * @event removerecords
349 * Fired when one or more Model instances have been removed from this Store. You should listen
350 * for this event if you have to update a representation of the records in this store in your UI.
351 * @param {Ext.data.Store} store The Store object
352 * @param {Ext.data.Model[]} records The Model instances that was removed
353 * @param {Number[]} indices The indices of the records that were removed. These indices already
354 * take into account any potential earlier records that you remove. This means that if you loop
355 * over the records, you can get its current index in your data representation from this array.
356 */
357
358 /**
359 * @event updaterecord
360 * Fires when a Model instance has been updated
361 * @param {Ext.data.Store} this
362 * @param {Ext.data.Model} record The Model instance that was updated
363 * @param {Number} newIndex If the update changed the index of the record (due to sorting for example), then
364 * this gives you the new index in the store.
365 * @param {Number} oldIndex If the update changed the index of the record (due to sorting for example), then
366 * this gives you the old index in the store.
367 * @param {Array} modifiedFieldNames An array containing the field names that have been modified since the
368 * record was committed or created
369 * @param {Object} modifiedValues An object where each key represents a field name that had it's value modified,
370 * and where the value represents the old value for that field. To get the new value in a listener
371 * you should use the {@link Ext.data.Model#get get} method.
372 */
373
374 /**
375 * @event update
376 * @inheritdoc Ext.data.Store#updaterecord
377 * @removed 2.0 Listen to #updaterecord instead.
378 */
379
380 /**
381 * @event refresh
382 * Fires whenever the records in the Store have changed in a way that your representation of the records
383 * need to be entirely refreshed.
384 * @param {Ext.data.Store} this The data store
385 * @param {Ext.util.Collection} data The data collection containing all the records
386 */
387
388 /**
389 * @event beforeload
390 * Fires before a request is made for a new data object. If the beforeload handler returns false the load
391 * action will be canceled. Note that you should not listen for this event in order to refresh the
392 * data view. Use the {@link #refresh} event for this instead.
393 * @param {Ext.data.Store} store This Store
394 * @param {Ext.data.Operation} operation The Ext.data.Operation object that will be passed to the Proxy to
395 * load the Store
396 */
397
398 /**
399 * @event load
400 * Fires whenever records have been loaded into the store. Note that you should not listen
401 * for this event in order to refresh the data view. Use the {@link #refresh} event for this instead.
402 * @param {Ext.data.Store} this
403 * @param {Ext.data.Model[]} records An array of records
404 * @param {Boolean} successful `true` if the operation was successful.
405 * @param {Ext.data.Operation} operation The associated operation.
406 */
407
408 /**
409 * @event write
410 * Fires whenever a successful write has been made via the configured {@link #proxy Proxy}
411 * @param {Ext.data.Store} store This Store
412 * @param {Ext.data.Operation} operation The {@link Ext.data.Operation Operation} object that was used in
413 * the write
414 */
415
416 /**
417 * @event beforesync
418 * Fired before a call to {@link #sync} is executed. Return `false` from any listener to cancel the sync
419 * @param {Object} options Hash of all records to be synchronized, broken down into create, update and destroy
420 */
421
422 /**
423 * @event clear
424 * Fired after the {@link #removeAll} method is called. Note that you should not listen for this event in order
425 * to refresh the data view. Use the {@link #refresh} event for this instead.
426 * @param {Ext.data.Store} this
427 * @return {Ext.data.Store}
428 */
429
430 statics: {
431 create: function(store) {
432 if (!store.isStore) {
433 if (!store.type) {
434 store.type = 'store';
435 }
436 store = Ext.createByAlias('store.' + store.type, store);
437 }
438 return store;
439 }
440 },
441
442 isStore: true,
443
444 config: {
445 /**
446 * @cfg {String} storeId
447 * Unique identifier for this store. If present, this Store will be registered with the {@link Ext.data.StoreManager},
448 * making it easy to reuse elsewhere.
449 * @accessor
450 */
451 storeId: undefined,
452
453 /**
454 * @cfg {Object[]/Ext.data.Model[]} data
455 * Array of Model instances or data objects to load locally. See "Inline data" above for details.
456 * @accessor
457 */
458 data: null,
459
460 /**
461 * @cfg {Boolean/Object} [autoLoad=false]
462 * If data is not specified, and if `autoLoad` is `true` or an Object, this store's load method is automatically called
463 * after creation. If the value of `autoLoad` is an Object, this Object will be passed to the store's `load()` method.
464 * @accessor
465 */
466 autoLoad: null,
467
468 /**
469 * @cfg {Boolean} autoSync
470 * `true` to automatically sync the Store with its Proxy after every edit to one of its Records.
471 * @accessor
472 */
473 autoSync: false,
474
475 /**
476 * @cfg {String} model
477 * Returns Ext.data.Model and not a String.
478 * Name of the {@link Ext.data.Model Model} associated with this store.
479 * The string is used as an argument for {@link Ext.ModelManager#getModel}.
480 * @accessor
481 */
482 model: undefined,
483
484 /**
485 * @cfg {String/Ext.data.proxy.Proxy/Object} proxy The Proxy to use for this Store. This can be either a string, a config
486 * object or a Proxy instance - see {@link #setProxy} for details.
487 * @accessor
488 */
489 proxy: undefined,
490
491 /**
492 * @cfg {Object[]/Ext.util.Collection} fields
493 * Returns Ext.util.Collection not just an Object.
494 * Use in place of specifying a {@link #model} configuration. The fields should be a
495 * set of {@link Ext.data.Field} configuration objects. The store will automatically create a {@link Ext.data.Model}
496 * with these fields. In general this configuration option should be avoided, it exists for the purposes of
497 * backwards compatibility. For anything more complicated, such as specifying a particular id property or
498 * associations, a {@link Ext.data.Model} should be defined and specified for the {@link #model}
499 * config.
500 * @return Ext.util.Collection
501 * @accessor
502 */
503 fields: null,
504
505 /**
506 * @cfg {Boolean} remoteSort
507 * `true` to defer any sorting operation to the server. If `false`, sorting is done locally on the client.
508 *
509 * If this is set to `true`, you will have to manually call the {@link #method-load} method after you {@link #method-sort}, to retrieve the sorted
510 * data from the server.
511 *
512 * {@link #buffered Buffered} stores automatically set this to `true`. Buffered stores contain an abitrary
513 * subset of the full dataset which depends upon various configurations and which pages have been requested
514 * for rendering. Such *sparse* datasets are ineligible for local sorting.
515 * @accessor
516 */
517 remoteSort: false,
518
519 /**
520 * @cfg {Boolean} remoteFilter
521 * `true` to defer any filtering operation to the server. If `false`, filtering is done locally on the client.
522 *
523 * If this is set to `true`, you will have to manually call the {@link #method-load} method after you {@link #method-filter} to retrieve the filtered
524 * data from the server.
525 *
526 * {@link #buffered Buffered} stores automatically set this to `true`. Buffered stores contain an abitrary
527 * subset of the full dataset which depends upon various configurations and which pages have been requested
528 * for rendering. Such *sparse* datasets are ineligible for local filtering.
529 * @accessor
530 */
531 remoteFilter: false,
532
533 /**
534 * @cfg {Boolean} remoteGroup
535 * `true` to defer any grouping operation to the server. If `false`, grouping is done locally on the client.
536 *
537 * {@link #buffered Buffered} stores automatically set this to `true`. Buffered stores contain an abitrary
538 * subset of the full dataset which depends upon various configurations and which pages have been requested
539 * for rendering. Such *sparse* datasets are ineligible for local grouping.
540 * @accessor
541 */
542 remoteGroup: false,
543
544 /**
545 * @cfg {Object[]} filters
546 * Array of {@link Ext.util.Filter Filters} for this store. This configuration is handled by the
547 * {@link Ext.mixin.Filterable Filterable} mixin of the {@link Ext.util.Collection data} collection.
548 * @accessor
549 */
550 filters: null,
551
552 /**
553 * @cfg {Object[]} sorters
554 * Array of {@link Ext.util.Sorter Sorters} for this store. This configuration is handled by the
555 * {@link Ext.mixin.Sortable Sortable} mixin of the {@link Ext.util.Collection data} collection.
556 * See also the {@link #sort} method.
557 * @accessor
558 */
559 sorters: null,
560
561 /**
562 * @cfg {Object} grouper
563 * A configuration object for this Store's {@link Ext.util.Grouper grouper}.
564 *
565 * For example, to group a store's items by the first letter of the last name:
566 *
567 * Ext.define('People', {
568 * extend: 'Ext.data.Store',
569 *
570 * config: {
571 * fields: ['first_name', 'last_name'],
572 *
573 * grouper: {
574 * groupFn: function(record) {
575 * return record.get('last_name').substr(0, 1);
576 * },
577 * sortProperty: 'last_name'
578 * }
579 * }
580 * });
581 *
582 * @accessor
583 */
584 grouper: null,
585
586 /**
587 * @cfg {String} groupField
588 * The (optional) field by which to group data in the store. Internally, grouping is very similar to sorting - the
589 * groupField and {@link #groupDir} are injected as the first sorter (see {@link #sort}). Stores support a single
590 * level of grouping, and groups can be fetched via the {@link #getGroups} method.
591 * @accessor
592 */
593 groupField: null,
594
595 /**
596 * @cfg {String} groupDir
597 * The direction in which sorting should be applied when grouping. If you specify a grouper by using the {@link #groupField}
598 * configuration, this will automatically default to "ASC" - the other supported value is "DESC"
599 * @accessor
600 */
601 groupDir: null,
602
603 /**
604 * @cfg {Function} getGroupString This function will be passed to the {@link #grouper} configuration as it's `groupFn`.
605 * Note that this configuration is deprecated and grouper: `{groupFn: yourFunction}}` is preferred.
606 * @deprecated
607 * @accessor
608 */
609 getGroupString: null,
610
611 /**
612 * @cfg {Number} pageSize
613 * The number of records considered to form a 'page'. This is used to power the built-in
614 * paging using the nextPage and previousPage functions.
615 *
616 * If this Store is {@link #buffered}, pages are loaded into a page cache before the Store's
617 * data is updated from the cache. The pageSize is the number of rows loaded into the cache in one request.
618 * This will not affect the rendering of a buffered grid, but a larger page size will mean fewer loads.
619 *
620 * In a buffered grid, scrolling is monitored, and the page cache is kept primed with data ahead of the
621 * direction of scroll to provide rapid access to data when scrolling causes it to be required. Several pages
622 * in advance may be requested depending on various parameters.
623 *
624 * It is recommended to tune the {@link #pageSize}, {@link #trailingBufferZone} and
625 * {@link #leadingBufferZone} configurations based upon the conditions pertaining in your deployed application.
626 */
627 pageSize: 25,
628
629 /**
630 * @cfg {Number} totalCount The total number of records in the full dataset, as indicated by a server. If the
631 * server-side dataset contains 5000 records but only returns pages of 50 at a time, `totalCount` will be set to
632 * 5000 and {@link #getCount} will return 50
633 */
634 totalCount: null,
635
636 /**
637 * @cfg {Boolean} clearOnPageLoad `true` to empty the store when loading another page via {@link #loadPage},
638 * {@link #nextPage} or {@link #previousPage}. Setting to `false` keeps existing records, allowing
639 * large data sets to be loaded one page at a time but rendered all together.
640 * @accessor
641 */
642 clearOnPageLoad: true,
643
644 /**
645 * @cfg {Object} params Parameters to send into the proxy for any CRUD operations
646 *
647 * @accessor
648 */
649 params: {},
650
651 modelDefaults: {},
652
653 /**
654 * @cfg {Boolean} autoDestroy This is a private configuration used in the framework whether this Store
655 * can be destroyed.
656 * @private
657 */
658 autoDestroy: false,
659
660 /**
661 * @cfg {Boolean} syncRemovedRecords This configuration allows you to disable the synchronization of
662 * removed records on this Store. By default, when you call `removeAll()` or `remove()`, records will be added
663 * to an internal removed array. When you then sync the Store, we send a destroy request for these records.
664 * If you don't want this to happen, you can set this configuration to `false`.
665 */
666 syncRemovedRecords: true,
667
668 /**
669 * @cfg {Boolean} destroyRemovedRecords This configuration allows you to prevent destroying record
670 * instances when they are removed from this store and are not in any other store.
671 */
672 destroyRemovedRecords: true,
673
674 /**
675 * @cfg {Boolean} buffered
676 * Allows the Store to prefetch and cache in a **page cache**, pages of Records, and to then satisfy
677 * loading requirements from this page cache.
678 *
679 * To use buffered Stores, initiate the process by loading the first page. The number of rows rendered are
680 * determined automatically, and the range of pages needed to keep the cache primed for scrolling is
681 * requested and cached.
682 * Example:
683 *
684 * myStore.loadPage(1); // Load page 1
685 *
686 * A {@link Ext.grid.plugin.BufferedRenderer BufferedRenderer} is instantiated which will monitor the scrolling in the grid, and
687 * refresh the view's rows from the page cache as needed. It will also pull new data into the page
688 * cache when scrolling of the view draws upon data near either end of the prefetched data.
689 *
690 * The margins which trigger view refreshing from the prefetched data are {@link Ext.grid.plugin.BufferedRenderer#numFromEdge},
691 * {@link Ext.grid.plugin.BufferedRenderer#leadingBufferZone} and {@link Ext.grid.plugin.BufferedRenderer#trailingBufferZone}.
692 *
693 * The margins which trigger loading more data into the page cache are, {@link #leadingBufferZone} and
694 * {@link #trailingBufferZone}.
695 *
696 * By default, only 5 pages of data are cached in the page cache, with pages "scrolling" out of the buffer
697 * as the view moves down through the dataset.
698 * Setting this value to zero means that no pages are *ever* scrolled out of the page cache, and
699 * that eventually the whole dataset may become present in the page cache. This is sometimes desirable
700 * as long as datasets do not reach astronomical proportions.
701 *
702 * Selection state may be maintained across page boundaries by configuring the SelectionModel not to discard
703 * records from its collection when those Records cycle out of the Store's primary collection. This is done
704 * by configuring the SelectionModel like this:
705 *
706 * selModel: {
707 * pruneRemoved: false
708 * }
709 *
710 */
711 buffered: false,
712
713 /**
714 * @cfg {Object/Array} plugins
715 * @accessor
716 * An object or array of objects that will provide custom functionality for this component. The only
717 * requirement for a valid plugin is that it contain an init method that accepts a reference of type Ext.Component.
718 *
719 * When a component is created, if any plugins are available, the component will call the init method on each
720 * plugin, passing a reference to itself. Each plugin can then call methods or respond to events on the
721 * component as needed to provide its functionality.
722 *
723 * For examples of plugins, see Ext.plugin.PullRefresh and Ext.plugin.ListPaging
724 *
725 * ## Example code
726 *
727 * A plugin by alias:
728 *
729 * Ext.create('Ext.dataview.List', {
730 * config: {
731 * plugins: 'listpaging',
732 * itemTpl: '<div class="item">{title}</div>',
733 * store: 'Items'
734 * }
735 * });
736 *
737 * Multiple plugins by alias:
738 *
739 * Ext.create('Ext.dataview.List', {
740 * config: {
741 * plugins: ['listpaging', 'pullrefresh'],
742 * itemTpl: '<div class="item">{title}</div>',
743 * store: 'Items'
744 * }
745 * });
746 *
747 * Single plugin by class name with config options:
748 *
749 * Ext.create('Ext.dataview.List', {
750 * config: {
751 * plugins: {
752 * xclass: 'Ext.plugin.ListPaging', // Reference plugin by class
753 * autoPaging: true
754 * },
755 *
756 * itemTpl: '<div class="item">{title}</div>',
757 * store: 'Items'
758 * }
759 * });
760 *
761 * Multiple plugins by class name with config options:
762 *
763 * Ext.create('Ext.dataview.List', {
764 * config: {
765 * plugins: [
766 * {
767 * xclass: 'Ext.plugin.PullRefresh',
768 * pullRefreshText: 'Pull to refresh...'
769 * },
770 * {
771 * xclass: 'Ext.plugin.ListPaging',
772 * autoPaging: true
773 * }
774 * ],
775 *
776 * itemTpl: '<div class="item">{title}</div>',
777 * store: 'Items'
778 * }
779 * });
780 *
781 */
782 plugins: null
783 },
784
785 /**
786 * @property {Number} currentPage
787 * The page that the Store has most recently loaded (see {@link #loadPage})
788 */
789 currentPage: 1,
790
791 constructor: function(config) {
792 config = config || {};
793
794 this.data = this._data = this.createDataCollection();
795
796 this.data.setSortRoot('data');
797 this.data.setFilterRoot('data');
798
799 this.removed = [];
800
801 if (config.id && !config.storeId) {
802 config.storeId = config.id;
803 delete config.id;
804 }
805
806 // <deprecated product=touch since=2.0>
807 // <debug>
808 if (config.hasOwnProperty('sortOnLoad')) {
809 Ext.Logger.deprecate(
810 '[Ext.data.Store] sortOnLoad is always activated in Sencha Touch 2 so your Store is always fully ' +
811 'sorted after loading. The only exception is if you are using remoteSort and change sorting after ' +
812 'the Store as loaded, in which case you need to call store.load() to fetch the sorted data from the server.'
813 );
814 }
815
816 if (config.hasOwnProperty('filterOnLoad')) {
817 Ext.Logger.deprecate(
818 '[Ext.data.Store] filterOnLoad is always activated in Sencha Touch 2 so your Store is always fully ' +
819 'sorted after loading. The only exception is if you are using remoteFilter and change filtering after ' +
820 'the Store as loaded, in which case you need to call store.load() to fetch the filtered data from the server.'
821 );
822 }
823
824 if (config.hasOwnProperty('sortOnFilter')) {
825 Ext.Logger.deprecate(
826 '[Ext.data.Store] sortOnFilter is deprecated and is always effectively true when sorting and filtering locally'
827 );
828 }
829 // </debug>
830 // </deprecated>
831
832 this.initConfig(config);
833
834 this.callParent(arguments);
835 },
836
837 applyPlugins: function(config) {
838 var ln, i, configObj;
839
840 if (!config) {
841 return config;
842 }
843
844 config = [].concat(config);
845
846 for (i = 0, ln = config.length; i < ln; i++) {
847 configObj = config[i];
848 config[i] = Ext.factory(configObj, 'Ext.plugin.Plugin', null, 'plugin');
849 }
850
851 return config;
852 },
853
854 updatePlugins: function(newPlugins, oldPlugins) {
855 var ln, i;
856
857 if (newPlugins) {
858 for (i = 0, ln = newPlugins.length; i < ln; i++) {
859 newPlugins[i].init(this);
860 }
861 }
862
863 if (oldPlugins) {
864 for (i = 0, ln = oldPlugins.length; i < ln; i++) {
865 Ext.destroy(oldPlugins[i]);
866 }
867 }
868 },
869
870 /**
871 * @private
872 * @return {Ext.util.Collection}
873 */
874 createDataCollection: function() {
875 return new Ext.util.Collection(function(record) {
876 return record.getId();
877 });
878 },
879
880 applyStoreId: function(storeId) {
881 if (storeId === undefined || storeId === null) {
882 storeId = this.getUniqueId();
883 }
884 return storeId;
885 },
886
887 updateStoreId: function(storeId, oldStoreId) {
888 if (oldStoreId) {
889 Ext.data.StoreManager.unregister(this);
890 }
891 if (storeId) {
892 Ext.data.StoreManager.register(this);
893 }
894 },
895
896 applyModel: function(model) {
897 if (typeof model == 'string') {
898 var registeredModel = Ext.data.ModelManager.getModel(model);
899 if (!registeredModel) {
900 Ext.Logger.error('Model with name "' + model + '" does not exist.');
901 }
902 model = registeredModel;
903 }
904
905 if (model && !model.prototype.isModel && Ext.isObject(model)) {
906 model = Ext.data.ModelManager.registerType(model.storeId || model.id || Ext.id(), model);
907 }
908
909 if (!model) {
910 var fields = this.getFields(),
911 data = this.config.data;
912
913 if (!fields && data && data.length) {
914 fields = Ext.Object.getKeys(data[0]);
915 }
916
917 if (fields) {
918 model = Ext.define('Ext.data.Store.ImplicitModel-' + (this.getStoreId() || Ext.id()), {
919 extend: 'Ext.data.Model',
920 config: {
921 fields: fields,
922 useCache: false,
923 proxy: this.getProxy()
924 }
925 });
926
927 this.implicitModel = true;
928 }
929 }
930 if (!model && this.getProxy()) {
931 model = this.getProxy().getModel();
932 }
933
934 // <debug>
935 if (!model) {
936 Ext.Logger.warn('Unless you define your model through metadata, a store needs to have a model defined on either itself or on its proxy');
937 }
938 // </debug>
939
940 return model;
941 },
942
943 updateModel: function(model) {
944 var proxy = this.getProxy();
945
946 if (proxy && !proxy.getModel()) {
947 proxy.setModel(model);
948 }
949 },
950
951 applyProxy: function(proxy, currentProxy) {
952 proxy = Ext.factory(proxy, Ext.data.Proxy, currentProxy, 'proxy');
953
954 if (!proxy && this.getModel()) {
955 proxy = this.getModel().getProxy();
956 }
957
958 if (!proxy) {
959 proxy = new Ext.data.proxy.Memory({
960 model: this.getModel()
961 });
962 }
963
964 if (proxy.isMemoryProxy) {
965 this.setSyncRemovedRecords(false);
966 }
967
968 return proxy;
969 },
970
971 updateProxy: function(proxy, oldProxy) {
972 if (proxy) {
973 if (!proxy.getModel()) {
974 proxy.setModel(this.getModel());
975 }
976 proxy.on('metachange', 'onMetaChange', this);
977 }
978 if (oldProxy) {
979 proxy.un('metachange', 'onMetaChange', this);
980 }
981 },
982
983 /**
984 * We are using applyData so that we can return nothing and prevent the `this.data`
985 * property to be overridden.
986 * @param {Array/Object} data
987 */
988 applyData: function(data) {
989 var me = this,
990 proxy;
991 if (data) {
992 proxy = me.getProxy();
993 if (proxy instanceof Ext.data.proxy.Memory) {
994 proxy.setData(data);
995 me.load();
996 return;
997 } else {
998 // We make it silent because we don't want to fire a refresh event
999 me.removeAll(true);
1000
1001 // This means we have to fire a clear event though
1002 me.fireEvent('clear', me);
1003
1004 // We don't want to fire addrecords event since we will be firing
1005 // a refresh event later which will already take care of updating
1006 // any views bound to this store
1007 me.suspendEvents();
1008 me.add(data);
1009 me.resumeEvents();
1010
1011 // We set this to true so isAutoLoading to try
1012 me.dataLoaded = true;
1013 }
1014 } else {
1015 me.removeAll(true);
1016
1017 // This means we have to fire a clear event though
1018 me.fireEvent('clear', me);
1019 }
1020
1021 me.fireEvent('refresh', me, me.data);
1022 },
1023
1024 clearData: function() {
1025 this.setData(null);
1026 },
1027
1028 /**
1029 * Uses the configured {@link Ext.data.reader.Reader reader} to convert the data into records
1030 * and adds it to the Store. Use this when you have raw data that needs to pass trough converters,
1031 * mappings and other extra logic from the reader.
1032 *
1033 * If your data is already formated and ready for consumption, use {@link #add} method.
1034 *
1035 * @param {Object[]} data Array of data to load
1036 */
1037 addData: function(data) {
1038 var reader = this.getProxy().getReader(),
1039 resultSet = reader.read(data),
1040 records = resultSet.getRecords();
1041
1042 this.add(records);
1043 },
1044
1045 updateAutoLoad: function(autoLoad) {
1046 var proxy = this.getProxy();
1047 if (autoLoad && (proxy && !proxy.isMemoryProxy)) {
1048 this.load(Ext.isObject(autoLoad) ? autoLoad : null);
1049 }
1050 },
1051
1052 /**
1053 * Returns `true` if the Store is set to {@link #autoLoad} or is a type which loads upon instantiation.
1054 * @return {Boolean}
1055 */
1056 isAutoLoading: function() {
1057 var proxy = this.getProxy();
1058 return (this.getAutoLoad() || (proxy && proxy.isMemoryProxy) || this.dataLoaded);
1059 },
1060
1061 updateGroupField: function(groupField) {
1062 var grouper = this.getGrouper();
1063 if (groupField) {
1064 if (!grouper) {
1065 this.setGrouper({
1066 property: groupField,
1067 direction: this.getGroupDir() || 'ASC'
1068 });
1069 } else {
1070 grouper.setProperty(groupField);
1071 }
1072 } else if (grouper) {
1073 this.setGrouper(null);
1074 }
1075 },
1076
1077 updateGroupDir: function(groupDir) {
1078 var grouper = this.getGrouper();
1079 if (grouper) {
1080 grouper.setDirection(groupDir);
1081 }
1082 },
1083
1084 applyGetGroupString: function(getGroupStringFn) {
1085 var grouper = this.getGrouper();
1086 if (getGroupStringFn) {
1087 // <debug>
1088 Ext.Logger.warn('Specifying getGroupString on a store has been deprecated. Please use grouper: {groupFn: yourFunction}');
1089 // </debug>
1090
1091 if (grouper) {
1092 grouper.setGroupFn(getGroupStringFn);
1093 } else {
1094 this.setGrouper({
1095 groupFn: getGroupStringFn
1096 });
1097 }
1098 } else if (grouper) {
1099 this.setGrouper(null);
1100 }
1101 },
1102
1103 applyGrouper: function(grouper) {
1104 if (typeof grouper == 'string') {
1105 grouper = {
1106 property: grouper
1107 };
1108 }
1109 else if (typeof grouper == 'function') {
1110 grouper = {
1111 groupFn: grouper
1112 };
1113 }
1114
1115 grouper = Ext.factory(grouper, Ext.util.Grouper);
1116 return grouper;
1117 },
1118
1119 updateGrouper: function(grouper, oldGrouper) {
1120 var data = this.data;
1121 if (oldGrouper) {
1122 data.removeSorter(oldGrouper);
1123 if (!grouper) {
1124 data.getSorters().removeSorter('isGrouper');
1125 }
1126 }
1127 if (grouper) {
1128 data.insertSorter(0, grouper);
1129 if (!oldGrouper) {
1130 data.getSorters().addSorter({
1131 direction: 'DESC',
1132 property: 'isGrouper',
1133 transform: function(value) {
1134 return (value === true) ? 1 : -1;
1135 }
1136 });
1137 }
1138 }
1139 this.fireEvent('refresh', this, data);
1140 },
1141
1142 /**
1143 * This method tells you if this store has a grouper defined on it.
1144 * @return {Boolean} `true` if this store has a grouper defined.
1145 */
1146 isGrouped: function() {
1147 return !!this.getGrouper();
1148 },
1149
1150 updateSorters: function(sorters) {
1151 var grouper = this.getGrouper(),
1152 data = this.data,
1153 autoSort = data.getAutoSort();
1154
1155 // While we remove/add sorters we don't want to automatically sort because we still need
1156 // to apply any field sortTypes as transforms on the Sorters after we have added them.
1157 data.setAutoSort(false);
1158
1159 data.setSorters(sorters);
1160 if (grouper) {
1161 data.insertSorter(0, grouper);
1162 }
1163
1164 this.updateSortTypes();
1165
1166 // Now we put back autoSort on the Collection to the value it had before. If it was
1167 // auto sorted, setting this back will cause it to sort right away.
1168 data.setAutoSort(autoSort);
1169 },
1170
1171 updateSortTypes: function() {
1172 var model = this.getModel(),
1173 fields = model && model.getFields(),
1174 data = this.data;
1175
1176 // We loop over each sorter and set it's transform method to the every field's sortType.
1177 if (fields) {
1178 data.getSorters().each(function(sorter) {
1179 var property = sorter.getProperty(),
1180 field;
1181
1182 if (!sorter.isGrouper && property && !sorter.getTransform()) {
1183 field = fields.get(property);
1184 if (field) {
1185 sorter.setTransform(field.getSortType());
1186 }
1187 }
1188 });
1189 }
1190 },
1191
1192 updateFilters: function(filters) {
1193 this.data.setFilters(filters);
1194 },
1195
1196 /**
1197 * Adds Model instance to the Store. This method accepts either:
1198 *
1199 * - An array of Model instances or Model configuration objects.
1200 * - Any number of Model instance or Model configuration object arguments.
1201 *
1202 * The new Model instances will be added at the end of the existing collection.
1203 *
1204 * Sample usage:
1205 *
1206 * myStore.add({some: 'data2'}, {some: 'other data2'});
1207 *
1208 * Use {@link #addData} method instead if you have raw data that need to pass
1209 * through the data reader.
1210 *
1211 * @param {Ext.data.Model[]/Ext.data.Model...} model An array of Model instances
1212 * or Model configuration objects, or variable number of Model instance or config arguments.
1213 * @return {Ext.data.Model[]} The model instances that were added.
1214 */
1215 add: function(records) {
1216 if (!Ext.isArray(records)) {
1217 records = Array.prototype.slice.call(arguments);
1218 }
1219
1220 return this.insert(this.data.length, records);
1221 },
1222
1223 /**
1224 * Inserts Model instances into the Store at the given index and fires the {@link #add} event.
1225 * See also `{@link #add}`.
1226 * @param {Number} index The start index at which to insert the passed Records.
1227 * @param {Ext.data.Model[]} records An Array of Ext.data.Model objects to add to the cache.
1228 * @return {Object}
1229 */
1230 insert: function(index, records) {
1231 if (!Ext.isArray(records)) {
1232 records = Array.prototype.slice.call(arguments, 1);
1233 }
1234
1235 var me = this,
1236 sync = false,
1237 data = this.data,
1238 ln = records.length,
1239 Model = this.getModel(),
1240 modelDefaults = me.getModelDefaults(),
1241 added = false,
1242 i, record;
1243
1244 records = records.slice();
1245
1246 for (i = 0; i < ln; i++) {
1247 record = records[i];
1248 if (!record.isModel) {
1249 record = new Model(record);
1250 }
1251 // If we are adding a record that is already an instance which was still in the
1252 // removed array, then we remove it from the removed array
1253 else if (this.removed.indexOf(record) != -1) {
1254 Ext.Array.remove(this.removed, record);
1255 }
1256
1257 record.set(modelDefaults);
1258 record.join(me);
1259
1260 records[i] = record;
1261
1262 // If this is a newly created record, then we might want to sync it later
1263 sync = sync || (record.phantom === true);
1264 }
1265
1266 // Now we insert all these records in one go to the collection. Saves many function
1267 // calls to data.insert. Does however create two loops over the records we are adding.
1268 if (records.length === 1) {
1269 added = data.insert(index, records[0]);
1270 if (added) {
1271 added = [added];
1272 }
1273 } else {
1274 added = data.insertAll(index, records);
1275 }
1276
1277 if (added) {
1278 me.fireEvent('addrecords', me, added);
1279 }
1280
1281 if (me.getAutoSync() && sync) {
1282 me.sync();
1283 }
1284
1285 return records;
1286 },
1287
1288 /**
1289 * Removes the given record from the Store, firing the `removerecords` event passing all the instances that are removed.
1290 * @param {Ext.data.Model/Ext.data.Model[]} records Model instance or array of instances to remove.
1291 */
1292 remove: function (records) {
1293 if (records.isModel) {
1294 records = [records];
1295 }
1296
1297 var me = this,
1298 sync = false,
1299 i = 0,
1300 autoSync = this.getAutoSync(),
1301 syncRemovedRecords = me.getSyncRemovedRecords(),
1302 destroyRemovedRecords = this.getDestroyRemovedRecords(),
1303 ln = records.length,
1304 indices = [],
1305 removed = [],
1306 isPhantom,
1307 items = me.data.items,
1308 record, index;
1309
1310 for (; i < ln; i++) {
1311 record = records[i];
1312
1313 if (me.data.contains(record)) {
1314 isPhantom = (record.phantom === true);
1315
1316 index = items.indexOf(record);
1317 if (index !== -1) {
1318 removed.push(record);
1319 indices.push(index);
1320 }
1321
1322 record.unjoin(me);
1323 me.data.remove(record);
1324
1325 if (destroyRemovedRecords && !syncRemovedRecords && !record.stores.length) {
1326 record.destroy();
1327 }
1328 else if (!isPhantom && syncRemovedRecords) {
1329 // don't push phantom records onto removed
1330 me.removed.push(record);
1331 }
1332
1333 sync = sync || !isPhantom;
1334 }
1335 }
1336
1337 me.fireEvent('removerecords', me, removed, indices);
1338
1339 if (autoSync && sync) {
1340 me.sync();
1341 }
1342 },
1343
1344 /**
1345 * Removes the model instance at the given index.
1346 * @param {Number} index The record index.
1347 */
1348 removeAt: function(index) {
1349 var record = this.getAt(index);
1350
1351 if (record) {
1352 this.remove(record);
1353 }
1354 },
1355
1356 /**
1357 * Remove all items from the store.
1358 * @param {Boolean} [silent] Prevent the `clear` event from being fired.
1359 */
1360 removeAll: function(silent) {
1361 if (silent !== true && this.eventFiringSuspended !== true) {
1362 this.fireAction('clear', [this], 'doRemoveAll');
1363 } else {
1364 this.doRemoveAll.call(this, true);
1365 }
1366 },
1367
1368 doRemoveAll: function (silent) {
1369 var me = this,
1370 destroyRemovedRecords = this.getDestroyRemovedRecords(),
1371 syncRemovedRecords = this.getSyncRemovedRecords(),
1372 records = me.data.all.slice(),
1373 ln = records.length,
1374 i, record;
1375
1376 for (i = 0; i < ln; i++) {
1377 record = records[i];
1378 record.unjoin(me);
1379
1380 if (destroyRemovedRecords && !syncRemovedRecords && !record.stores.length) {
1381 record.destroy();
1382 }
1383 else if (record.phantom !== true && syncRemovedRecords) {
1384 me.removed.push(record);
1385 }
1386 }
1387
1388 me.data.clear();
1389
1390 if (silent !== true) {
1391 me.fireEvent('refresh', me, me.data);
1392 }
1393
1394 if (me.getAutoSync()) {
1395 this.sync();
1396 }
1397 },
1398
1399 /**
1400 * Calls the specified function for each of the {@link Ext.data.Model Records} in the cache.
1401 *
1402 * // Set up a model to use in our Store
1403 * Ext.define('User', {
1404 * extend: 'Ext.data.Model',
1405 * config: {
1406 * fields: [
1407 * {name: 'firstName', type: 'string'},
1408 * {name: 'lastName', type: 'string'}
1409 * ]
1410 * }
1411 * });
1412 *
1413 * var store = Ext.create('Ext.data.Store', {
1414 * model: 'User',
1415 * data : [
1416 * {firstName: 'Ed', lastName: 'Spencer'},
1417 * {firstName: 'Tommy', lastName: 'Maintz'},
1418 * {firstName: 'Aaron', lastName: 'Conran'},
1419 * {firstName: 'Jamie', lastName: 'Avins'}
1420 * ]
1421 * });
1422 *
1423 * store.each(function (item, index, length) {
1424 * console.log(item.get('firstName'), index);
1425 * });
1426 *
1427 * @param {Function} fn The function to call. Returning `false` aborts and exits the iteration.
1428 * @param {Ext.data.Model} fn.item
1429 * @param {Number} fn.index
1430 * @param {Number} fn.length
1431 * @param {Object} scope (optional) The scope (`this` reference) in which the function is executed.
1432 * Defaults to the current {@link Ext.data.Model Record} in the iteration.
1433 */
1434 each: function(fn, scope) {
1435 this.data.each(fn, scope);
1436 },
1437
1438 /**
1439 * Gets the number of cached records. Note that filtered records are not included in this count.
1440 * If using paging, this may not be the total size of the dataset.
1441 * @return {Number} The number of Records in the Store's cache.
1442 */
1443 getCount: function() {
1444 return this.data.items.length || 0;
1445 },
1446
1447 /**
1448 * Gets the number of all cached records including the ones currently filtered.
1449 * If using paging, this may not be the total size of the dataset.
1450 * @return {Number} The number of all Records in the Store's cache.
1451 */
1452 getAllCount: function () {
1453 return this.data.all.length || 0;
1454 },
1455
1456 /**
1457 * Get the Record at the specified index.
1458 * @param {Number} index The index of the Record to find.
1459 * @return {Ext.data.Model/undefined} The Record at the passed index. Returns `undefined` if not found.
1460 */
1461 getAt: function(index) {
1462 return this.data.getAt(index);
1463 },
1464
1465 /**
1466 * Returns a range of Records between specified indices. Note that if the store is filtered, only filtered results
1467 * are returned.
1468 * @param {Number} [startIndex=0] (optional) The starting index.
1469 * @param {Number} [endIndex=-1] (optional) The ending index (defaults to the last Record in the Store).
1470 * @return {Ext.data.Model[]} An array of Records.
1471 */
1472 getRange: function(start, end) {
1473 return this.data.getRange(start, end);
1474 },
1475
1476 /**
1477 * Get the Record with the specified id.
1478 * @param {String} id The id of the Record to find.
1479 * @return {Ext.data.Model/undefined} The Record with the passed id. Returns `undefined` if not found.
1480 */
1481 getById: function(id) {
1482 return this.data.findBy(function(record) {
1483 return record.getId() == id;
1484 });
1485 },
1486
1487 /**
1488 * Get the index within the cache of the passed Record.
1489 * @param {Ext.data.Model} record The Ext.data.Model object to find.
1490 * @return {Number} The index of the passed Record. Returns -1 if not found.
1491 */
1492 indexOf: function(record) {
1493 return this.data.indexOf(record);
1494 },
1495
1496 /**
1497 * Get the index within the cache of the Record with the passed id.
1498 * @param {String} id The id of the Record to find.
1499 * @return {Number} The index of the Record. Returns -1 if not found.
1500 */
1501 indexOfId: function(id) {
1502 return this.data.indexOfKey(id);
1503 },
1504
1505 /**
1506 * @private
1507 * A model instance should call this method on the Store it has been {@link Ext.data.Model#join joined} to.
1508 * @param {Ext.data.Model} record The model instance that was edited.
1509 * @param {String[]} modifiedFieldNames Array of field names changed during edit.
1510 * @param {Object} modified
1511 */
1512 afterEdit: function(record, modifiedFieldNames, modified) {
1513 var me = this,
1514 data = me.data,
1515 currentId = modified[record.getIdProperty()] || record.getId(),
1516 currentIndex = data.keys.indexOf(currentId),
1517 newIndex;
1518
1519 if (currentIndex === -1 && data.map[currentId] === undefined) {
1520 return;
1521 }
1522
1523 if (me.getAutoSync()) {
1524 me.sync();
1525 }
1526
1527 if (currentId !== record.getId()) {
1528 data.replace(currentId, record);
1529 } else {
1530 data.replace(record);
1531 }
1532
1533 newIndex = data.indexOf(record);
1534 if (currentIndex === -1 && newIndex !== -1) {
1535 me.fireEvent('addrecords', me, [record]);
1536 }
1537 else if (currentIndex !== -1 && newIndex === -1) {
1538 me.fireEvent('removerecords', me, [record], [currentIndex]);
1539 }
1540 else if (newIndex !== -1) {
1541 me.fireEvent('updaterecord', me, record, newIndex, currentIndex, modifiedFieldNames, modified);
1542 }
1543 },
1544
1545 /**
1546 * @private
1547 * A model instance should call this method on the Store it has been {@link Ext.data.Model#join joined} to.
1548 * @param {Ext.data.Model} record The model instance that was edited.
1549 */
1550 afterReject: function(record) {
1551 var index = this.data.indexOf(record);
1552 this.fireEvent('updaterecord', this, record, index, index, [], {});
1553 },
1554
1555 /**
1556 * @private
1557 * A model instance should call this method on the Store it has been {@link Ext.data.Model#join joined} to.
1558 * @param {Ext.data.Model} record The model instance that was edited.
1559 * @param {String[]} modifiedFieldNames
1560 * @param {Object} modified
1561 */
1562 afterCommit: function(record, modifiedFieldNames, modified) {
1563 var me = this,
1564 data = me.data,
1565 currentId = modified[record.getIdProperty()] || record.getId(),
1566 currentIndex = data.keys.indexOf(currentId),
1567 newIndex;
1568
1569 if (currentIndex === -1 && data.map[currentId] === undefined) {
1570 return;
1571 }
1572
1573 if (currentId !== record.getId()) {
1574 data.replace(currentId, record);
1575 } else {
1576 data.replace(record);
1577 }
1578
1579 newIndex = data.indexOf(record);
1580 if (currentIndex === -1 && newIndex !== -1) {
1581 me.fireEvent('addrecords', me, [record]);
1582 }
1583 else if (currentIndex !== -1 && newIndex === -1) {
1584 me.fireEvent('removerecords', me, [record], [currentIndex]);
1585 }
1586 else if (newIndex !== -1) {
1587 me.fireEvent('updaterecord', me, record, newIndex, currentIndex, modifiedFieldNames, modified);
1588 }
1589 },
1590
1591 /**
1592 * This gets called by a record after is gets erased from the server.
1593 * @param {Ext.data.Model} record
1594 * @private
1595 */
1596 afterErase: function(record) {
1597 var me = this,
1598 data = me.data,
1599 index = data.indexOf(record);
1600
1601 if (index !== -1) {
1602 data.remove(record);
1603 me.fireEvent('removerecords', me, [record], [index]);
1604 }
1605 },
1606
1607 applyRemoteFilter: function(value) {
1608 var proxy = this.getProxy();
1609 return value || (proxy && proxy.isSQLProxy === true);
1610 },
1611
1612 applyRemoteSort: function(value) {
1613 var proxy = this.getProxy();
1614 return value || (proxy && proxy.isSQLProxy === true);
1615 },
1616
1617 applyRemoteGroup: function(value) {
1618 var proxy = this.getProxy();
1619 return value || (proxy && proxy.isSQLProxy === true);
1620 },
1621
1622 updateRemoteFilter: function(remoteFilter) {
1623 this.data.setAutoFilter(!remoteFilter);
1624 },
1625
1626 updateRemoteSort: function(remoteSort) {
1627 this.data.setAutoSort(!remoteSort);
1628 },
1629
1630 /**
1631 * Sorts the data in the Store by one or more of its properties. Example usage:
1632 *
1633 * // sort by a single field
1634 * myStore.sort('myField', 'DESC');
1635 *
1636 * // sorting by multiple fields
1637 * myStore.sort([
1638 * {
1639 * property : 'age',
1640 * direction: 'ASC'
1641 * },
1642 * {
1643 * property : 'name',
1644 * direction: 'DESC'
1645 * }
1646 * ]);
1647 *
1648 * Internally, Store converts the passed arguments into an array of {@link Ext.util.Sorter} instances, and delegates
1649 * the actual sorting to its internal {@link Ext.util.Collection}.
1650 *
1651 * When passing a single string argument to sort, Store maintains a ASC/DESC toggler per field, so this code:
1652 *
1653 * store.sort('myField');
1654 * store.sort('myField');
1655 *
1656 * is equivalent to this code:
1657 *
1658 * store.sort('myField', 'ASC');
1659 * store.sort('myField', 'DESC');
1660 *
1661 * because Store handles the toggling automatically.
1662 *
1663 * If the {@link #remoteSort} configuration has been set to `true`, you will have to manually call the {@link #method-load}
1664 * method after you sort to retrieve the sorted data from the server.
1665 *
1666 * @param {String/Ext.util.Sorter[]} sorters Either a string name of one of the fields in this Store's configured
1667 * {@link Ext.data.Model Model}, or an array of sorter configurations.
1668 * @param {String} [defaultDirection=ASC] The default overall direction to sort the data by.
1669 * @param {String} where (Optional) This can be either `'prepend'` or `'append'`. If you leave this undefined
1670 * it will clear the current sorters.
1671 */
1672 sort: function(sorters, defaultDirection, where) {
1673 var data = this.data,
1674 grouper = this.getGrouper(),
1675 autoSort = data.getAutoSort();
1676
1677 if (sorters) {
1678 // While we are adding sorters we don't want to sort right away
1679 // since we need to update sortTypes on the sorters.
1680 data.setAutoSort(false);
1681 if (typeof where === 'string') {
1682 if (where == 'prepend') {
1683 data.insertSorters(grouper ? 1 : 0, sorters, defaultDirection);
1684 } else {
1685 data.addSorters(sorters, defaultDirection);
1686 }
1687 } else {
1688 data.setSorters(null);
1689 if (grouper) {
1690 data.addSorters(grouper);
1691 }
1692 data.addSorters(sorters, defaultDirection);
1693 }
1694 this.updateSortTypes();
1695 // Setting back autoSort to true (if it was like that before) will
1696 // instantly sort the data again.
1697 data.setAutoSort(autoSort);
1698 }
1699
1700 if (!this.getRemoteSort()) {
1701 // If we haven't added any new sorters we have to manually call sort
1702 if (!sorters) {
1703 this.data.sort();
1704 }
1705
1706 this.fireEvent('sort', this, this.data, this.data.getSorters());
1707 if (data.length) {
1708 this.fireEvent('refresh', this, this.data);
1709 }
1710 }
1711 },
1712
1713 /**
1714 * Filters the loaded set of records by a given set of filters.
1715 *
1716 * Filtering by single field:
1717 *
1718 * store.filter("email", /\.com$/);
1719 *
1720 * Using multiple filters:
1721 *
1722 * store.filter([
1723 * {property: "email", value: /\.com$/},
1724 * {filterFn: function(item) { return item.get("age") > 10; }}
1725 * ]);
1726 *
1727 * Using Ext.util.Filter instances instead of config objects
1728 * (note that we need to specify the {@link Ext.util.Filter#root root} config option in this case):
1729 *
1730 * store.filter([
1731 * Ext.create('Ext.util.Filter', {property: "email", value: /\.com$/, root: 'data'}),
1732 * Ext.create('Ext.util.Filter', {filterFn: function(item) { return item.get("age") > 10; }, root: 'data'})
1733 * ]);
1734 *
1735 * If the {@link #remoteFilter} configuration has been set to `true`, you will have to manually call the {@link #method-load}
1736 * method after you filter to retrieve the filtered data from the server.
1737 *
1738 * @param {Object[]/Ext.util.Filter[]/String} filters The set of filters to apply to the data.
1739 * These are stored internally on the store, but the filtering itself is done on the Store's
1740 * {@link Ext.util.MixedCollection MixedCollection}. See MixedCollection's
1741 * {@link Ext.util.MixedCollection#filter filter} method for filter syntax.
1742 * Alternatively, pass in a property string.
1743 * @param {String} [value] value to filter by (only if using a property string as the first argument).
1744 * @param {Boolean} [anyMatch=false] `true` to allow any match, false to anchor regex beginning with `^`.
1745 * @param {Boolean} [caseSensitive=false] `true` to make the filtering regex case sensitive.
1746 */
1747 filter: function(property, value, anyMatch, caseSensitive) {
1748 var data = this.data,
1749 filter = null;
1750
1751 if (property) {
1752 if (Ext.isFunction(property)) {
1753 filter = {filterFn: property};
1754 }
1755 else if (Ext.isArray(property) || property.isFilter) {
1756 filter = property;
1757 }
1758 else {
1759 filter = {
1760 property : property,
1761 value : value,
1762 anyMatch : anyMatch,
1763 caseSensitive: caseSensitive,
1764 id : property
1765 };
1766 }
1767 }
1768
1769 if (this.getRemoteFilter()) {
1770 data.addFilters(filter);
1771 } else {
1772 data.filter(filter);
1773 this.fireEvent('filter', this, data, data.getFilters());
1774 this.fireEvent('refresh', this, data);
1775 }
1776 },
1777
1778 /**
1779 * Filter by a function. The specified function will be called for each
1780 * Record in this Store. If the function returns `true` the Record is included,
1781 * otherwise it is filtered out.
1782 * @param {Function} fn The function to be called. It will be passed the following parameters:
1783 * @param {Ext.data.Model} fn.record The {@link Ext.data.Model record}
1784 * to test for filtering. Access field values using {@link Ext.data.Model#get}.
1785 * @param {Object} fn.id The ID of the Record passed.
1786 * @param {Object} scope (optional) The scope (`this` reference) in which the function is executed. Defaults to this Store.
1787 */
1788 filterBy: function(fn, scope) {
1789 var me = this,
1790 data = me.data,
1791 ln = data.length;
1792
1793 data.filter({
1794 filterFn: function(record) {
1795 return fn.call(scope || me, record, record.getId());
1796 }
1797 });
1798
1799 this.fireEvent('filter', this, data, data.getFilters());
1800
1801 if (data.length !== ln) {
1802 this.fireEvent('refresh', this, data);
1803 }
1804 },
1805
1806 /**
1807 * Query the cached records in this Store using a filtering function. The specified function
1808 * will be called with each record in this Store. If the function returns `true` the record is
1809 * included in the results.
1810 * @param {Function} fn The function to be called. It will be passed the following parameters:
1811 * @param {Ext.data.Model} fn.record The {@link Ext.data.Model record}
1812 * to test for filtering. Access field values using {@link Ext.data.Model#get}.
1813 * @param {Object} fn.id The ID of the Record passed.
1814 * @param {Object} scope (optional) The scope (`this` reference) in which the function is executed. Defaults to this Store.
1815 * @return {Ext.util.MixedCollection} Returns an Ext.util.MixedCollection of the matched records.
1816 */
1817 queryBy: function(fn, scope) {
1818 return this.data.filterBy(fn, scope || this);
1819 },
1820
1821 /**
1822 * Reverts to a view of the Record cache with no filtering applied.
1823 * @param {Boolean} [suppressEvent=false] `true` to clear silently without firing the `refresh` event.
1824 */
1825 clearFilter: function(suppressEvent) {
1826 var ln = this.data.length;
1827 if (suppressEvent) {
1828 this.suspendEvents();
1829 }
1830 this.data.setFilters(null);
1831 if (suppressEvent) {
1832 this.resumeEvents(true);
1833 } else if (ln !== this.data.length) {
1834 this.fireEvent('refresh', this, this.data);
1835 }
1836 },
1837
1838 /**
1839 * Returns `true` if this store is currently filtered.
1840 * @return {Boolean}
1841 */
1842 isFiltered : function () {
1843 return this.data.filtered;
1844 },
1845
1846 /**
1847 * Returns `true` if this store is currently sorted.
1848 * @return {Boolean}
1849 */
1850 isSorted : function () {
1851 return this.data.sorted;
1852 },
1853
1854 getSorters: function() {
1855 var sorters = this.data.getSorters();
1856 return (sorters) ? sorters.items : [];
1857 },
1858
1859 getFilters: function() {
1860 var filters = this.data.getFilters();
1861 return (filters) ? filters.items : [];
1862 },
1863
1864 /**
1865 * Returns an array containing the result of applying the grouper to the records in this store. See {@link #groupField},
1866 * {@link #groupDir} and {@link #grouper}. Example for a store containing records with a color field:
1867 *
1868 * var myStore = Ext.create('Ext.data.Store', {
1869 * groupField: 'color',
1870 * groupDir : 'DESC'
1871 * });
1872 *
1873 * myStore.getGroups(); //returns:
1874 * [
1875 * {
1876 * name: 'yellow',
1877 * children: [
1878 * //all records where the color field is 'yellow'
1879 * ]
1880 * },
1881 * {
1882 * name: 'red',
1883 * children: [
1884 * //all records where the color field is 'red'
1885 * ]
1886 * }
1887 * ]
1888 *
1889 * @param {String} groupName (Optional) Pass in an optional `groupName` argument to access a specific group as defined by {@link #grouper}.
1890 * @return {Object/Object[]} The grouped data.
1891 */
1892 getGroups: function(requestGroupString) {
1893 var records = this.data.items,
1894 length = records.length,
1895 grouper = this.getGrouper(),
1896 groups = [],
1897 pointers = {},
1898 record,
1899 groupStr,
1900 group,
1901 i;
1902
1903 // <debug>
1904 if (!grouper) {
1905 Ext.Logger.error('Trying to get groups for a store that has no grouper');
1906 }
1907 // </debug>
1908
1909 for (i = 0; i < length; i++) {
1910 record = records[i];
1911 groupStr = grouper.getGroupString(record);
1912 group = pointers[groupStr];
1913
1914 if (group === undefined) {
1915 group = {
1916 name: groupStr,
1917 children: []
1918 };
1919
1920 groups.push(group);
1921 pointers[groupStr] = group;
1922 }
1923
1924 group.children.push(record);
1925 }
1926
1927 return requestGroupString ? pointers[requestGroupString] : groups;
1928 },
1929
1930 /**
1931 * @param {Ext.data.Model} record
1932 * @return {null}
1933 */
1934 getGroupString: function(record) {
1935 var grouper = this.getGrouper();
1936 if (grouper) {
1937 return grouper.getGroupString(record);
1938 }
1939 return null;
1940 },
1941
1942 /**
1943 * Finds the index of the first matching Record in this store by a specific field value.
1944 * @param {String} fieldName The name of the Record field to test.
1945 * @param {String/RegExp} value Either a string that the field value
1946 * should begin with, or a RegExp to test against the field.
1947 * @param {Number} startIndex (optional) The index to start searching at.
1948 * @param {Boolean} anyMatch (optional) `true` to match any part of the string, not just the beginning.
1949 * @param {Boolean} caseSensitive (optional) `true` for case sensitive comparison.
1950 * @param {Boolean} [exactMatch=false] (optional) `true` to force exact match (^ and $ characters added to the regex).
1951 * @return {Number} The matched index or -1
1952 */
1953 find: function(fieldName, value, startIndex, anyMatch, caseSensitive, exactMatch) {
1954 var filter = Ext.create('Ext.util.Filter', {
1955 property: fieldName,
1956 value: value,
1957 anyMatch: anyMatch,
1958 caseSensitive: caseSensitive,
1959 exactMatch: exactMatch,
1960 root: 'data'
1961 });
1962 return this.data.findIndexBy(filter.getFilterFn(), null, startIndex);
1963 },
1964
1965 /**
1966 * Finds the first matching Record in this store by a specific field value.
1967 * @param {String} fieldName The name of the Record field to test.
1968 * @param {String/RegExp} value Either a string that the field value
1969 * should begin with, or a RegExp to test against the field.
1970 * @param {Number} startIndex (optional) The index to start searching at.
1971 * @param {Boolean} anyMatch (optional) `true` to match any part of the string, not just the beginning.
1972 * @param {Boolean} caseSensitive (optional) `true` for case sensitive comparison.
1973 * @param {Boolean} [exactMatch=false] (optional) `true` to force exact match (^ and $ characters added to the regex).
1974 * @return {Ext.data.Model} The matched record or `null`.
1975 */
1976 findRecord: function() {
1977 var me = this,
1978 index = me.find.apply(me, arguments);
1979 return index !== -1 ? me.getAt(index) : null;
1980 },
1981
1982 /**
1983 * Finds the index of the first matching Record in this store by a specific field value.
1984 * @param {String} fieldName The name of the Record field to test.
1985 * @param {Object} value The value to match the field against.
1986 * @param {Number} startIndex (optional) The index to start searching at.
1987 * @return {Number} The matched index or -1.
1988 */
1989 findExact: function(fieldName, value, startIndex) {
1990 return this.data.findIndexBy(function(record) {
1991 return record.get(fieldName) === value;
1992 }, this, startIndex);
1993 },
1994
1995 /**
1996 * Find the index of the first matching Record in this Store by a function.
1997 * If the function returns `true` it is considered a match.
1998 * @param {Function} fn The function to be called. It will be passed the following parameters:
1999 * @param {Ext.data.Model} fn.record The {@link Ext.data.Model record}
2000 * to test for filtering. Access field values using {@link Ext.data.Model#get}.
2001 * @param {Object} fn.id The ID of the Record passed.
2002 * @param {Object} scope (optional) The scope (`this` reference) in which the function is executed. Defaults to this Store.
2003 * @param {Number} startIndex (optional) The index to start searching at.
2004 * @return {Number} The matched index or -1.
2005 */
2006 findBy: function(fn, scope, startIndex) {
2007 return this.data.findIndexBy(fn, scope, startIndex);
2008 },
2009
2010 /**
2011 * Loads data into the Store via the configured {@link #proxy}. This uses the Proxy to make an
2012 * asynchronous call to whatever storage backend the Proxy uses, automatically adding the retrieved
2013 * instances into the Store and calling an optional callback if required. Example usage:
2014 *
2015 * store.load({
2016 * callback: function(records, operation, success) {
2017 * // the {@link Ext.data.Operation operation} object contains all of the details of the load operation
2018 * console.log(records);
2019 * },
2020 * scope: this
2021 * });
2022 *
2023 * If only the callback and scope options need to be specified, then one can call it simply like so:
2024 *
2025 * store.load(function(records, operation, success) {
2026 * console.log('loaded records');
2027 * }, this);
2028 *
2029 * @param {Object/Function} [options] config object, passed into the {@link Ext.data.Operation} object before loading.
2030 * @param {Object} [scope] Scope for the function.
2031 * @return {Object}
2032 */
2033 load: function(options, scope) {
2034 var me = this,
2035 operation,
2036 currentPage = me.currentPage,
2037 pageSize = me.getPageSize();
2038
2039 options = options || {};
2040
2041 if (Ext.isFunction(options)) {
2042 options = {
2043 callback: options,
2044 scope: scope || this
2045 };
2046 }
2047
2048 if (me.getRemoteSort()) {
2049 options.sorters = options.sorters || this.getSorters();
2050 }
2051
2052 if (me.getRemoteFilter()) {
2053 options.filters = options.filters || this.getFilters();
2054 }
2055
2056 if (me.getRemoteGroup()) {
2057 options.grouper = options.grouper || this.getGrouper();
2058 }
2059
2060 Ext.applyIf(options, {
2061 page: currentPage,
2062 start: (currentPage - 1) * pageSize,
2063 limit: pageSize,
2064 addRecords: false,
2065 action: 'read',
2066 params: this.getParams(),
2067 model: this.getModel()
2068 });
2069
2070 operation = Ext.create('Ext.data.Operation', options);
2071
2072 if (me.fireEvent('beforeload', me, operation) !== false) {
2073 me.loading = true;
2074 me.getProxy().read(operation, me.onProxyLoad, me);
2075 }
2076
2077 return me;
2078 },
2079
2080 /**
2081 * Returns `true` if the Store is currently performing a load operation.
2082 * @return {Boolean} `true` if the Store is currently loading.
2083 */
2084 isLoading: function() {
2085 return Boolean(this.loading);
2086 },
2087
2088 /**
2089 * Returns `true` if the Store has been loaded.
2090 * @return {Boolean} `true` if the Store has been loaded.
2091 */
2092 isLoaded: function() {
2093 return Boolean(this.loaded);
2094 },
2095
2096 /**
2097 * Synchronizes the Store with its Proxy. This asks the Proxy to batch together any new, updated
2098 * and deleted records in the store, updating the Store's internal representation of the records
2099 * as each operation completes.
2100 * @return {Object}
2101 * @return {Object} return.added
2102 * @return {Object} return.updated
2103 * @return {Object} return.removed
2104 */
2105 sync: function(options) {
2106 var me = this,
2107 operations = {},
2108 toCreate = me.getNewRecords(),
2109 toUpdate = me.getUpdatedRecords(),
2110 toDestroy = me.getRemovedRecords(),
2111 needsSync = false;
2112
2113 if (toCreate.length > 0) {
2114 operations.create = toCreate;
2115 needsSync = true;
2116 }
2117
2118 if (toUpdate.length > 0) {
2119 operations.update = toUpdate;
2120 needsSync = true;
2121 }
2122
2123 if (toDestroy.length > 0) {
2124 operations.destroy = toDestroy;
2125 needsSync = true;
2126 }
2127
2128 if (needsSync && me.fireEvent('beforesync', this, operations) !== false) {
2129 me.getProxy().batch(Ext.merge({
2130 operations: operations,
2131 listeners: me.getBatchListeners()
2132 }, options || {}));
2133 }
2134
2135 return {
2136 added: toCreate,
2137 updated: toUpdate,
2138 removed: toDestroy
2139 };
2140 },
2141
2142 /**
2143 * Convenience function for getting the first model instance in the store.
2144 * @return {Ext.data.Model/undefined} The first model instance in the store, or `undefined`.
2145 */
2146 first: function() {
2147 return this.data.first();
2148 },
2149
2150 /**
2151 * Convenience function for getting the last model instance in the store.
2152 * @return {Ext.data.Model/undefined} The last model instance in the store, or `undefined`.
2153 */
2154 last: function() {
2155 return this.data.last();
2156 },
2157
2158 /**
2159 * Sums the value of `property` for each {@link Ext.data.Model record} between `start`
2160 * and `end` and returns the result.
2161 * @param {String} field The field in each record.
2162 * @return {Number} The sum.
2163 */
2164 sum: function(field) {
2165 var total = 0,
2166 i = 0,
2167 records = this.data.items,
2168 len = records.length;
2169
2170 for (; i < len; ++i) {
2171 total += records[i].get(field);
2172 }
2173
2174 return total;
2175 },
2176
2177 /**
2178 * Gets the minimum value in the store.
2179 * @param {String} field The field in each record.
2180 * @return {Object/undefined} The minimum value, if no items exist, `undefined`.
2181 */
2182 min: function(field) {
2183 var i = 1,
2184 records = this.data.items,
2185 len = records.length,
2186 value, min;
2187
2188 if (len > 0) {
2189 min = records[0].get(field);
2190 }
2191
2192 for (; i < len; ++i) {
2193 value = records[i].get(field);
2194 if (value < min) {
2195 min = value;
2196 }
2197 }
2198 return min;
2199 },
2200
2201 /**
2202 * Gets the maximum value in the store.
2203 * @param {String} field The field in each record.
2204 * @return {Object/undefined} The maximum value, if no items exist, `undefined`.
2205 */
2206 max: function(field) {
2207 var i = 1,
2208 records = this.data.items,
2209 len = records.length,
2210 value,
2211 max;
2212
2213 if (len > 0) {
2214 max = records[0].get(field);
2215 }
2216
2217 for (; i < len; ++i) {
2218 value = records[i].get(field);
2219 if (value > max) {
2220 max = value;
2221 }
2222 }
2223 return max;
2224 },
2225
2226 /**
2227 * Gets the average value in the store.
2228 * @param {String} field The field in each record you want to get the average for.
2229 * @return {Number} The average value, if no items exist, 0.
2230 */
2231 average: function(field) {
2232 var i = 0,
2233 records = this.data.items,
2234 len = records.length,
2235 sum = 0;
2236
2237 if (records.length > 0) {
2238 for (; i < len; ++i) {
2239 sum += records[i].get(field);
2240 }
2241 return sum / len;
2242 }
2243 return 0;
2244 },
2245
2246 /**
2247 * @private
2248 * Returns an object which is passed in as the listeners argument to `proxy.batch` inside `this.sync`.
2249 * This is broken out into a separate function to allow for customization of the listeners.
2250 * @return {Object} The listeners object.
2251 * @return {Object} return.scope
2252 * @return {Object} return.exception
2253 * @return {Object} return.complete
2254 */
2255 getBatchListeners: function() {
2256 return {
2257 scope: this,
2258 exception: this.onBatchException,
2259 complete: this.onBatchComplete
2260 };
2261 },
2262
2263 /**
2264 * @private
2265 * Attached as the `complete` event listener to a proxy's Batch object. Iterates over the batch operations
2266 * and updates the Store's internal data MixedCollection.
2267 */
2268 onBatchComplete: function(batch) {
2269 var me = this,
2270 operations = batch.operations,
2271 length = operations.length,
2272 i;
2273
2274 for (i = 0; i < length; i++) {
2275 me.onProxyWrite(operations[i]);
2276 }
2277 },
2278
2279 onBatchException: function(batch, operation) {
2280 // //decide what to do... could continue with the next operation
2281 // batch.start();
2282 //
2283 // //or retry the last operation
2284 // batch.retry();
2285 },
2286
2287 /**
2288 * @private
2289 * Called internally when a Proxy has completed a load request.
2290 */
2291 onProxyLoad: function(operation) {
2292 var me = this,
2293 records = operation.getRecords(),
2294 resultSet = operation.getResultSet(),
2295 successful = operation.wasSuccessful();
2296
2297 if (resultSet) {
2298 me.setTotalCount(resultSet.getTotal());
2299 }
2300
2301 if (successful) {
2302 this.fireAction('datarefresh', [this, this.data, operation], 'doDataRefresh');
2303 }
2304
2305 me.loaded = true;
2306 me.loading = false;
2307 me.fireEvent('load', this, records, successful, operation);
2308
2309 //this is a callback that would have been passed to the 'read' function and is optional
2310 Ext.callback(operation.getCallback(), operation.getScope() || me, [records, operation, successful]);
2311 },
2312
2313 doDataRefresh: function(store, data, operation) {
2314 var records = operation.getRecords(),
2315 me = this,
2316 destroyRemovedRecords = me.getDestroyRemovedRecords(),
2317 currentRecords = data.all.slice(),
2318 ln = currentRecords.length,
2319 ln2 = records.length,
2320 ids = {},
2321 i, record;
2322
2323 if (operation.getAddRecords() !== true) {
2324 for (i = 0; i < ln2; i++) {
2325 ids[records[i].id] = true;
2326 }
2327 for (i = 0; i < ln; i++) {
2328 record = currentRecords[i];
2329 record.unjoin(me);
2330
2331 // If the record we are removing is not part of the records we are about to add to the store then handle
2332 // the destroying or removing of the record to avoid memory leaks.
2333 if (ids[record.id] !== true && destroyRemovedRecords && !record.stores.length) {
2334 record.destroy();
2335 }
2336 }
2337
2338 data.clear();
2339 // This means we have to fire a clear event though
2340 me.fireEvent('clear', me);
2341 }
2342 if (records && records.length) {
2343 // Now lets add the records without firing an addrecords event
2344 me.suspendEvents();
2345 me.add(records);
2346 me.resumeEvents(true); // true to discard the queue
2347 }
2348
2349 me.fireEvent('refresh', me, data);
2350 },
2351
2352 /**
2353 * @private
2354 * Callback for any write Operation over the Proxy. Updates the Store's MixedCollection to reflect
2355 * the updates provided by the Proxy.
2356 */
2357 onProxyWrite: function(operation) {
2358 var me = this,
2359 success = operation.wasSuccessful(),
2360 records = operation.getRecords();
2361
2362 switch (operation.getAction()) {
2363 case 'create':
2364 me.onCreateRecords(records, operation, success);
2365 break;
2366 case 'update':
2367 me.onUpdateRecords(records, operation, success);
2368 break;
2369 case 'destroy':
2370 me.onDestroyRecords(records, operation, success);
2371 break;
2372 }
2373
2374 if (success) {
2375 me.fireEvent('write', me, operation);
2376 }
2377 //this is a callback that would have been passed to the 'create', 'update' or 'destroy' function and is optional
2378 Ext.callback(operation.getCallback(), operation.getScope() || me, [records, operation, success]);
2379 },
2380
2381 // These methods are now just template methods since updating the records etc is all taken care of
2382 // by the operation itself.
2383 onCreateRecords: function(records, operation, success) {},
2384 onUpdateRecords: function(records, operation, success) {},
2385
2386 onDestroyRecords: function(records, operation, success) {
2387 this.removed = [];
2388 },
2389
2390 onMetaChange: function(data) {
2391 var model = this.getProxy().getModel();
2392 if (!this.getModel() && model) {
2393 this.setModel(model);
2394 }
2395
2396 /**
2397 * @event metachange
2398 * Fires whenever the server has sent back new metadata to reconfigure the Reader.
2399 * @param {Ext.data.Store} this
2400 * @param {Object} data The metadata sent back from the server.
2401 */
2402 this.fireEvent('metachange', this, data);
2403 },
2404
2405 /**
2406 * Returns all Model instances that are either currently a phantom (e.g. have no id), or have an ID but have not
2407 * yet been saved on this Store (this happens when adding a non-phantom record from another Store into this one).
2408 * @return {Ext.data.Model[]} The Model instances.
2409 */
2410 getNewRecords: function() {
2411 return this.data.filterBy(function(item) {
2412 // only want phantom records that are valid
2413 return item.phantom === true && item.isValid();
2414 }).items;
2415 },
2416
2417 /**
2418 * Returns all Model instances that have been updated in the Store but not yet synchronized with the Proxy.
2419 * @return {Ext.data.Model[]} The updated Model instances.
2420 */
2421 getUpdatedRecords: function() {
2422 return this.data.filterBy(function(item) {
2423 // only want dirty records, not phantoms that are valid
2424 return item.dirty === true && item.phantom !== true && item.isValid();
2425 }).items;
2426 },
2427
2428 /**
2429 * Returns any records that have been removed from the store but not yet destroyed on the proxy.
2430 * @return {Ext.data.Model[]} The removed Model instances.
2431 */
2432 getRemovedRecords: function() {
2433 return this.removed;
2434 },
2435
2436 // PAGING METHODS
2437 /**
2438 * Loads a given 'page' of data by setting the start and limit values appropriately. Internally this just causes a normal
2439 * load operation, passing in calculated `start` and `limit` params.
2440 * @param {Number} page The number of the page to load.
2441 * @param {Object} options See options for {@link #method-load}.
2442 * @param {Object} scope
2443 */
2444 loadPage: function(page, options, scope) {
2445 if (typeof options === 'function') {
2446 options = {
2447 callback: options,
2448 scope: scope || this
2449 };
2450
2451 }
2452 var me = this,
2453 pageSize = me.getPageSize(),
2454 clearOnPageLoad = me.getClearOnPageLoad();
2455
2456 options = Ext.apply({}, options);
2457
2458 me.currentPage = page;
2459
2460 me.load(Ext.applyIf(options, {
2461 page: page,
2462 start: (page - 1) * pageSize,
2463 limit: pageSize,
2464 addRecords: !clearOnPageLoad
2465 }));
2466 },
2467
2468 /**
2469 * Loads the next 'page' in the current data set.
2470 * @param {Object} options See options for {@link #method-load}.
2471 */
2472 nextPage: function(options) {
2473 this.loadPage(this.currentPage + 1, options);
2474 },
2475
2476 /**
2477 * Loads the previous 'page' in the current data set.
2478 * @param {Object} options See options for {@link #method-load}.
2479 */
2480 previousPage: function(options) {
2481 this.loadPage(this.currentPage - 1, options);
2482 },
2483
2484 destroy: function() {
2485 this.clearData();
2486 var proxy = this.getProxy();
2487 if (proxy) {
2488 proxy.onDestroy();
2489 }
2490 Ext.data.StoreManager.unregister(this);
2491
2492 Ext.destroy(this.getPlugins());
2493
2494 if (this.implicitModel && this.getModel()) {
2495 delete Ext.data.ModelManager.types[this.getModel().getName()];
2496 }
2497 Ext.destroy(this.data);
2498
2499 this.callParent(arguments);
2500 }
2501
2502 // <deprecated product=touch since=2.0>
2503 ,onClassExtended: function(cls, data) {
2504 var prototype = this.prototype,
2505 defaultConfig = prototype.config,
2506 config = data.config || {},
2507 key;
2508
2509 // Convert deprecated properties in application into a config object
2510 for (key in defaultConfig) {
2511 if (key != "control" && key in data) {
2512 config[key] = data[key];
2513 delete data[key];
2514 // <debug warn>
2515 Ext.Logger.deprecate(key + ' is deprecated as a property directly on the ' + this.$className +
2516 ' prototype. Please put it inside the config object.');
2517 // </debug>
2518 }
2519 }
2520
2521 data.config = config;
2522 }
2523 }, function() {
2524 /**
2525 * Loads an array of data straight into the Store.
2526 * @param {Ext.data.Model[]/Object[]} data Array of data to load. Any non-model instances will be cast into model instances first.
2527 * @param {Boolean} append `true` to add the records to the existing records in the store, `false` to remove the old ones first.
2528 * @deprecated 2.0 Please use #add or #setData instead.
2529 * @method loadData
2530 */
2531 this.override({
2532 loadData: function(data, append) {
2533 Ext.Logger.deprecate("loadData is deprecated, please use either add or setData");
2534 if (append) {
2535 this.add(data);
2536 } else {
2537 this.setData(data);
2538 }
2539 },
2540
2541 //@private
2542 doAddListener: function(name, fn, scope, options, order) {
2543 // <debug>
2544 switch(name) {
2545 case 'update':
2546 Ext.Logger.warn('The update event on Store has been removed. Please use the updaterecord event from now on.');
2547 return this;
2548 case 'add':
2549 Ext.Logger.warn('The add event on Store has been removed. Please use the addrecords event from now on.');
2550 return this;
2551 case 'remove':
2552 Ext.Logger.warn('The remove event on Store has been removed. Please use the removerecords event from now on.');
2553 return this;
2554 case 'datachanged':
2555 Ext.Logger.warn('The datachanged event on Store has been removed. Please use the refresh event from now on.');
2556 return this;
2557 break;
2558 }
2559 // </debug>
2560
2561 return this.callParent(arguments);
2562 }
2563 });
2564
2565 /**
2566 * @member Ext.data.Store
2567 * @method loadRecords
2568 * @inheritdoc Ext.data.Store#add
2569 * @deprecated 2.0.0 Please use {@link #add} instead.
2570 */
2571 Ext.deprecateMethod(this, 'loadRecords', 'add', "Ext.data.Store#loadRecords has been deprecated. Please use the add method.");
2572
2573 // </deprecated>
2574 });