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