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