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