Upgrade to ExtJS 4.0.1 - Released 05/18/2011
[extjs.git] / src / tree / Panel.js
1 /**
2  * @class Ext.tree.Panel
3  * @extends Ext.panel.Table
4  * 
5  * The TreePanel provides tree-structured UI representation of tree-structured data.
6  * A TreePanel must be bound to a {@link Ext.data.TreeStore}. TreePanel's support
7  * multiple columns through the {@link columns} configuration. 
8  * 
9  * Simple TreePanel using inline data.
10  *
11  * {@img Ext.tree.Panel/Ext.tree.Panel1.png Ext.tree.Panel component}
12  * 
13  * ## Simple Tree Panel (no columns)
14  *
15  *     var store = Ext.create('Ext.data.TreeStore', {
16  *         root: {
17  *             expanded: true, 
18  *             text:"",
19  *             user:"",
20  *             status:"", 
21  *             children: [
22  *                 { text:"detention", leaf: true },
23  *                 { text:"homework", expanded: true, 
24  *                     children: [
25  *                         { text:"book report", leaf: true },
26  *                         { text:"alegrbra", leaf: true}
27  *                     ]
28  *                 },
29  *                 { text: "buy lottery tickets", leaf:true }
30  *             ]
31  *         }
32  *     });     
33  *             
34  *     Ext.create('Ext.tree.Panel', {
35  *         title: 'Simple Tree',
36  *         width: 200,
37  *         height: 150,
38  *         store: store,
39  *         rootVisible: false,        
40  *         renderTo: Ext.getBody()
41  *     });
42  *
43  * @xtype treepanel
44  */
45 Ext.define('Ext.tree.Panel', {
46     extend: 'Ext.panel.Table',
47     alias: 'widget.treepanel',
48     alternateClassName: ['Ext.tree.TreePanel', 'Ext.TreePanel'],
49     requires: ['Ext.tree.View', 'Ext.selection.TreeModel', 'Ext.tree.Column'],
50     viewType: 'treeview',
51     selType: 'treemodel',
52     
53     treeCls: Ext.baseCSSPrefix + 'tree-panel',
54     
55     /**
56      * @cfg {Boolean} lines false to disable tree lines (defaults to true)
57      */
58     lines: true,
59     
60     /**
61      * @cfg {Boolean} useArrows true to use Vista-style arrows in the tree (defaults to false)
62      */
63     useArrows: false,
64     
65     /**
66      * @cfg {Boolean} singleExpand <tt>true</tt> if only 1 node per branch may be expanded
67      */
68     singleExpand: false,
69     
70     ddConfig: {
71         enableDrag: true,
72         enableDrop: true
73     },
74     
75     /** 
76      * @cfg {Boolean} animate <tt>true</tt> to enable animated expand/collapse (defaults to the value of {@link Ext#enableFx Ext.enableFx})
77      */
78             
79     /** 
80      * @cfg {Boolean} rootVisible <tt>false</tt> to hide the root node (defaults to <tt>true</tt>)
81      */
82     rootVisible: true,
83     
84     /** 
85      * @cfg {Boolean} displayField The field inside the model that will be used as the node's text. (defaults to <tt>text</tt>)
86      */    
87     displayField: 'text',
88
89     /** 
90      * @cfg {Boolean} root Allows you to not specify a store on this TreePanel. This is useful for creating a simple
91      * tree with preloaded data without having to specify a TreeStore and Model. A store and model will be created and
92      * root will be passed to that store.
93      */
94     root: null,
95     
96     // Required for the Lockable Mixin. These are the configurations which will be copied to the
97     // normal and locked sub tablepanels
98     normalCfgCopy: ['displayField', 'root', 'singleExpand', 'useArrows', 'lines', 'rootVisible', 'scroll'],
99     lockedCfgCopy: ['displayField', 'root', 'singleExpand', 'useArrows', 'lines', 'rootVisible'],
100
101     /**
102      * @cfg {Boolean} hideHeaders
103      * Specify as <code>true</code> to hide the headers.
104      */
105     
106     /**
107      * @cfg {Boolean} folderSort Set to true to automatically prepend a leaf sorter to the store (defaults to <tt>undefined</tt>)
108      */ 
109     
110     constructor: function(config) {
111         config = config || {};
112         if (config.animate === undefined) {
113             config.animate = Ext.enableFx;
114         }
115         this.enableAnimations = config.animate;
116         delete config.animate;
117         
118         this.callParent([config]);
119     },
120     
121     initComponent: function() {
122         var me = this,
123             cls = [me.treeCls];
124
125         if (me.useArrows) {
126             cls.push(Ext.baseCSSPrefix + 'tree-arrows');
127             me.lines = false;
128         }
129         
130         if (me.lines) {
131             cls.push(Ext.baseCSSPrefix + 'tree-lines');
132         } else if (!me.useArrows) {
133             cls.push(Ext.baseCSSPrefix + 'tree-no-lines');
134         }
135         
136         if (Ext.isString(me.store)) {
137             me.store = Ext.StoreMgr.lookup(me.store);
138         } else if (!me.store || Ext.isObject(me.store) && !me.store.isStore) {
139             me.store = Ext.create('Ext.data.TreeStore', Ext.apply({}, me.store || {}, {
140                 root: me.root,
141                 fields: me.fields,
142                 model: me.model,
143                 folderSort: me.folderSort
144             }));
145         } else if (me.root) {
146             me.store = Ext.data.StoreManager.lookup(me.store);
147             me.store.setRootNode(me.root);
148             if (me.folderSort !== undefined) {
149                 me.store.folderSort = me.folderSort;
150                 me.store.sort();
151             }            
152         }
153         
154         // I'm not sure if we want to this. It might be confusing
155         // if (me.initialConfig.rootVisible === undefined && !me.getRootNode()) {
156         //     me.rootVisible = false;
157         // }
158         
159         me.viewConfig = Ext.applyIf(me.viewConfig || {}, {
160             rootVisible: me.rootVisible,
161             animate: me.enableAnimations,
162             singleExpand: me.singleExpand,
163             node: me.store.getRootNode(),
164             hideHeaders: me.hideHeaders
165         });
166         
167         me.mon(me.store, {
168             scope: me,
169             rootchange: me.onRootChange,
170             clear: me.onClear
171         });
172     
173         me.relayEvents(me.store, [
174             /**
175              * @event beforeload
176              * Event description
177              * @param {Ext.data.Store} store This Store
178              * @param {Ext.data.Operation} operation The Ext.data.Operation object that will be passed to the Proxy to load the Store
179              */
180             'beforeload',
181
182             /**
183              * @event load
184              * Fires whenever the store reads data from a remote data source.
185              * @param {Ext.data.store} this
186              * @param {Array} records An array of records
187              * @param {Boolean} successful True if the operation was successful.
188              */
189             'load'   
190         ]);
191         
192         me.store.on({
193             /**
194              * @event itemappend
195              * Fires when a new child node is appended to a node in the tree.
196              * @param {Tree} tree The owner tree
197              * @param {Node} parent The parent node
198              * @param {Node} node The newly appended node
199              * @param {Number} index The index of the newly appended node
200              */
201             append: me.createRelayer('itemappend'),
202             
203             /**
204              * @event itemremove
205              * Fires when a child node is removed from a node in the tree
206              * @param {Tree} tree The owner tree
207              * @param {Node} parent The parent node
208              * @param {Node} node The child node removed
209              */
210             remove: me.createRelayer('itemremove'),
211             
212             /**
213              * @event itemmove
214              * Fires when a node is moved to a new location in the tree
215              * @param {Tree} tree The owner tree
216              * @param {Node} node The node moved
217              * @param {Node} oldParent The old parent of this node
218              * @param {Node} newParent The new parent of this node
219              * @param {Number} index The index it was moved to
220              */
221             move: me.createRelayer('itemmove'),
222             
223             /**
224              * @event iteminsert
225              * Fires when a new child node is inserted in a node in tree
226              * @param {Tree} tree The owner tree
227              * @param {Node} parent The parent node
228              * @param {Node} node The child node inserted
229              * @param {Node} refNode The child node the node was inserted before
230              */
231             insert: me.createRelayer('iteminsert'),
232             
233             /**
234              * @event beforeitemappend
235              * Fires before a new child is appended to a node in this tree, return false to cancel the append.
236              * @param {Tree} tree The owner tree
237              * @param {Node} parent The parent node
238              * @param {Node} node The child node to be appended
239              */
240             beforeappend: me.createRelayer('beforeitemappend'),
241             
242             /**
243              * @event beforeitemremove
244              * Fires before a child is removed from a node in this tree, return false to cancel the remove.
245              * @param {Tree} tree The owner tree
246              * @param {Node} parent The parent node
247              * @param {Node} node The child node to be removed
248              */
249             beforeremove: me.createRelayer('beforeitemremove'),
250             
251             /**
252              * @event beforeitemmove
253              * Fires before a node is moved to a new location in the tree. Return false to cancel the move.
254              * @param {Tree} tree The owner tree
255              * @param {Node} node The node being moved
256              * @param {Node} oldParent The parent of the node
257              * @param {Node} newParent The new parent the node is moving to
258              * @param {Number} index The index it is being moved to
259              */
260             beforemove: me.createRelayer('beforeitemmove'),
261             
262             /**
263              * @event beforeiteminsert
264              * Fires before a new child is inserted in a node in this tree, return false to cancel the insert.
265              * @param {Tree} tree The owner tree
266              * @param {Node} parent The parent node
267              * @param {Node} node The child node to be inserted
268              * @param {Node} refNode The child node the node is being inserted before
269              */
270             beforeinsert: me.createRelayer('beforeiteminsert'),
271              
272             /**
273              * @event itemexpand
274              * Fires when a node is expanded.
275              * @param {Node} this The expanding node
276              */
277             expand: me.createRelayer('itemexpand'),
278              
279             /**
280              * @event itemcollapse
281              * Fires when a node is collapsed.
282              * @param {Node} this The collapsing node
283              */
284             collapse: me.createRelayer('itemcollapse'),
285              
286             /**
287              * @event beforeitemexpand
288              * Fires before a node is expanded.
289              * @param {Node} this The expanding node
290              */
291             beforeexpand: me.createRelayer('beforeitemexpand'),
292              
293             /**
294              * @event beforeitemcollapse
295              * Fires before a node is collapsed.
296              * @param {Node} this The collapsing node
297              */
298             beforecollapse: me.createRelayer('beforeitemcollapse')
299         });
300         
301         // If the user specifies the headers collection manually then dont inject our own
302         if (!me.columns) {
303             if (me.initialConfig.hideHeaders === undefined) {
304                 me.hideHeaders = true;
305             }
306             me.columns = [{
307                 xtype    : 'treecolumn',
308                 text     : 'Name',
309                 flex     : 1,
310                 dataIndex: me.displayField         
311             }];
312         }
313         
314         if (me.cls) {
315             cls.push(me.cls);
316         }
317         me.cls = cls.join(' ');
318         me.callParent();
319         
320         me.relayEvents(me.getView(), [
321             /**
322              * @event checkchange
323              * Fires when a node with a checkbox's checked property changes
324              * @param {Ext.data.Model} node The node who's checked property was changed
325              * @param {Boolean} checked The node's new checked state
326              */
327             'checkchange'
328         ]);
329             
330         // If the root is not visible and there is no rootnode defined, then just lets load the store
331         if (!me.getView().rootVisible && !me.getRootNode()) {
332             me.setRootNode({
333                 expanded: true
334             });
335         }
336     },
337     
338     onClear: function(){
339         this.view.onClear();
340     },
341     
342     setRootNode: function() {
343         return this.store.setRootNode.apply(this.store, arguments);
344     },
345     
346     getRootNode: function() {
347         return this.store.getRootNode();
348     },
349     
350     onRootChange: function(root) {
351         this.view.setRootNode(root);
352     },
353
354     /**
355      * Retrieve an array of checked records.
356      * @return {Array} An array containing the checked records
357      */
358     getChecked: function() {
359         return this.getView().getChecked();
360     },
361     
362     isItemChecked: function(rec) {
363         return rec.get('checked');
364     },
365         
366     /**
367      * Expand all nodes
368      * @param {Function} callback (optional) A function to execute when the expand finishes.
369      * @param {Object} scope (optional) The scope of the callback function
370      */
371     expandAll : function(callback, scope) {
372         var root = this.getRootNode();
373         if (root) {
374             root.expand(true, callback, scope);
375         }
376     },
377
378     /**
379      * Collapse all nodes
380      * @param {Function} callback (optional) A function to execute when the collapse finishes.
381      * @param {Object} scope (optional) The scope of the callback function
382      */
383     collapseAll : function(callback, scope) {
384         var root = this.getRootNode();
385         if (root) {
386             if (this.getView().rootVisible) {
387                 root.collapse(true, callback, scope);
388             }
389             else {
390                 root.collapseChildren(true, callback, scope);
391             }
392         }
393     },
394
395     /**
396      * Expand the tree to the path of a particular node.
397      * @param {String} path The path to expand. The path should include a leading separator.
398      * @param {String} field (optional) The field to get the data from. Defaults to the model idProperty.
399      * @param {String} separator (optional) A separator to use. Defaults to <tt>'/'</tt>.
400      * @param {Function} callback (optional) A function to execute when the expand finishes. The callback will be called with
401      * (success, lastNode) where success is if the expand was successful and lastNode is the last node that was expanded.
402      * @param {Object} scope (optional) The scope of the callback function
403      */
404     expandPath: function(path, field, separator, callback, scope) {
405         var me = this,
406             current = me.getRootNode(),
407             index = 1,
408             view = me.getView(),
409             keys,
410             expander;
411         
412         field = field || me.getRootNode().idProperty;
413         separator = separator || '/';
414         
415         if (Ext.isEmpty(path)) {
416             Ext.callback(callback, scope || me, [false, null]);
417             return;
418         }
419         
420         keys = path.split(separator);
421         if (current.get(field) != keys[1]) {
422             // invalid root
423             Ext.callback(callback, scope || me, [false, current]);
424             return;
425         }
426         
427         expander = function(){
428             if (++index === keys.length) {
429                 Ext.callback(callback, scope || me, [true, current]);
430                 return;
431             }
432             var node = current.findChild(field, keys[index]);
433             if (!node) {
434                 Ext.callback(callback, scope || me, [false, current]);
435                 return;
436             }
437             current = node;
438             current.expand(false, expander);
439         };
440         current.expand(false, expander);
441     },
442     
443     /**
444      * Expand the tree to the path of a particular node, then selecti t.
445      * @param {String} path The path to select. The path should include a leading separator.
446      * @param {String} field (optional) The field to get the data from. Defaults to the model idProperty.
447      * @param {String} separator (optional) A separator to use. Defaults to <tt>'/'</tt>.
448      * @param {Function} callback (optional) A function to execute when the select finishes. The callback will be called with
449      * (bSuccess, oLastNode) where bSuccess is if the select was successful and oLastNode is the last node that was expanded.
450      * @param {Object} scope (optional) The scope of the callback function
451      */
452     selectPath: function(path, field, separator, callback, scope) {
453         var me = this,
454             keys,
455             last;
456         
457         field = field || me.getRootNode().idProperty;
458         separator = separator || '/';
459         
460         keys = path.split(separator);
461         last = keys.pop();
462         
463         me.expandPath(keys.join('/'), field, separator, function(success, node){
464             var doSuccess = false;
465             if (success && node) {
466                 node = node.findChild(field, last);
467                 if (node) {
468                     me.getSelectionModel().select(node);
469                     Ext.callback(callback, scope || me, [true, node]);
470                     doSuccess = true;
471                 }
472             } else if (node === me.getRootNode()) {
473                 doSuccess = true;
474             }
475             Ext.callback(callback, scope || me, [doSuccess, node]);
476         }, me);
477     }
478 });