Upgrade to ExtJS 3.2.1 - Released 04/27/2010
[extjs.git] / src / widgets / tree / TreeNodeUI.js
1 /*!
2  * Ext JS Library 3.2.1
3  * Copyright(c) 2006-2010 Ext JS, Inc.
4  * licensing@extjs.com
5  * http://www.extjs.com/license
6  */
7 /**
8  * @class Ext.tree.TreeNodeUI
9  * This class provides the default UI implementation for Ext TreeNodes.
10  * The TreeNode UI implementation is separate from the
11  * tree implementation, and allows customizing of the appearance of
12  * tree nodes.<br>
13  * <p>
14  * If you are customizing the Tree's user interface, you
15  * may need to extend this class, but you should never need to instantiate this class.<br>
16  * <p>
17  * This class provides access to the user interface components of an Ext TreeNode, through
18  * {@link Ext.tree.TreeNode#getUI}
19  */
20 Ext.tree.TreeNodeUI = function(node){
21     this.node = node;
22     this.rendered = false;
23     this.animating = false;
24     this.wasLeaf = true;
25     this.ecc = 'x-tree-ec-icon x-tree-elbow';
26     this.emptyIcon = Ext.BLANK_IMAGE_URL;
27 };
28
29 Ext.tree.TreeNodeUI.prototype = {
30     // private
31     removeChild : function(node){
32         if(this.rendered){
33             this.ctNode.removeChild(node.ui.getEl());
34         }
35     },
36
37     // private
38     beforeLoad : function(){
39          this.addClass("x-tree-node-loading");
40     },
41
42     // private
43     afterLoad : function(){
44          this.removeClass("x-tree-node-loading");
45     },
46
47     // private
48     onTextChange : function(node, text, oldText){
49         if(this.rendered){
50             this.textNode.innerHTML = text;
51         }
52     },
53
54     // private
55     onDisableChange : function(node, state){
56         this.disabled = state;
57         if (this.checkbox) {
58             this.checkbox.disabled = state;
59         }
60         if(state){
61             this.addClass("x-tree-node-disabled");
62         }else{
63             this.removeClass("x-tree-node-disabled");
64         }
65     },
66
67     // private
68     onSelectedChange : function(state){
69         if(state){
70             this.focus();
71             this.addClass("x-tree-selected");
72         }else{
73             //this.blur();
74             this.removeClass("x-tree-selected");
75         }
76     },
77
78     // private
79     onMove : function(tree, node, oldParent, newParent, index, refNode){
80         this.childIndent = null;
81         if(this.rendered){
82             var targetNode = newParent.ui.getContainer();
83             if(!targetNode){//target not rendered
84                 this.holder = document.createElement("div");
85                 this.holder.appendChild(this.wrap);
86                 return;
87             }
88             var insertBefore = refNode ? refNode.ui.getEl() : null;
89             if(insertBefore){
90                 targetNode.insertBefore(this.wrap, insertBefore);
91             }else{
92                 targetNode.appendChild(this.wrap);
93             }
94             this.node.renderIndent(true, oldParent != newParent);
95         }
96     },
97
98 /**
99  * Adds one or more CSS classes to the node's UI element.
100  * Duplicate classes are automatically filtered out.
101  * @param {String/Array} className The CSS class to add, or an array of classes
102  */
103     addClass : function(cls){
104         if(this.elNode){
105             Ext.fly(this.elNode).addClass(cls);
106         }
107     },
108
109 /**
110  * Removes one or more CSS classes from the node's UI element.
111  * @param {String/Array} className The CSS class to remove, or an array of classes
112  */
113     removeClass : function(cls){
114         if(this.elNode){
115             Ext.fly(this.elNode).removeClass(cls);
116         }
117     },
118
119     // private
120     remove : function(){
121         if(this.rendered){
122             this.holder = document.createElement("div");
123             this.holder.appendChild(this.wrap);
124         }
125     },
126
127     // private
128     fireEvent : function(){
129         return this.node.fireEvent.apply(this.node, arguments);
130     },
131
132     // private
133     initEvents : function(){
134         this.node.on("move", this.onMove, this);
135
136         if(this.node.disabled){
137             this.onDisableChange(this.node, true);
138         }
139         if(this.node.hidden){
140             this.hide();
141         }
142         var ot = this.node.getOwnerTree();
143         var dd = ot.enableDD || ot.enableDrag || ot.enableDrop;
144         if(dd && (!this.node.isRoot || ot.rootVisible)){
145             Ext.dd.Registry.register(this.elNode, {
146                 node: this.node,
147                 handles: this.getDDHandles(),
148                 isHandle: false
149             });
150         }
151     },
152
153     // private
154     getDDHandles : function(){
155         return [this.iconNode, this.textNode, this.elNode];
156     },
157
158 /**
159  * Hides this node.
160  */
161     hide : function(){
162         this.node.hidden = true;
163         if(this.wrap){
164             this.wrap.style.display = "none";
165         }
166     },
167
168 /**
169  * Shows this node.
170  */
171     show : function(){
172         this.node.hidden = false;
173         if(this.wrap){
174             this.wrap.style.display = "";
175         }
176     },
177
178     // private
179     onContextMenu : function(e){
180         if (this.node.hasListener("contextmenu") || this.node.getOwnerTree().hasListener("contextmenu")) {
181             e.preventDefault();
182             this.focus();
183             this.fireEvent("contextmenu", this.node, e);
184         }
185     },
186
187     // private
188     onClick : function(e){
189         if(this.dropping){
190             e.stopEvent();
191             return;
192         }
193         if(this.fireEvent("beforeclick", this.node, e) !== false){
194             var a = e.getTarget('a');
195             if(!this.disabled && this.node.attributes.href && a){
196                 this.fireEvent("click", this.node, e);
197                 return;
198             }else if(a && e.ctrlKey){
199                 e.stopEvent();
200             }
201             e.preventDefault();
202             if(this.disabled){
203                 return;
204             }
205
206             if(this.node.attributes.singleClickExpand && !this.animating && this.node.isExpandable()){
207                 this.node.toggle();
208             }
209
210             this.fireEvent("click", this.node, e);
211         }else{
212             e.stopEvent();
213         }
214     },
215
216     // private
217     onDblClick : function(e){
218         e.preventDefault();
219         if(this.disabled){
220             return;
221         }
222         if(this.fireEvent("beforedblclick", this.node, e) !== false){
223             if(this.checkbox){
224                 this.toggleCheck();
225             }
226             if(!this.animating && this.node.isExpandable()){
227                 this.node.toggle();
228             }
229             this.fireEvent("dblclick", this.node, e);
230         }
231     },
232
233     onOver : function(e){
234         this.addClass('x-tree-node-over');
235     },
236
237     onOut : function(e){
238         this.removeClass('x-tree-node-over');
239     },
240
241     // private
242     onCheckChange : function(){
243         var checked = this.checkbox.checked;
244         // fix for IE6
245         this.checkbox.defaultChecked = checked;
246         this.node.attributes.checked = checked;
247         this.fireEvent('checkchange', this.node, checked);
248     },
249
250     // private
251     ecClick : function(e){
252         if(!this.animating && this.node.isExpandable()){
253             this.node.toggle();
254         }
255     },
256
257     // private
258     startDrop : function(){
259         this.dropping = true;
260     },
261
262     // delayed drop so the click event doesn't get fired on a drop
263     endDrop : function(){
264        setTimeout(function(){
265            this.dropping = false;
266        }.createDelegate(this), 50);
267     },
268
269     // private
270     expand : function(){
271         this.updateExpandIcon();
272         this.ctNode.style.display = "";
273     },
274
275     // private
276     focus : function(){
277         if(!this.node.preventHScroll){
278             try{this.anchor.focus();
279             }catch(e){}
280         }else{
281             try{
282                 var noscroll = this.node.getOwnerTree().getTreeEl().dom;
283                 var l = noscroll.scrollLeft;
284                 this.anchor.focus();
285                 noscroll.scrollLeft = l;
286             }catch(e){}
287         }
288     },
289
290 /**
291  * Sets the checked status of the tree node to the passed value, or, if no value was passed,
292  * toggles the checked status. If the node was rendered with no checkbox, this has no effect.
293  * @param {Boolean} value (optional) The new checked status.
294  */
295     toggleCheck : function(value){
296         var cb = this.checkbox;
297         if(cb){
298             cb.checked = (value === undefined ? !cb.checked : value);
299             this.onCheckChange();
300         }
301     },
302
303     // private
304     blur : function(){
305         try{
306             this.anchor.blur();
307         }catch(e){}
308     },
309
310     // private
311     animExpand : function(callback){
312         var ct = Ext.get(this.ctNode);
313         ct.stopFx();
314         if(!this.node.isExpandable()){
315             this.updateExpandIcon();
316             this.ctNode.style.display = "";
317             Ext.callback(callback);
318             return;
319         }
320         this.animating = true;
321         this.updateExpandIcon();
322
323         ct.slideIn('t', {
324            callback : function(){
325                this.animating = false;
326                Ext.callback(callback);
327             },
328             scope: this,
329             duration: this.node.ownerTree.duration || .25
330         });
331     },
332
333     // private
334     highlight : function(){
335         var tree = this.node.getOwnerTree();
336         Ext.fly(this.wrap).highlight(
337             tree.hlColor || "C3DAF9",
338             {endColor: tree.hlBaseColor}
339         );
340     },
341
342     // private
343     collapse : function(){
344         this.updateExpandIcon();
345         this.ctNode.style.display = "none";
346     },
347
348     // private
349     animCollapse : function(callback){
350         var ct = Ext.get(this.ctNode);
351         ct.enableDisplayMode('block');
352         ct.stopFx();
353
354         this.animating = true;
355         this.updateExpandIcon();
356
357         ct.slideOut('t', {
358             callback : function(){
359                this.animating = false;
360                Ext.callback(callback);
361             },
362             scope: this,
363             duration: this.node.ownerTree.duration || .25
364         });
365     },
366
367     // private
368     getContainer : function(){
369         return this.ctNode;
370     },
371
372 /**
373  * Returns the element which encapsulates this node.
374  * @return {HtmlElement} The DOM element. The default implementation uses a <code>&lt;li></code>.
375  */
376     getEl : function(){
377         return this.wrap;
378     },
379
380     // private
381     appendDDGhost : function(ghostNode){
382         ghostNode.appendChild(this.elNode.cloneNode(true));
383     },
384
385     // private
386     getDDRepairXY : function(){
387         return Ext.lib.Dom.getXY(this.iconNode);
388     },
389
390     // private
391     onRender : function(){
392         this.render();
393     },
394
395     // private
396     render : function(bulkRender){
397         var n = this.node, a = n.attributes;
398         var targetNode = n.parentNode ?
399               n.parentNode.ui.getContainer() : n.ownerTree.innerCt.dom;
400
401         if(!this.rendered){
402             this.rendered = true;
403
404             this.renderElements(n, a, targetNode, bulkRender);
405
406             if(a.qtip){
407                if(this.textNode.setAttributeNS){
408                    this.textNode.setAttributeNS("ext", "qtip", a.qtip);
409                    if(a.qtipTitle){
410                        this.textNode.setAttributeNS("ext", "qtitle", a.qtipTitle);
411                    }
412                }else{
413                    this.textNode.setAttribute("ext:qtip", a.qtip);
414                    if(a.qtipTitle){
415                        this.textNode.setAttribute("ext:qtitle", a.qtipTitle);
416                    }
417                }
418             }else if(a.qtipCfg){
419                 a.qtipCfg.target = Ext.id(this.textNode);
420                 Ext.QuickTips.register(a.qtipCfg);
421             }
422             this.initEvents();
423             if(!this.node.expanded){
424                 this.updateExpandIcon(true);
425             }
426         }else{
427             if(bulkRender === true) {
428                 targetNode.appendChild(this.wrap);
429             }
430         }
431     },
432
433     // private
434     renderElements : function(n, a, targetNode, bulkRender){
435         // add some indent caching, this helps performance when rendering a large tree
436         this.indentMarkup = n.parentNode ? n.parentNode.ui.getChildIndent() : '';
437
438         var cb = Ext.isBoolean(a.checked),
439             nel,
440             href = a.href ? a.href : Ext.isGecko ? "" : "#",
441             buf = ['<li class="x-tree-node"><div ext:tree-node-id="',n.id,'" class="x-tree-node-el x-tree-node-leaf x-unselectable ', a.cls,'" unselectable="on">',
442             '<span class="x-tree-node-indent">',this.indentMarkup,"</span>",
443             '<img src="', this.emptyIcon, '" class="x-tree-ec-icon x-tree-elbow" />',
444             '<img src="', a.icon || this.emptyIcon, '" class="x-tree-node-icon',(a.icon ? " x-tree-node-inline-icon" : ""),(a.iconCls ? " "+a.iconCls : ""),'" unselectable="on" />',
445             cb ? ('<input class="x-tree-node-cb" type="checkbox" ' + (a.checked ? 'checked="checked" />' : '/>')) : '',
446             '<a hidefocus="on" class="x-tree-node-anchor" href="',href,'" tabIndex="1" ',
447              a.hrefTarget ? ' target="'+a.hrefTarget+'"' : "", '><span unselectable="on">',n.text,"</span></a></div>",
448             '<ul class="x-tree-node-ct" style="display:none;"></ul>',
449             "</li>"].join('');
450
451         if(bulkRender !== true && n.nextSibling && (nel = n.nextSibling.ui.getEl())){
452             this.wrap = Ext.DomHelper.insertHtml("beforeBegin", nel, buf);
453         }else{
454             this.wrap = Ext.DomHelper.insertHtml("beforeEnd", targetNode, buf);
455         }
456
457         this.elNode = this.wrap.childNodes[0];
458         this.ctNode = this.wrap.childNodes[1];
459         var cs = this.elNode.childNodes;
460         this.indentNode = cs[0];
461         this.ecNode = cs[1];
462         this.iconNode = cs[2];
463         var index = 3;
464         if(cb){
465             this.checkbox = cs[3];
466             // fix for IE6
467             this.checkbox.defaultChecked = this.checkbox.checked;
468             index++;
469         }
470         this.anchor = cs[index];
471         this.textNode = cs[index].firstChild;
472     },
473
474 /**
475  * Returns the &lt;a> element that provides focus for the node's UI.
476  * @return {HtmlElement} The DOM anchor element.
477  */
478     getAnchor : function(){
479         return this.anchor;
480     },
481
482 /**
483  * Returns the text node.
484  * @return {HtmlNode} The DOM text node.
485  */
486     getTextEl : function(){
487         return this.textNode;
488     },
489
490 /**
491  * Returns the icon &lt;img> element.
492  * @return {HtmlElement} The DOM image element.
493  */
494     getIconEl : function(){
495         return this.iconNode;
496     },
497
498 /**
499  * Returns the checked status of the node. If the node was rendered with no
500  * checkbox, it returns false.
501  * @return {Boolean} The checked flag.
502  */
503     isChecked : function(){
504         return this.checkbox ? this.checkbox.checked : false;
505     },
506
507     // private
508     updateExpandIcon : function(){
509         if(this.rendered){
510             var n = this.node,
511                 c1,
512                 c2,
513                 cls = n.isLast() ? "x-tree-elbow-end" : "x-tree-elbow",
514                 hasChild = n.hasChildNodes();
515             if(hasChild || n.attributes.expandable){
516                 if(n.expanded){
517                     cls += "-minus";
518                     c1 = "x-tree-node-collapsed";
519                     c2 = "x-tree-node-expanded";
520                 }else{
521                     cls += "-plus";
522                     c1 = "x-tree-node-expanded";
523                     c2 = "x-tree-node-collapsed";
524                 }
525                 if(this.wasLeaf){
526                     this.removeClass("x-tree-node-leaf");
527                     this.wasLeaf = false;
528                 }
529                 if(this.c1 != c1 || this.c2 != c2){
530                     Ext.fly(this.elNode).replaceClass(c1, c2);
531                     this.c1 = c1; this.c2 = c2;
532                 }
533             }else{
534                 if(!this.wasLeaf){
535                     Ext.fly(this.elNode).replaceClass("x-tree-node-expanded", "x-tree-node-collapsed");
536                     delete this.c1;
537                     delete this.c2;
538                     this.wasLeaf = true;
539                 }
540             }
541             var ecc = "x-tree-ec-icon "+cls;
542             if(this.ecc != ecc){
543                 this.ecNode.className = ecc;
544                 this.ecc = ecc;
545             }
546         }
547     },
548
549     // private
550     onIdChange: function(id){
551         if(this.rendered){
552             this.elNode.setAttribute('ext:tree-node-id', id);
553         }
554     },
555
556     // private
557     getChildIndent : function(){
558         if(!this.childIndent){
559             var buf = [],
560                 p = this.node;
561             while(p){
562                 if(!p.isRoot || (p.isRoot && p.ownerTree.rootVisible)){
563                     if(!p.isLast()) {
564                         buf.unshift('<img src="'+this.emptyIcon+'" class="x-tree-elbow-line" />');
565                     } else {
566                         buf.unshift('<img src="'+this.emptyIcon+'" class="x-tree-icon" />');
567                     }
568                 }
569                 p = p.parentNode;
570             }
571             this.childIndent = buf.join("");
572         }
573         return this.childIndent;
574     },
575
576     // private
577     renderIndent : function(){
578         if(this.rendered){
579             var indent = "",
580                 p = this.node.parentNode;
581             if(p){
582                 indent = p.ui.getChildIndent();
583             }
584             if(this.indentMarkup != indent){ // don't rerender if not required
585                 this.indentNode.innerHTML = indent;
586                 this.indentMarkup = indent;
587             }
588             this.updateExpandIcon();
589         }
590     },
591
592     destroy : function(){
593         if(this.elNode){
594             Ext.dd.Registry.unregister(this.elNode.id);
595         }
596
597         Ext.each(['textnode', 'anchor', 'checkbox', 'indentNode', 'ecNode', 'iconNode', 'elNode', 'ctNode', 'wrap', 'holder'], function(el){
598             if(this[el]){
599                 Ext.fly(this[el]).remove();
600                 delete this[el];
601             }
602         }, this);
603         delete this.node;
604     }
605 };
606
607 /**
608  * @class Ext.tree.RootTreeNodeUI
609  * This class provides the default UI implementation for <b>root</b> Ext TreeNodes.
610  * The RootTreeNode UI implementation allows customizing the appearance of the root tree node.<br>
611  * <p>
612  * If you are customizing the Tree's user interface, you
613  * may need to extend this class, but you should never need to instantiate this class.<br>
614  */
615 Ext.tree.RootTreeNodeUI = Ext.extend(Ext.tree.TreeNodeUI, {
616     // private
617     render : function(){
618         if(!this.rendered){
619             var targetNode = this.node.ownerTree.innerCt.dom;
620             this.node.expanded = true;
621             targetNode.innerHTML = '<div class="x-tree-root-node"></div>';
622             this.wrap = this.ctNode = targetNode.firstChild;
623         }
624     },
625     collapse : Ext.emptyFn,
626     expand : Ext.emptyFn
627 });