Upgrade to ExtJS 4.0.7 - Released 10/19/2011
[extjs.git] / src / data / TreeStore.js
1 /*
2
3 This file is part of Ext JS 4
4
5 Copyright (c) 2011 Sencha Inc
6
7 Contact:  http://www.sencha.com/contact
8
9 GNU General Public License Usage
10 This file may be used under the terms of the GNU General Public License version 3.0 as published by the Free Software Foundation and appearing in the file LICENSE included in the packaging of this file.  Please review the following information to ensure the GNU General Public License version 3.0 requirements will be met: http://www.gnu.org/copyleft/gpl.html.
11
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
13
14 */
15 /**
16  * The TreeStore is a store implementation that is backed by by an {@link Ext.data.Tree}.
17  * It provides convenience methods for loading nodes, as well as the ability to use
18  * the hierarchical tree structure combined with a store. This class is generally used
19  * in conjunction with {@link Ext.tree.Panel}. This class also relays many events from
20  * the Tree for convenience.
21  *
22  * # Using Models
23  *
24  * If no Model is specified, an implicit model will be created that implements {@link Ext.data.NodeInterface}.
25  * The standard Tree fields will also be copied onto the Model for maintaining their state. These fields are listed
26  * in the {@link Ext.data.NodeInterface} documentation.
27  *
28  * # Reading Nested Data
29  *
30  * For the tree to read nested data, the {@link Ext.data.reader.Reader} must be configured with a root property,
31  * so the reader can find nested data for each node. If a root is not specified, it will default to
32  * 'children'.
33  */
34 Ext.define('Ext.data.TreeStore', {
35     extend: 'Ext.data.AbstractStore',
36     alias: 'store.tree',
37     requires: ['Ext.data.Tree', 'Ext.data.NodeInterface', 'Ext.data.NodeStore'],
38
39     /**
40      * @cfg {Ext.data.Model/Ext.data.NodeInterface/Object} root
41      * The root node for this store. For example:
42      *
43      *     root: {
44      *         expanded: true,
45      *         text: "My Root",
46      *         children: [
47      *             { text: "Child 1", leaf: true },
48      *             { text: "Child 2", expanded: true, children: [
49      *                 { text: "GrandChild", leaf: true }
50      *             ] }
51      *         ]
52      *     }
53      *
54      * Setting the `root` config option is the same as calling {@link #setRootNode}.
55      */
56
57     /**
58      * @cfg {Boolean} clearOnLoad
59      * Remove previously existing child nodes before loading. Default to true.
60      */
61     clearOnLoad : true,
62
63     /**
64      * @cfg {String} nodeParam
65      * The name of the parameter sent to the server which contains the identifier of the node.
66      * Defaults to 'node'.
67      */
68     nodeParam: 'node',
69
70     /**
71      * @cfg {String} defaultRootId
72      * The default root id. Defaults to 'root'
73      */
74     defaultRootId: 'root',
75
76     /**
77      * @cfg {String} defaultRootProperty
78      * The root property to specify on the reader if one is not explicitly defined.
79      */
80     defaultRootProperty: 'children',
81
82     /**
83      * @cfg {Boolean} folderSort
84      * Set to true to automatically prepend a leaf sorter. Defaults to `undefined`.
85      */
86     folderSort: false,
87
88     constructor: function(config) {
89         var me = this,
90             root,
91             fields;
92
93         config = Ext.apply({}, config);
94
95         /**
96          * If we have no fields declare for the store, add some defaults.
97          * These will be ignored if a model is explicitly specified.
98          */
99         fields = config.fields || me.fields;
100         if (!fields) {
101             config.fields = [{name: 'text', type: 'string'}];
102         }
103
104         me.callParent([config]);
105
106         // We create our data tree.
107         me.tree = Ext.create('Ext.data.Tree');
108
109         me.relayEvents(me.tree, [
110             /**
111              * @event append
112              * @alias Ext.data.Tree#append
113              */
114             "append",
115
116             /**
117              * @event remove
118              * @alias Ext.data.Tree#remove
119              */
120             "remove",
121
122             /**
123              * @event move
124              * @alias Ext.data.Tree#move
125              */
126             "move",
127
128             /**
129              * @event insert
130              * @alias Ext.data.Tree#insert
131              */
132             "insert",
133
134             /**
135              * @event beforeappend
136              * @alias Ext.data.Tree#beforeappend
137              */
138             "beforeappend",
139
140             /**
141              * @event beforeremove
142              * @alias Ext.data.Tree#beforeremove
143              */
144             "beforeremove",
145
146             /**
147              * @event beforemove
148              * @alias Ext.data.Tree#beforemove
149              */
150             "beforemove",
151
152             /**
153              * @event beforeinsert
154              * @alias Ext.data.Tree#beforeinsert
155              */
156             "beforeinsert",
157
158              /**
159               * @event expand
160               * @alias Ext.data.Tree#expand
161               */
162              "expand",
163
164              /**
165               * @event collapse
166               * @alias Ext.data.Tree#collapse
167               */
168              "collapse",
169
170              /**
171               * @event beforeexpand
172               * @alias Ext.data.Tree#beforeexpand
173               */
174              "beforeexpand",
175
176              /**
177               * @event beforecollapse
178               * @alias Ext.data.Tree#beforecollapse
179               */
180              "beforecollapse",
181
182              /**
183               * @event rootchange
184               * @alias Ext.data.Tree#rootchange
185               */
186              "rootchange"
187         ]);
188
189         me.tree.on({
190             scope: me,
191             remove: me.onNodeRemove,
192             // this event must follow the relay to beforeitemexpand to allow users to
193             // cancel the expand:
194             beforeexpand: me.onBeforeNodeExpand,
195             beforecollapse: me.onBeforeNodeCollapse,
196             append: me.onNodeAdded,
197             insert: me.onNodeAdded
198         });
199
200         me.onBeforeSort();
201
202         root = me.root;
203         if (root) {
204             delete me.root;
205             me.setRootNode(root);
206         }
207
208         me.addEvents(
209             /**
210              * @event sort
211              * Fires when this TreeStore is sorted.
212              * @param {Ext.data.NodeInterface} node The node that is sorted.
213              */
214             'sort'
215         );
216
217         //<deprecated since=0.99>
218         if (Ext.isDefined(me.nodeParameter)) {
219             if (Ext.isDefined(Ext.global.console)) {
220                 Ext.global.console.warn('Ext.data.TreeStore: nodeParameter has been deprecated. Please use nodeParam instead.');
221             }
222             me.nodeParam = me.nodeParameter;
223             delete me.nodeParameter;
224         }
225         //</deprecated>
226     },
227
228     // inherit docs
229     setProxy: function(proxy) {
230         var reader,
231             needsRoot;
232
233         if (proxy instanceof Ext.data.proxy.Proxy) {
234             // proxy instance, check if a root was set
235             needsRoot = Ext.isEmpty(proxy.getReader().root);
236         } else if (Ext.isString(proxy)) {
237             // string type, means a reader can't be set
238             needsRoot = true;
239         } else {
240             // object, check if a reader and a root were specified.
241             reader = proxy.reader;
242             needsRoot = !(reader && !Ext.isEmpty(reader.root));
243         }
244         proxy = this.callParent(arguments);
245         if (needsRoot) {
246             reader = proxy.getReader();
247             reader.root = this.defaultRootProperty;
248             // force rebuild
249             reader.buildExtractors(true);
250         }
251     },
252
253     // inherit docs
254     onBeforeSort: function() {
255         if (this.folderSort) {
256             this.sort({
257                 property: 'leaf',
258                 direction: 'ASC'
259             }, 'prepend', false);
260         }
261     },
262
263     /**
264      * Called before a node is expanded.
265      * @private
266      * @param {Ext.data.NodeInterface} node The node being expanded.
267      * @param {Function} callback The function to run after the expand finishes
268      * @param {Object} scope The scope in which to run the callback function
269      */
270     onBeforeNodeExpand: function(node, callback, scope) {
271         if (node.isLoaded()) {
272             Ext.callback(callback, scope || node, [node.childNodes]);
273         }
274         else if (node.isLoading()) {
275             this.on('load', function() {
276                 Ext.callback(callback, scope || node, [node.childNodes]);
277             }, this, {single: true});
278         }
279         else {
280             this.read({
281                 node: node,
282                 callback: function() {
283                     Ext.callback(callback, scope || node, [node.childNodes]);
284                 }
285             });
286         }
287     },
288
289     //inherit docs
290     getNewRecords: function() {
291         return Ext.Array.filter(this.tree.flatten(), this.filterNew);
292     },
293
294     //inherit docs
295     getUpdatedRecords: function() {
296         return Ext.Array.filter(this.tree.flatten(), this.filterUpdated);
297     },
298
299     /**
300      * Called before a node is collapsed.
301      * @private
302      * @param {Ext.data.NodeInterface} node The node being collapsed.
303      * @param {Function} callback The function to run after the collapse finishes
304      * @param {Object} scope The scope in which to run the callback function
305      */
306     onBeforeNodeCollapse: function(node, callback, scope) {
307         callback.call(scope || node, node.childNodes);
308     },
309
310     onNodeRemove: function(parent, node) {
311         var removed = this.removed;
312
313         if (!node.isReplace && Ext.Array.indexOf(removed, node) == -1) {
314             removed.push(node);
315         }
316     },
317
318     onNodeAdded: function(parent, node) {
319         var proxy = this.getProxy(),
320             reader = proxy.getReader(),
321             data = node.raw || node.data,
322             dataRoot, children;
323
324         Ext.Array.remove(this.removed, node);
325
326         if (!node.isLeaf() && !node.isLoaded()) {
327             dataRoot = reader.getRoot(data);
328             if (dataRoot) {
329                 this.fillNode(node, reader.extractData(dataRoot));
330                 delete data[reader.root];
331             }
332         }
333     },
334
335     /**
336      * Sets the root node for this store.  See also the {@link #root} config option.
337      * @param {Ext.data.Model/Ext.data.NodeInterface/Object} root
338      * @return {Ext.data.NodeInterface} The new root
339      */
340     setRootNode: function(root) {
341         var me = this;
342
343         root = root || {};
344         if (!root.isNode) {
345             // create a default rootNode and create internal data struct.
346             Ext.applyIf(root, {
347                 id: me.defaultRootId,
348                 text: 'Root',
349                 allowDrag: false
350             });
351             root = Ext.ModelManager.create(root, me.model);
352         }
353         Ext.data.NodeInterface.decorate(root);
354
355         // Because we have decorated the model with new fields,
356         // we need to build new extactor functions on the reader.
357         me.getProxy().getReader().buildExtractors(true);
358
359         // When we add the root to the tree, it will automaticaly get the NodeInterface
360         me.tree.setRootNode(root);
361
362         // If the user has set expanded: true on the root, we want to call the expand function
363         if (!root.isLoaded() && (me.autoLoad === true || root.isExpanded())) {
364             me.load({
365                 node: root
366             });
367         }
368
369         return root;
370     },
371
372     /**
373      * Returns the root node for this tree.
374      * @return {Ext.data.NodeInterface}
375      */
376     getRootNode: function() {
377         return this.tree.getRootNode();
378     },
379
380     /**
381      * Returns the record node by id
382      * @return {Ext.data.NodeInterface}
383      */
384     getNodeById: function(id) {
385         return this.tree.getNodeById(id);
386     },
387
388     /**
389      * Loads the Store using its configured {@link #proxy}.
390      * @param {Object} options (Optional) config object. This is passed into the {@link Ext.data.Operation Operation}
391      * object that is created and then sent to the proxy's {@link Ext.data.proxy.Proxy#read} function.
392      * The options can also contain a node, which indicates which node is to be loaded. If not specified, it will
393      * default to the root node.
394      */
395     load: function(options) {
396         options = options || {};
397         options.params = options.params || {};
398
399         var me = this,
400             node = options.node || me.tree.getRootNode(),
401             root;
402
403         // If there is not a node it means the user hasnt defined a rootnode yet. In this case lets just
404         // create one for them.
405         if (!node) {
406             node = me.setRootNode({
407                 expanded: true
408             });
409         }
410
411         if (me.clearOnLoad) {
412             node.removeAll(true);
413         }
414
415         Ext.applyIf(options, {
416             node: node
417         });
418         options.params[me.nodeParam] = node ? node.getId() : 'root';
419
420         if (node) {
421             node.set('loading', true);
422         }
423
424         return me.callParent([options]);
425     },
426
427
428     /**
429      * Fills a node with a series of child records.
430      * @private
431      * @param {Ext.data.NodeInterface} node The node to fill
432      * @param {Ext.data.Model[]} records The records to add
433      */
434     fillNode: function(node, records) {
435         var me = this,
436             ln = records ? records.length : 0,
437             i = 0, sortCollection;
438
439         if (ln && me.sortOnLoad && !me.remoteSort && me.sorters && me.sorters.items) {
440             sortCollection = Ext.create('Ext.util.MixedCollection');
441             sortCollection.addAll(records);
442             sortCollection.sort(me.sorters.items);
443             records = sortCollection.items;
444         }
445
446         node.set('loaded', true);
447         for (; i < ln; i++) {
448             node.appendChild(records[i], undefined, true);
449         }
450
451         return records;
452     },
453
454     // inherit docs
455     onProxyLoad: function(operation) {
456         var me = this,
457             successful = operation.wasSuccessful(),
458             records = operation.getRecords(),
459             node = operation.node;
460
461         me.loading = false;
462         node.set('loading', false);
463         if (successful) {
464             records = me.fillNode(node, records);
465         }
466         // The load event has an extra node parameter
467         // (differing from the load event described in AbstractStore)
468         /**
469          * @event load
470          * Fires whenever the store reads data from a remote data source.
471          * @param {Ext.data.TreeStore} this
472          * @param {Ext.data.NodeInterface} node The node that was loaded.
473          * @param {Ext.data.Model[]} records An array of records.
474          * @param {Boolean} successful True if the operation was successful.
475          */
476         // deprecate read?
477         me.fireEvent('read', me, operation.node, records, successful);
478         me.fireEvent('load', me, operation.node, records, successful);
479         //this is a callback that would have been passed to the 'read' function and is optional
480         Ext.callback(operation.callback, operation.scope || me, [records, operation, successful]);
481     },
482
483     /**
484      * Creates any new records when a write is returned from the server.
485      * @private
486      * @param {Ext.data.Model[]} records The array of new records
487      * @param {Ext.data.Operation} operation The operation that just completed
488      * @param {Boolean} success True if the operation was successful
489      */
490     onCreateRecords: function(records, operation, success) {
491         if (success) {
492             var i = 0,
493                 length = records.length,
494                 originalRecords = operation.records,
495                 parentNode,
496                 record,
497                 original,
498                 index;
499
500             /*
501              * Loop over each record returned from the server. Assume they are
502              * returned in order of how they were sent. If we find a matching
503              * record, replace it with the newly created one.
504              */
505             for (; i < length; ++i) {
506                 record = records[i];
507                 original = originalRecords[i];
508                 if (original) {
509                     parentNode = original.parentNode;
510                     if (parentNode) {
511                         // prevent being added to the removed cache
512                         original.isReplace = true;
513                         parentNode.replaceChild(record, original);
514                         delete original.isReplace;
515                     }
516                     record.phantom = false;
517                 }
518             }
519         }
520     },
521
522     /**
523      * Updates any records when a write is returned from the server.
524      * @private
525      * @param {Ext.data.Model[]} records The array of updated records
526      * @param {Ext.data.Operation} operation The operation that just completed
527      * @param {Boolean} success True if the operation was successful
528      */
529     onUpdateRecords: function(records, operation, success){
530         if (success) {
531             var me = this,
532                 i = 0,
533                 length = records.length,
534                 data = me.data,
535                 original,
536                 parentNode,
537                 record;
538
539             for (; i < length; ++i) {
540                 record = records[i];
541                 original = me.tree.getNodeById(record.getId());
542                 parentNode = original.parentNode;
543                 if (parentNode) {
544                     // prevent being added to the removed cache
545                     original.isReplace = true;
546                     parentNode.replaceChild(record, original);
547                     original.isReplace = false;
548                 }
549             }
550         }
551     },
552
553     /**
554      * Removes any records when a write is returned from the server.
555      * @private
556      * @param {Ext.data.Model[]} records The array of removed records
557      * @param {Ext.data.Operation} operation The operation that just completed
558      * @param {Boolean} success True if the operation was successful
559      */
560     onDestroyRecords: function(records, operation, success){
561         if (success) {
562             this.removed = [];
563         }
564     },
565
566     // inherit docs
567     removeAll: function() {
568         this.getRootNode().destroy(true);
569         this.fireEvent('clear', this);
570     },
571
572     // inherit docs
573     doSort: function(sorterFn) {
574         var me = this;
575         if (me.remoteSort) {
576             //the load function will pick up the new sorters and request the sorted data from the proxy
577             me.load();
578         } else {
579             me.tree.sort(sorterFn, true);
580             me.fireEvent('datachanged', me);
581         }
582         me.fireEvent('sort', me);
583     }
584 });
585