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