Upgrade to ExtJS 4.0.7 - Released 10/19/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  *     @example
23  *     var store = Ext.create('Ext.data.TreeStore', {
24  *         root: {
25  *             expanded: true,
26  *             children: [
27  *                 { text: "detention", leaf: true },
28  *                 { text: "homework", expanded: true, children: [
29  *                     { text: "book report", leaf: true },
30  *                     { text: "alegrbra", leaf: true}
31  *                 ] },
32  *                 { text: "buy lottery tickets", leaf: true }
33  *             ]
34  *         }
35  *     });
36  *
37  *     Ext.create('Ext.tree.Panel', {
38  *         title: 'Simple Tree',
39  *         width: 200,
40  *         height: 150,
41  *         store: store,
42  *         rootVisible: false,
43  *         renderTo: Ext.getBody()
44  *     });
45  *
46  * For the tree node config options (like `text`, `leaf`, `expanded`), see the documentation of
47  * {@link Ext.data.NodeInterface NodeInterface} config options.
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.
63      */
64     lines: true,
65
66     /**
67      * @cfg {Boolean} useArrows True to use Vista-style arrows in the tree.
68      */
69     useArrows: false,
70
71     /**
72      * @cfg {Boolean} singleExpand True if only 1 node per branch may be expanded.
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.
87      */
88     rootVisible: true,
89
90     /**
91      * @cfg {Boolean} displayField The field inside the model that will be used as the node's 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              * @alias Ext.data.Store#beforeload
196              */
197             'beforeload',
198
199             /**
200              * @event load
201              * @alias Ext.data.Store#load
202              */
203             'load'
204         ]);
205
206         me.store.on({
207             /**
208              * @event itemappend
209              * @alias Ext.data.TreeStore#append
210              */
211             append: me.createRelayer('itemappend'),
212
213             /**
214              * @event itemremove
215              * @alias Ext.data.TreeStore#remove
216              */
217             remove: me.createRelayer('itemremove'),
218
219             /**
220              * @event itemmove
221              * @alias Ext.data.TreeStore#move
222              */
223             move: me.createRelayer('itemmove'),
224
225             /**
226              * @event iteminsert
227              * @alias Ext.data.TreeStore#insert
228              */
229             insert: me.createRelayer('iteminsert'),
230
231             /**
232              * @event beforeitemappend
233              * @alias Ext.data.TreeStore#beforeappend
234              */
235             beforeappend: me.createRelayer('beforeitemappend'),
236
237             /**
238              * @event beforeitemremove
239              * @alias Ext.data.TreeStore#beforeremove
240              */
241             beforeremove: me.createRelayer('beforeitemremove'),
242
243             /**
244              * @event beforeitemmove
245              * @alias Ext.data.TreeStore#beforemove
246              */
247             beforemove: me.createRelayer('beforeitemmove'),
248
249             /**
250              * @event beforeiteminsert
251              * @alias Ext.data.TreeStore#beforeinsert
252              */
253             beforeinsert: me.createRelayer('beforeiteminsert'),
254
255             /**
256              * @event itemexpand
257              * @alias Ext.data.TreeStore#expand
258              */
259             expand: me.createRelayer('itemexpand'),
260
261             /**
262              * @event itemcollapse
263              * @alias Ext.data.TreeStore#collapse
264              */
265             collapse: me.createRelayer('itemcollapse'),
266
267             /**
268              * @event beforeitemexpand
269              * @alias Ext.data.TreeStore#beforeexpand
270              */
271             beforeexpand: me.createRelayer('beforeitemexpand'),
272
273             /**
274              * @event beforeitemcollapse
275              * @alias Ext.data.TreeStore#beforecollapse
276              */
277             beforecollapse: me.createRelayer('beforeitemcollapse')
278         });
279
280         // If the user specifies the headers collection manually then dont inject our own
281         if (!me.columns) {
282             if (me.initialConfig.hideHeaders === undefined) {
283                 me.hideHeaders = true;
284             }
285             me.columns = [{
286                 xtype    : 'treecolumn',
287                 text     : 'Name',
288                 flex     : 1,
289                 dataIndex: me.displayField
290             }];
291         }
292
293         if (me.cls) {
294             cls.push(me.cls);
295         }
296         me.cls = cls.join(' ');
297         me.callParent();
298
299         me.relayEvents(me.getView(), [
300             /**
301              * @event checkchange
302              * Fires when a node with a checkbox's checked property changes
303              * @param {Ext.data.Model} node The node who's checked property was changed
304              * @param {Boolean} checked The node's new checked state
305              */
306             'checkchange'
307         ]);
308
309         // If the root is not visible and there is no rootnode defined, then just lets load the store
310         if (!me.getView().rootVisible && !me.getRootNode()) {
311             me.setRootNode({
312                 expanded: true
313             });
314         }
315     },
316
317     onClear: function(){
318         this.view.onClear();
319     },
320
321     /**
322      * Sets root node of this tree.
323      * @param {Ext.data.Model/Ext.data.NodeInterface/Object} root
324      * @return {Ext.data.NodeInterface} The new root
325      */
326     setRootNode: function() {
327         return this.store.setRootNode.apply(this.store, arguments);
328     },
329
330     /**
331      * Returns the root node for this tree.
332      * @return {Ext.data.NodeInterface}
333      */
334     getRootNode: function() {
335         return this.store.getRootNode();
336     },
337
338     onRootChange: function(root) {
339         this.view.setRootNode(root);
340     },
341
342     /**
343      * Retrieve an array of checked records.
344      * @return {Ext.data.Model[]} An array containing the checked records
345      */
346     getChecked: function() {
347         return this.getView().getChecked();
348     },
349
350     isItemChecked: function(rec) {
351         return rec.get('checked');
352     },
353
354     /**
355      * Expand all nodes
356      * @param {Function} callback (optional) A function to execute when the expand finishes.
357      * @param {Object} scope (optional) The scope of the callback function
358      */
359     expandAll : function(callback, scope) {
360         var root = this.getRootNode(),
361             animate = this.enableAnimations,
362             view = this.getView();
363         if (root) {
364             if (!animate) {
365                 view.beginBulkUpdate();
366             }
367             root.expand(true, callback, scope);
368             if (!animate) {
369                 view.endBulkUpdate();
370             }
371         }
372     },
373
374     /**
375      * Collapse all nodes
376      * @param {Function} callback (optional) A function to execute when the collapse finishes.
377      * @param {Object} scope (optional) The scope of the callback function
378      */
379     collapseAll : function(callback, scope) {
380         var root = this.getRootNode(),
381             animate = this.enableAnimations,
382             view = this.getView();
383
384         if (root) {
385             if (!animate) {
386                 view.beginBulkUpdate();
387             }
388             if (view.rootVisible) {
389                 root.collapse(true, callback, scope);
390             } else {
391                 root.collapseChildren(true, callback, scope);
392             }
393             if (!animate) {
394                 view.endBulkUpdate();
395             }
396         }
397     },
398
399     /**
400      * Expand the tree to the path of a particular node.
401      * @param {String} path The path to expand. The path should include a leading separator.
402      * @param {String} field (optional) The field to get the data from. Defaults to the model idProperty.
403      * @param {String} separator (optional) A separator to use. Defaults to `'/'`.
404      * @param {Function} callback (optional) A function to execute when the expand finishes. The callback will be called with
405      * (success, lastNode) where success is if the expand was successful and lastNode is the last node that was expanded.
406      * @param {Object} scope (optional) The scope of the callback function
407      */
408     expandPath: function(path, field, separator, callback, scope) {
409         var me = this,
410             current = me.getRootNode(),
411             index = 1,
412             view = me.getView(),
413             keys,
414             expander;
415
416         field = field || me.getRootNode().idProperty;
417         separator = separator || '/';
418
419         if (Ext.isEmpty(path)) {
420             Ext.callback(callback, scope || me, [false, null]);
421             return;
422         }
423
424         keys = path.split(separator);
425         if (current.get(field) != keys[1]) {
426             // invalid root
427             Ext.callback(callback, scope || me, [false, current]);
428             return;
429         }
430
431         expander = function(){
432             if (++index === keys.length) {
433                 Ext.callback(callback, scope || me, [true, current]);
434                 return;
435             }
436             var node = current.findChild(field, keys[index]);
437             if (!node) {
438                 Ext.callback(callback, scope || me, [false, current]);
439                 return;
440             }
441             current = node;
442             current.expand(false, expander);
443         };
444         current.expand(false, expander);
445     },
446
447     /**
448      * Expand the tree to the path of a particular node, then select it.
449      * @param {String} path The path to select. The path should include a leading separator.
450      * @param {String} field (optional) The field to get the data from. Defaults to the model idProperty.
451      * @param {String} separator (optional) A separator to use. Defaults to `'/'`.
452      * @param {Function} callback (optional) A function to execute when the select finishes. The callback will be called with
453      * (bSuccess, oLastNode) where bSuccess is if the select was successful and oLastNode is the last node that was expanded.
454      * @param {Object} scope (optional) The scope of the callback function
455      */
456     selectPath: function(path, field, separator, callback, scope) {
457         var me = this,
458             keys,
459             last;
460
461         field = field || me.getRootNode().idProperty;
462         separator = separator || '/';
463
464         keys = path.split(separator);
465         last = keys.pop();
466
467         me.expandPath(keys.join(separator), field, separator, function(success, node){
468             var doSuccess = false;
469             if (success && node) {
470                 node = node.findChild(field, last);
471                 if (node) {
472                     me.getSelectionModel().select(node);
473                     Ext.callback(callback, scope || me, [true, node]);
474                     doSuccess = true;
475                 }
476             } else if (node === me.getRootNode()) {
477                 doSuccess = true;
478             }
479             Ext.callback(callback, scope || me, [doSuccess, node]);
480         }, me);
481     }
482 });
483