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