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