Upgrade to ExtJS 3.1.0 - Released 12/16/2009
[extjs.git] / src / widgets / tree / TreeNodeUI.js
1 /*!
2  * Ext JS Library 3.1.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.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 /**\r
373  * Returns the element which encapsulates this node.\r
374  * @return {HtmlElement} The DOM element. The default implementation uses a <code>&lt;li></code>.\r
375  */\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 = Ext.isBoolean(a.checked),\r
439             nel,\r
440             href = a.href ? a.href : Ext.isGecko ? "" : "#",\r
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">',\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         if(bulkRender !== true && n.nextSibling && (nel = n.nextSibling.ui.getEl())){\r
452             this.wrap = Ext.DomHelper.insertHtml("beforeBegin", nel, buf);\r
453         }else{\r
454             this.wrap = Ext.DomHelper.insertHtml("beforeEnd", targetNode, buf);\r
455         }\r
456         \r
457         this.elNode = this.wrap.childNodes[0];\r
458         this.ctNode = this.wrap.childNodes[1];\r
459         var cs = this.elNode.childNodes;\r
460         this.indentNode = cs[0];\r
461         this.ecNode = cs[1];\r
462         this.iconNode = cs[2];\r
463         var index = 3;\r
464         if(cb){\r
465             this.checkbox = cs[3];\r
466             // fix for IE6\r
467             this.checkbox.defaultChecked = this.checkbox.checked;\r
468             index++;\r
469         }\r
470         this.anchor = cs[index];\r
471         this.textNode = cs[index].firstChild;\r
472     },\r
473 \r
474 /**\r
475  * Returns the &lt;a> element that provides focus for the node's UI.\r
476  * @return {HtmlElement} The DOM anchor element.\r
477  */\r
478     getAnchor : function(){\r
479         return this.anchor;\r
480     },\r
481     \r
482 /**\r
483  * Returns the text node.\r
484  * @return {HtmlNode} The DOM text node.\r
485  */\r
486     getTextEl : function(){\r
487         return this.textNode;\r
488     },\r
489     \r
490 /**\r
491  * Returns the icon &lt;img> element.\r
492  * @return {HtmlElement} The DOM image element.\r
493  */\r
494     getIconEl : function(){\r
495         return this.iconNode;\r
496     },\r
497 \r
498 /**\r
499  * Returns the checked status of the node. If the node was rendered with no\r
500  * checkbox, it returns false.\r
501  * @return {Boolean} The checked flag.\r
502  */\r
503     isChecked : function(){\r
504         return this.checkbox ? this.checkbox.checked : false; \r
505     },\r
506 \r
507     // private\r
508     updateExpandIcon : function(){\r
509         if(this.rendered){\r
510             var n = this.node, \r
511                 c1, \r
512                 c2,\r
513                 cls = n.isLast() ? "x-tree-elbow-end" : "x-tree-elbow",\r
514                 hasChild = n.hasChildNodes();\r
515             if(hasChild || n.attributes.expandable){\r
516                 if(n.expanded){\r
517                     cls += "-minus";\r
518                     c1 = "x-tree-node-collapsed";\r
519                     c2 = "x-tree-node-expanded";\r
520                 }else{\r
521                     cls += "-plus";\r
522                     c1 = "x-tree-node-expanded";\r
523                     c2 = "x-tree-node-collapsed";\r
524                 }\r
525                 if(this.wasLeaf){\r
526                     this.removeClass("x-tree-node-leaf");\r
527                     this.wasLeaf = false;\r
528                 }\r
529                 if(this.c1 != c1 || this.c2 != c2){\r
530                     Ext.fly(this.elNode).replaceClass(c1, c2);\r
531                     this.c1 = c1; this.c2 = c2;\r
532                 }\r
533             }else{\r
534                 if(!this.wasLeaf){\r
535                     Ext.fly(this.elNode).replaceClass("x-tree-node-expanded", "x-tree-node-leaf");\r
536                     delete this.c1;\r
537                     delete this.c2;\r
538                     this.wasLeaf = true;\r
539                 }\r
540             }\r
541             var ecc = "x-tree-ec-icon "+cls;\r
542             if(this.ecc != ecc){\r
543                 this.ecNode.className = ecc;\r
544                 this.ecc = ecc;\r
545             }\r
546         }\r
547     },\r
548     \r
549     // private\r
550     onIdChange: function(id){\r
551         if(this.rendered){\r
552             this.elNode.setAttribute('ext:tree-node-id', id);\r
553         }\r
554     },\r
555 \r
556     // private\r
557     getChildIndent : function(){\r
558         if(!this.childIndent){\r
559             var buf = [],\r
560                 p = this.node;\r
561             while(p){\r
562                 if(!p.isRoot || (p.isRoot && p.ownerTree.rootVisible)){\r
563                     if(!p.isLast()) {\r
564                         buf.unshift('<img src="'+this.emptyIcon+'" class="x-tree-elbow-line" />');\r
565                     } else {\r
566                         buf.unshift('<img src="'+this.emptyIcon+'" class="x-tree-icon" />');\r
567                     }\r
568                 }\r
569                 p = p.parentNode;\r
570             }\r
571             this.childIndent = buf.join("");\r
572         }\r
573         return this.childIndent;\r
574     },\r
575 \r
576     // private\r
577     renderIndent : function(){\r
578         if(this.rendered){\r
579             var indent = "",\r
580                 p = this.node.parentNode;\r
581             if(p){\r
582                 indent = p.ui.getChildIndent();\r
583             }\r
584             if(this.indentMarkup != indent){ // don't rerender if not required\r
585                 this.indentNode.innerHTML = indent;\r
586                 this.indentMarkup = indent;\r
587             }\r
588             this.updateExpandIcon();\r
589         }\r
590     },\r
591 \r
592     destroy : function(){\r
593         if(this.elNode){\r
594             Ext.dd.Registry.unregister(this.elNode.id);\r
595         }\r
596         \r
597         Ext.each(['textnode', 'anchor', 'checkbox', 'indentNode', 'ecNode', 'iconNode', 'elNode', 'ctNode', 'wrap', 'holder'], function(el){\r
598             if(this[el]){\r
599                 Ext.fly(this[el]).remove();\r
600                 delete this[el];\r
601             }\r
602         }, this);\r
603         delete this.node;\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 });