Upgrade to ExtJS 3.1.1 - Released 02/08/2010
[extjs.git] / src / widgets / tree / TreeNode.js
1 /*!
2  * Ext JS Library 3.1.1
3  * Copyright(c) 2006-2010 Ext JS, LLC
4  * licensing@extjs.com
5  * http://www.extjs.com/license
6  */
7 /**
8  * @class Ext.tree.TreeNode
9  * @extends Ext.data.Node
10  * @cfg {String} text The text for this node
11  * @cfg {Boolean} expanded true to start the node expanded
12  * @cfg {Boolean} allowDrag False to make this node undraggable if {@link #draggable} = true (defaults to true)
13  * @cfg {Boolean} allowDrop False if this node cannot have child nodes dropped on it (defaults to true)
14  * @cfg {Boolean} disabled true to start the node disabled
15  * @cfg {String} icon The path to an icon for the node. The preferred way to do this
16  * is to use the cls or iconCls attributes and add the icon via a CSS background image.
17  * @cfg {String} cls A css class to be added to the node
18  * @cfg {String} iconCls A css class to be added to the nodes icon element for applying css background images
19  * @cfg {String} href URL of the link used for the node (defaults to #)
20  * @cfg {String} hrefTarget target frame for the link
21  * @cfg {Boolean} hidden True to render hidden. (Defaults to false).
22  * @cfg {String} qtip An Ext QuickTip for the node
23  * @cfg {Boolean} expandable If set to true, the node will always show a plus/minus icon, even when empty
24  * @cfg {String} qtipCfg An Ext QuickTip config for the node (used instead of qtip)
25  * @cfg {Boolean} singleClickExpand True for single click expand on this node
26  * @cfg {Function} uiProvider A UI <b>class</b> to use for this node (defaults to Ext.tree.TreeNodeUI)
27  * @cfg {Boolean} checked True to render a checked checkbox for this node, false to render an unchecked checkbox
28  * (defaults to undefined with no checkbox rendered)
29  * @cfg {Boolean} draggable True to make this node draggable (defaults to false)
30  * @cfg {Boolean} isTarget False to not allow this node to act as a drop target (defaults to true)
31  * @cfg {Boolean} allowChildren False to not allow this node to have child nodes (defaults to true)
32  * @cfg {Boolean} editable False to not allow this node to be edited by an {@link Ext.tree.TreeEditor} (defaults to true)
33  * @constructor
34  * @param {Object/String} attributes The attributes/config for the node or just a string with the text for the node
35  */
36 Ext.tree.TreeNode = function(attributes){
37     attributes = attributes || {};
38     if(Ext.isString(attributes)){
39         attributes = {text: attributes};
40     }
41     this.childrenRendered = false;
42     this.rendered = false;
43     Ext.tree.TreeNode.superclass.constructor.call(this, attributes);
44     this.expanded = attributes.expanded === true;
45     this.isTarget = attributes.isTarget !== false;
46     this.draggable = attributes.draggable !== false && attributes.allowDrag !== false;
47     this.allowChildren = attributes.allowChildren !== false && attributes.allowDrop !== false;
48
49     /**
50      * Read-only. The text for this node. To change it use <code>{@link #setText}</code>.
51      * @type String
52      */
53     this.text = attributes.text;
54     /**
55      * True if this node is disabled.
56      * @type Boolean
57      */
58     this.disabled = attributes.disabled === true;
59     /**
60      * True if this node is hidden.
61      * @type Boolean
62      */
63     this.hidden = attributes.hidden === true;
64
65     this.addEvents(
66         /**
67         * @event textchange
68         * Fires when the text for this node is changed
69         * @param {Node} this This node
70         * @param {String} text The new text
71         * @param {String} oldText The old text
72         */
73         'textchange',
74         /**
75         * @event beforeexpand
76         * Fires before this node is expanded, return false to cancel.
77         * @param {Node} this This node
78         * @param {Boolean} deep
79         * @param {Boolean} anim
80         */
81         'beforeexpand',
82         /**
83         * @event beforecollapse
84         * Fires before this node is collapsed, return false to cancel.
85         * @param {Node} this This node
86         * @param {Boolean} deep
87         * @param {Boolean} anim
88         */
89         'beforecollapse',
90         /**
91         * @event expand
92         * Fires when this node is expanded
93         * @param {Node} this This node
94         */
95         'expand',
96         /**
97         * @event disabledchange
98         * Fires when the disabled status of this node changes
99         * @param {Node} this This node
100         * @param {Boolean} disabled
101         */
102         'disabledchange',
103         /**
104         * @event collapse
105         * Fires when this node is collapsed
106         * @param {Node} this This node
107         */
108         'collapse',
109         /**
110         * @event beforeclick
111         * Fires before click processing. Return false to cancel the default action.
112         * @param {Node} this This node
113         * @param {Ext.EventObject} e The event object
114         */
115         'beforeclick',
116         /**
117         * @event click
118         * Fires when this node is clicked
119         * @param {Node} this This node
120         * @param {Ext.EventObject} e The event object
121         */
122         'click',
123         /**
124         * @event checkchange
125         * Fires when a node with a checkbox's checked property changes
126         * @param {Node} this This node
127         * @param {Boolean} checked
128         */
129         'checkchange',
130         /**
131         * @event beforedblclick
132         * Fires before double click processing. Return false to cancel the default action.
133         * @param {Node} this This node
134         * @param {Ext.EventObject} e The event object
135         */
136         'beforedblclick',
137         /**
138         * @event dblclick
139         * Fires when this node is double clicked
140         * @param {Node} this This node
141         * @param {Ext.EventObject} e The event object
142         */
143         'dblclick',
144         /**
145         * @event contextmenu
146         * Fires when this node is right clicked
147         * @param {Node} this This node
148         * @param {Ext.EventObject} e The event object
149         */
150         'contextmenu',
151         /**
152         * @event beforechildrenrendered
153         * Fires right before the child nodes for this node are rendered
154         * @param {Node} this This node
155         */
156         'beforechildrenrendered'
157     );
158
159     var uiClass = this.attributes.uiProvider || this.defaultUI || Ext.tree.TreeNodeUI;
160
161     /**
162      * Read-only. The UI for this node
163      * @type TreeNodeUI
164      */
165     this.ui = new uiClass(this);
166 };
167 Ext.extend(Ext.tree.TreeNode, Ext.data.Node, {
168     preventHScroll : true,
169     /**
170      * Returns true if this node is expanded
171      * @return {Boolean}
172      */
173     isExpanded : function(){
174         return this.expanded;
175     },
176
177 /**
178  * Returns the UI object for this node.
179  * @return {TreeNodeUI} The object which is providing the user interface for this tree
180  * node. Unless otherwise specified in the {@link #uiProvider}, this will be an instance
181  * of {@link Ext.tree.TreeNodeUI}
182  */
183     getUI : function(){
184         return this.ui;
185     },
186
187     getLoader : function(){
188         var owner;
189         return this.loader || ((owner = this.getOwnerTree()) && owner.loader ? owner.loader : (this.loader = new Ext.tree.TreeLoader()));
190     },
191
192     // private override
193     setFirstChild : function(node){
194         var of = this.firstChild;
195         Ext.tree.TreeNode.superclass.setFirstChild.call(this, node);
196         if(this.childrenRendered && of && node != of){
197             of.renderIndent(true, true);
198         }
199         if(this.rendered){
200             this.renderIndent(true, true);
201         }
202     },
203
204     // private override
205     setLastChild : function(node){
206         var ol = this.lastChild;
207         Ext.tree.TreeNode.superclass.setLastChild.call(this, node);
208         if(this.childrenRendered && ol && node != ol){
209             ol.renderIndent(true, true);
210         }
211         if(this.rendered){
212             this.renderIndent(true, true);
213         }
214     },
215
216     // these methods are overridden to provide lazy rendering support
217     // private override
218     appendChild : function(n){
219         var node, exists;
220         if(!n.render && !Ext.isArray(n)){
221             n = this.getLoader().createNode(n);
222         }else{
223             exists = !n.parentNode;
224         }
225         node = Ext.tree.TreeNode.superclass.appendChild.call(this, n);
226         if(node){
227             this.afterAdd(node, exists);
228         }
229         this.ui.updateExpandIcon();
230         return node;
231     },
232
233     // private override
234     removeChild : function(node, destroy){
235         this.ownerTree.getSelectionModel().unselect(node);
236         Ext.tree.TreeNode.superclass.removeChild.apply(this, arguments);
237         // if it's been rendered remove dom node
238         if(node.ui.rendered){
239             node.ui.remove();
240         }
241         if(this.childNodes.length < 1){
242             this.collapse(false, false);
243         }else{
244             this.ui.updateExpandIcon();
245         }
246         if(!this.firstChild && !this.isHiddenRoot()) {
247             this.childrenRendered = false;
248         }
249         return node;
250     },
251
252     // private override
253     insertBefore : function(node, refNode){
254         var newNode, exists;
255         if(!node.render){
256             node = this.getLoader().createNode(node);
257         } else {
258             exists = Ext.isObject(node.parentNode);
259         }
260         newNode = Ext.tree.TreeNode.superclass.insertBefore.call(this, node, refNode);
261         if(newNode && refNode){
262             this.afterAdd(newNode, exists);
263         }
264         this.ui.updateExpandIcon();
265         return newNode;
266     },
267     
268     // private
269     afterAdd : function(node, exists){
270         if(this.childrenRendered){
271             // bulk render if the node already exists
272             node.render(exists);
273         }else if(exists){
274             // make sure we update the indent
275             node.renderIndent(true, true);
276         }
277     },
278
279     /**
280      * Sets the text for this node
281      * @param {String} text
282      */
283     setText : function(text){
284         var oldText = this.text;
285         this.text = this.attributes.text = text;
286         if(this.rendered){ // event without subscribing
287             this.ui.onTextChange(this, text, oldText);
288         }
289         this.fireEvent('textchange', this, text, oldText);
290     },
291
292     /**
293      * Triggers selection of this node
294      */
295     select : function(){
296         var t = this.getOwnerTree();
297         if(t){
298             t.getSelectionModel().select(this);
299         }
300     },
301
302     /**
303      * Triggers deselection of this node
304      * @param {Boolean} silent (optional) True to stop selection change events from firing.
305      */
306     unselect : function(silent){
307         var t = this.getOwnerTree();
308         if(t){
309             t.getSelectionModel().unselect(this, silent);
310         }
311     },
312
313     /**
314      * Returns true if this node is selected
315      * @return {Boolean}
316      */
317     isSelected : function(){
318         var t = this.getOwnerTree();
319         return t ? t.getSelectionModel().isSelected(this) : false;
320     },
321
322     /**
323      * Expand this node.
324      * @param {Boolean} deep (optional) True to expand all children as well
325      * @param {Boolean} anim (optional) false to cancel the default animation
326      * @param {Function} callback (optional) A callback to be called when
327      * expanding this node completes (does not wait for deep expand to complete).
328      * Called with 1 parameter, this node.
329      * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the callback is executed. Defaults to this TreeNode.
330      */
331     expand : function(deep, anim, callback, scope){
332         if(!this.expanded){
333             if(this.fireEvent('beforeexpand', this, deep, anim) === false){
334                 return;
335             }
336             if(!this.childrenRendered){
337                 this.renderChildren();
338             }
339             this.expanded = true;
340             if(!this.isHiddenRoot() && (this.getOwnerTree().animate && anim !== false) || anim){
341                 this.ui.animExpand(function(){
342                     this.fireEvent('expand', this);
343                     this.runCallback(callback, scope || this, [this]);
344                     if(deep === true){
345                         this.expandChildNodes(true);
346                     }
347                 }.createDelegate(this));
348                 return;
349             }else{
350                 this.ui.expand();
351                 this.fireEvent('expand', this);
352                 this.runCallback(callback, scope || this, [this]);
353             }
354         }else{
355            this.runCallback(callback, scope || this, [this]);
356         }
357         if(deep === true){
358             this.expandChildNodes(true);
359         }
360     },
361
362     runCallback : function(cb, scope, args){
363         if(Ext.isFunction(cb)){
364             cb.apply(scope, args);
365         }
366     },
367
368     isHiddenRoot : function(){
369         return this.isRoot && !this.getOwnerTree().rootVisible;
370     },
371
372     /**
373      * Collapse this node.
374      * @param {Boolean} deep (optional) True to collapse all children as well
375      * @param {Boolean} anim (optional) false to cancel the default animation
376      * @param {Function} callback (optional) A callback to be called when
377      * expanding this node completes (does not wait for deep expand to complete).
378      * Called with 1 parameter, this node.
379      * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the callback is executed. Defaults to this TreeNode.
380      */
381     collapse : function(deep, anim, callback, scope){
382         if(this.expanded && !this.isHiddenRoot()){
383             if(this.fireEvent('beforecollapse', this, deep, anim) === false){
384                 return;
385             }
386             this.expanded = false;
387             if((this.getOwnerTree().animate && anim !== false) || anim){
388                 this.ui.animCollapse(function(){
389                     this.fireEvent('collapse', this);
390                     this.runCallback(callback, scope || this, [this]);
391                     if(deep === true){
392                         this.collapseChildNodes(true);
393                     }
394                 }.createDelegate(this));
395                 return;
396             }else{
397                 this.ui.collapse();
398                 this.fireEvent('collapse', this);
399                 this.runCallback(callback, scope || this, [this]);
400             }
401         }else if(!this.expanded){
402             this.runCallback(callback, scope || this, [this]);
403         }
404         if(deep === true){
405             var cs = this.childNodes;
406             for(var i = 0, len = cs.length; i < len; i++) {
407                 cs[i].collapse(true, false);
408             }
409         }
410     },
411
412     // private
413     delayedExpand : function(delay){
414         if(!this.expandProcId){
415             this.expandProcId = this.expand.defer(delay, this);
416         }
417     },
418
419     // private
420     cancelExpand : function(){
421         if(this.expandProcId){
422             clearTimeout(this.expandProcId);
423         }
424         this.expandProcId = false;
425     },
426
427     /**
428      * Toggles expanded/collapsed state of the node
429      */
430     toggle : function(){
431         if(this.expanded){
432             this.collapse();
433         }else{
434             this.expand();
435         }
436     },
437
438     /**
439      * Ensures all parent nodes are expanded, and if necessary, scrolls
440      * the node into view.
441      * @param {Function} callback (optional) A function to call when the node has been made visible.
442      * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the callback is executed. Defaults to this TreeNode.
443      */
444     ensureVisible : function(callback, scope){
445         var tree = this.getOwnerTree();
446         tree.expandPath(this.parentNode ? this.parentNode.getPath() : this.getPath(), false, function(){
447             var node = tree.getNodeById(this.id);  // Somehow if we don't do this, we lose changes that happened to node in the meantime
448             tree.getTreeEl().scrollChildIntoView(node.ui.anchor);
449             this.runCallback(callback, scope || this, [this]);
450         }.createDelegate(this));
451     },
452
453     /**
454      * Expand all child nodes
455      * @param {Boolean} deep (optional) true if the child nodes should also expand their child nodes
456      */
457     expandChildNodes : function(deep){
458         var cs = this.childNodes;
459         for(var i = 0, len = cs.length; i < len; i++) {
460                 cs[i].expand(deep);
461         }
462     },
463
464     /**
465      * Collapse all child nodes
466      * @param {Boolean} deep (optional) true if the child nodes should also collapse their child nodes
467      */
468     collapseChildNodes : function(deep){
469         var cs = this.childNodes;
470         for(var i = 0, len = cs.length; i < len; i++) {
471                 cs[i].collapse(deep);
472         }
473     },
474
475     /**
476      * Disables this node
477      */
478     disable : function(){
479         this.disabled = true;
480         this.unselect();
481         if(this.rendered && this.ui.onDisableChange){ // event without subscribing
482             this.ui.onDisableChange(this, true);
483         }
484         this.fireEvent('disabledchange', this, true);
485     },
486
487     /**
488      * Enables this node
489      */
490     enable : function(){
491         this.disabled = false;
492         if(this.rendered && this.ui.onDisableChange){ // event without subscribing
493             this.ui.onDisableChange(this, false);
494         }
495         this.fireEvent('disabledchange', this, false);
496     },
497
498     // private
499     renderChildren : function(suppressEvent){
500         if(suppressEvent !== false){
501             this.fireEvent('beforechildrenrendered', this);
502         }
503         var cs = this.childNodes;
504         for(var i = 0, len = cs.length; i < len; i++){
505             cs[i].render(true);
506         }
507         this.childrenRendered = true;
508     },
509
510     // private
511     sort : function(fn, scope){
512         Ext.tree.TreeNode.superclass.sort.apply(this, arguments);
513         if(this.childrenRendered){
514             var cs = this.childNodes;
515             for(var i = 0, len = cs.length; i < len; i++){
516                 cs[i].render(true);
517             }
518         }
519     },
520
521     // private
522     render : function(bulkRender){
523         this.ui.render(bulkRender);
524         if(!this.rendered){
525             // make sure it is registered
526             this.getOwnerTree().registerNode(this);
527             this.rendered = true;
528             if(this.expanded){
529                 this.expanded = false;
530                 this.expand(false, false);
531             }
532         }
533     },
534
535     // private
536     renderIndent : function(deep, refresh){
537         if(refresh){
538             this.ui.childIndent = null;
539         }
540         this.ui.renderIndent();
541         if(deep === true && this.childrenRendered){
542             var cs = this.childNodes;
543             for(var i = 0, len = cs.length; i < len; i++){
544                 cs[i].renderIndent(true, refresh);
545             }
546         }
547     },
548
549     beginUpdate : function(){
550         this.childrenRendered = false;
551     },
552
553     endUpdate : function(){
554         if(this.expanded && this.rendered){
555             this.renderChildren();
556         }
557     },
558
559     destroy : function(){
560         this.unselect(true);
561         Ext.tree.TreeNode.superclass.destroy.call(this);
562         Ext.destroy(this.ui, this.loader);
563         this.ui = this.loader = null;
564     },
565
566     // private
567     onIdChange : function(id){
568         this.ui.onIdChange(id);
569     }
570 });
571
572 Ext.tree.TreePanel.nodeTypes.node = Ext.tree.TreeNode;