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