Upgrade to ExtJS 3.0.0 - Released 07/06/2009
[extjs.git] / src / data / Tree.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.data.Tree\r
9  * @extends Ext.util.Observable\r
10  * Represents a tree data structure and bubbles all the events for its nodes. The nodes\r
11  * in the tree have most standard DOM functionality.\r
12  * @constructor\r
13  * @param {Node} root (optional) The root node\r
14  */\r
15 Ext.data.Tree = function(root){\r
16    this.nodeHash = {};\r
17    /**\r
18     * The root node for this tree\r
19     * @type Node\r
20     */\r
21    this.root = null;\r
22    if(root){\r
23        this.setRootNode(root);\r
24    }\r
25    this.addEvents(\r
26        /**\r
27         * @event append\r
28         * Fires when a new child node is appended to a node in this tree.\r
29         * @param {Tree} tree The owner tree\r
30         * @param {Node} parent The parent node\r
31         * @param {Node} node The newly appended node\r
32         * @param {Number} index The index of the newly appended node\r
33         */\r
34        "append",\r
35        /**\r
36         * @event remove\r
37         * Fires when a child node is removed from a node in this tree.\r
38         * @param {Tree} tree The owner tree\r
39         * @param {Node} parent The parent node\r
40         * @param {Node} node The child node removed\r
41         */\r
42        "remove",\r
43        /**\r
44         * @event move\r
45         * Fires when a node is moved to a new location in the tree\r
46         * @param {Tree} tree The owner tree\r
47         * @param {Node} node The node moved\r
48         * @param {Node} oldParent The old parent of this node\r
49         * @param {Node} newParent The new parent of this node\r
50         * @param {Number} index The index it was moved to\r
51         */\r
52        "move",\r
53        /**\r
54         * @event insert\r
55         * Fires when a new child node is inserted in a node in this tree.\r
56         * @param {Tree} tree The owner tree\r
57         * @param {Node} parent The parent node\r
58         * @param {Node} node The child node inserted\r
59         * @param {Node} refNode The child node the node was inserted before\r
60         */\r
61        "insert",\r
62        /**\r
63         * @event beforeappend\r
64         * Fires before a new child is appended to a node in this tree, return false to cancel the append.\r
65         * @param {Tree} tree The owner tree\r
66         * @param {Node} parent The parent node\r
67         * @param {Node} node The child node to be appended\r
68         */\r
69        "beforeappend",\r
70        /**\r
71         * @event beforeremove\r
72         * Fires before a child is removed from a node in this tree, return false to cancel the remove.\r
73         * @param {Tree} tree The owner tree\r
74         * @param {Node} parent The parent node\r
75         * @param {Node} node The child node to be removed\r
76         */\r
77        "beforeremove",\r
78        /**\r
79         * @event beforemove\r
80         * Fires before a node is moved to a new location in the tree. Return false to cancel the move.\r
81         * @param {Tree} tree The owner tree\r
82         * @param {Node} node The node being moved\r
83         * @param {Node} oldParent The parent of the node\r
84         * @param {Node} newParent The new parent the node is moving to\r
85         * @param {Number} index The index it is being moved to\r
86         */\r
87        "beforemove",\r
88        /**\r
89         * @event beforeinsert\r
90         * Fires before a new child is inserted in a node in this tree, return false to cancel the insert.\r
91         * @param {Tree} tree The owner tree\r
92         * @param {Node} parent The parent node\r
93         * @param {Node} node The child node to be inserted\r
94         * @param {Node} refNode The child node the node is being inserted before\r
95         */\r
96        "beforeinsert"\r
97    );\r
98 \r
99     Ext.data.Tree.superclass.constructor.call(this);\r
100 };\r
101 \r
102 Ext.extend(Ext.data.Tree, Ext.util.Observable, {\r
103     /**\r
104      * @cfg {String} pathSeparator\r
105      * The token used to separate paths in node ids (defaults to '/').\r
106      */\r
107     pathSeparator: "/",\r
108 \r
109     // private\r
110     proxyNodeEvent : function(){\r
111         return this.fireEvent.apply(this, arguments);\r
112     },\r
113 \r
114     /**\r
115      * Returns the root node for this tree.\r
116      * @return {Node}\r
117      */\r
118     getRootNode : function(){\r
119         return this.root;\r
120     },\r
121 \r
122     /**\r
123      * Sets the root node for this tree.\r
124      * @param {Node} node\r
125      * @return {Node}\r
126      */\r
127     setRootNode : function(node){\r
128         this.root = node;\r
129         node.ownerTree = this;\r
130         node.isRoot = true;\r
131         this.registerNode(node);\r
132         return node;\r
133     },\r
134 \r
135     /**\r
136      * Gets a node in this tree by its id.\r
137      * @param {String} id\r
138      * @return {Node}\r
139      */\r
140     getNodeById : function(id){\r
141         return this.nodeHash[id];\r
142     },\r
143 \r
144     // private\r
145     registerNode : function(node){\r
146         this.nodeHash[node.id] = node;\r
147     },\r
148 \r
149     // private\r
150     unregisterNode : function(node){\r
151         delete this.nodeHash[node.id];\r
152     },\r
153 \r
154     toString : function(){\r
155         return "[Tree"+(this.id?" "+this.id:"")+"]";\r
156     }\r
157 });\r
158 \r
159 /**\r
160  * @class Ext.data.Node\r
161  * @extends Ext.util.Observable\r
162  * @cfg {Boolean} leaf true if this node is a leaf and does not have children\r
163  * @cfg {String} id The id for this node. If one is not specified, one is generated.\r
164  * @constructor\r
165  * @param {Object} attributes The attributes/config for the node\r
166  */\r
167 Ext.data.Node = function(attributes){\r
168     /**\r
169      * The attributes supplied for the node. You can use this property to access any custom attributes you supplied.\r
170      * @type {Object}\r
171      */\r
172     this.attributes = attributes || {};\r
173     this.leaf = this.attributes.leaf;\r
174     /**\r
175      * The node id. @type String\r
176      */\r
177     this.id = this.attributes.id;\r
178     if(!this.id){\r
179         this.id = Ext.id(null, "xnode-");\r
180         this.attributes.id = this.id;\r
181     }\r
182     /**\r
183      * All child nodes of this node. @type Array\r
184      */\r
185     this.childNodes = [];\r
186     if(!this.childNodes.indexOf){ // indexOf is a must\r
187         this.childNodes.indexOf = function(o){\r
188             for(var i = 0, len = this.length; i < len; i++){\r
189                 if(this[i] == o){\r
190                     return i;\r
191                 }\r
192             }\r
193             return -1;\r
194         };\r
195     }\r
196     /**\r
197      * The parent node for this node. @type Node\r
198      */\r
199     this.parentNode = null;\r
200     /**\r
201      * The first direct child node of this node, or null if this node has no child nodes. @type Node\r
202      */\r
203     this.firstChild = null;\r
204     /**\r
205      * The last direct child node of this node, or null if this node has no child nodes. @type Node\r
206      */\r
207     this.lastChild = null;\r
208     /**\r
209      * The node immediately preceding this node in the tree, or null if there is no sibling node. @type Node\r
210      */\r
211     this.previousSibling = null;\r
212     /**\r
213      * The node immediately following this node in the tree, or null if there is no sibling node. @type Node\r
214      */\r
215     this.nextSibling = null;\r
216 \r
217     this.addEvents({\r
218        /**\r
219         * @event append\r
220         * Fires when a new child node is appended\r
221         * @param {Tree} tree The owner tree\r
222         * @param {Node} this This node\r
223         * @param {Node} node The newly appended node\r
224         * @param {Number} index The index of the newly appended node\r
225         */\r
226        "append" : true,\r
227        /**\r
228         * @event remove\r
229         * Fires when a child node is removed\r
230         * @param {Tree} tree The owner tree\r
231         * @param {Node} this This node\r
232         * @param {Node} node The removed node\r
233         */\r
234        "remove" : true,\r
235        /**\r
236         * @event move\r
237         * Fires when this node is moved to a new location in the tree\r
238         * @param {Tree} tree The owner tree\r
239         * @param {Node} this This node\r
240         * @param {Node} oldParent The old parent of this node\r
241         * @param {Node} newParent The new parent of this node\r
242         * @param {Number} index The index it was moved to\r
243         */\r
244        "move" : true,\r
245        /**\r
246         * @event insert\r
247         * Fires when a new child node is inserted.\r
248         * @param {Tree} tree The owner tree\r
249         * @param {Node} this This node\r
250         * @param {Node} node The child node inserted\r
251         * @param {Node} refNode The child node the node was inserted before\r
252         */\r
253        "insert" : true,\r
254        /**\r
255         * @event beforeappend\r
256         * Fires before a new child is appended, return false to cancel the append.\r
257         * @param {Tree} tree The owner tree\r
258         * @param {Node} this This node\r
259         * @param {Node} node The child node to be appended\r
260         */\r
261        "beforeappend" : true,\r
262        /**\r
263         * @event beforeremove\r
264         * Fires before a child is removed, return false to cancel the remove.\r
265         * @param {Tree} tree The owner tree\r
266         * @param {Node} this This node\r
267         * @param {Node} node The child node to be removed\r
268         */\r
269        "beforeremove" : true,\r
270        /**\r
271         * @event beforemove\r
272         * Fires before this node is moved to a new location in the tree. Return false to cancel the move.\r
273         * @param {Tree} tree The owner tree\r
274         * @param {Node} this This node\r
275         * @param {Node} oldParent The parent of this node\r
276         * @param {Node} newParent The new parent this node is moving to\r
277         * @param {Number} index The index it is being moved to\r
278         */\r
279        "beforemove" : true,\r
280        /**\r
281         * @event beforeinsert\r
282         * Fires before a new child is inserted, return false to cancel the insert.\r
283         * @param {Tree} tree The owner tree\r
284         * @param {Node} this This node\r
285         * @param {Node} node The child node to be inserted\r
286         * @param {Node} refNode The child node the node is being inserted before\r
287         */\r
288        "beforeinsert" : true\r
289    });\r
290     this.listeners = this.attributes.listeners;\r
291     Ext.data.Node.superclass.constructor.call(this);\r
292 };\r
293 \r
294 Ext.extend(Ext.data.Node, Ext.util.Observable, {\r
295     // private\r
296     fireEvent : function(evtName){\r
297         // first do standard event for this node\r
298         if(Ext.data.Node.superclass.fireEvent.apply(this, arguments) === false){\r
299             return false;\r
300         }\r
301         // then bubble it up to the tree if the event wasn't cancelled\r
302         var ot = this.getOwnerTree();\r
303         if(ot){\r
304             if(ot.proxyNodeEvent.apply(ot, arguments) === false){\r
305                 return false;\r
306             }\r
307         }\r
308         return true;\r
309     },\r
310 \r
311     /**\r
312      * Returns true if this node is a leaf\r
313      * @return {Boolean}\r
314      */\r
315     isLeaf : function(){\r
316         return this.leaf === true;\r
317     },\r
318 \r
319     // private\r
320     setFirstChild : function(node){\r
321         this.firstChild = node;\r
322     },\r
323 \r
324     //private\r
325     setLastChild : function(node){\r
326         this.lastChild = node;\r
327     },\r
328 \r
329 \r
330     /**\r
331      * Returns true if this node is the last child of its parent\r
332      * @return {Boolean}\r
333      */\r
334     isLast : function(){\r
335        return (!this.parentNode ? true : this.parentNode.lastChild == this);\r
336     },\r
337 \r
338     /**\r
339      * Returns true if this node is the first child of its parent\r
340      * @return {Boolean}\r
341      */\r
342     isFirst : function(){\r
343        return (!this.parentNode ? true : this.parentNode.firstChild == this);\r
344     },\r
345 \r
346     /**\r
347      * Returns true if this node has one or more child nodes, else false.\r
348      * @return {Boolean}\r
349      */\r
350     hasChildNodes : function(){\r
351         return !this.isLeaf() && this.childNodes.length > 0;\r
352     },\r
353     \r
354     /**\r
355      * Returns true if this node has one or more child nodes, or if the <tt>expandable</tt>\r
356      * node attribute is explicitly specified as true (see {@link #attributes}), otherwise returns false.\r
357      * @return {Boolean}\r
358      */\r
359     isExpandable : function(){\r
360         return this.attributes.expandable || this.hasChildNodes();\r
361     },\r
362 \r
363     /**\r
364      * Insert node(s) as the last child node of this node.\r
365      * @param {Node/Array} node The node or Array of nodes to append\r
366      * @return {Node} The appended node if single append, or null if an array was passed\r
367      */\r
368     appendChild : function(node){\r
369         var multi = false;\r
370         if(Ext.isArray(node)){\r
371             multi = node;\r
372         }else if(arguments.length > 1){\r
373             multi = arguments;\r
374         }\r
375         // if passed an array or multiple args do them one by one\r
376         if(multi){\r
377             for(var i = 0, len = multi.length; i < len; i++) {\r
378                 this.appendChild(multi[i]);\r
379             }\r
380         }else{\r
381             if(this.fireEvent("beforeappend", this.ownerTree, this, node) === false){\r
382                 return false;\r
383             }\r
384             var index = this.childNodes.length;\r
385             var oldParent = node.parentNode;\r
386             // it's a move, make sure we move it cleanly\r
387             if(oldParent){\r
388                 if(node.fireEvent("beforemove", node.getOwnerTree(), node, oldParent, this, index) === false){\r
389                     return false;\r
390                 }\r
391                 oldParent.removeChild(node);\r
392             }\r
393             index = this.childNodes.length;\r
394             if(index === 0){\r
395                 this.setFirstChild(node);\r
396             }\r
397             this.childNodes.push(node);\r
398             node.parentNode = this;\r
399             var ps = this.childNodes[index-1];\r
400             if(ps){\r
401                 node.previousSibling = ps;\r
402                 ps.nextSibling = node;\r
403             }else{\r
404                 node.previousSibling = null;\r
405             }\r
406             node.nextSibling = null;\r
407             this.setLastChild(node);\r
408             node.setOwnerTree(this.getOwnerTree());\r
409             this.fireEvent("append", this.ownerTree, this, node, index);\r
410             if(oldParent){\r
411                 node.fireEvent("move", this.ownerTree, node, oldParent, this, index);\r
412             }\r
413             return node;\r
414         }\r
415     },\r
416 \r
417     /**\r
418      * Removes a child node from this node.\r
419      * @param {Node} node The node to remove\r
420      * @return {Node} The removed node\r
421      */\r
422     removeChild : function(node){\r
423         var index = this.childNodes.indexOf(node);\r
424         if(index == -1){\r
425             return false;\r
426         }\r
427         if(this.fireEvent("beforeremove", this.ownerTree, this, node) === false){\r
428             return false;\r
429         }\r
430 \r
431         // remove it from childNodes collection\r
432         this.childNodes.splice(index, 1);\r
433 \r
434         // update siblings\r
435         if(node.previousSibling){\r
436             node.previousSibling.nextSibling = node.nextSibling;\r
437         }\r
438         if(node.nextSibling){\r
439             node.nextSibling.previousSibling = node.previousSibling;\r
440         }\r
441 \r
442         // update child refs\r
443         if(this.firstChild == node){\r
444             this.setFirstChild(node.nextSibling);\r
445         }\r
446         if(this.lastChild == node){\r
447             this.setLastChild(node.previousSibling);\r
448         }\r
449 \r
450         node.setOwnerTree(null);\r
451         // clear any references from the node\r
452         node.parentNode = null;\r
453         node.previousSibling = null;\r
454         node.nextSibling = null;\r
455         this.fireEvent("remove", this.ownerTree, this, node);\r
456         return node;\r
457     },\r
458 \r
459     /**\r
460      * Inserts the first node before the second node in this nodes childNodes collection.\r
461      * @param {Node} node The node to insert\r
462      * @param {Node} refNode The node to insert before (if null the node is appended)\r
463      * @return {Node} The inserted node\r
464      */\r
465     insertBefore : function(node, refNode){\r
466         if(!refNode){ // like standard Dom, refNode can be null for append\r
467             return this.appendChild(node);\r
468         }\r
469         // nothing to do\r
470         if(node == refNode){\r
471             return false;\r
472         }\r
473 \r
474         if(this.fireEvent("beforeinsert", this.ownerTree, this, node, refNode) === false){\r
475             return false;\r
476         }\r
477         var index = this.childNodes.indexOf(refNode);\r
478         var oldParent = node.parentNode;\r
479         var refIndex = index;\r
480 \r
481         // when moving internally, indexes will change after remove\r
482         if(oldParent == this && this.childNodes.indexOf(node) < index){\r
483             refIndex--;\r
484         }\r
485 \r
486         // it's a move, make sure we move it cleanly\r
487         if(oldParent){\r
488             if(node.fireEvent("beforemove", node.getOwnerTree(), node, oldParent, this, index, refNode) === false){\r
489                 return false;\r
490             }\r
491             oldParent.removeChild(node);\r
492         }\r
493         if(refIndex === 0){\r
494             this.setFirstChild(node);\r
495         }\r
496         this.childNodes.splice(refIndex, 0, node);\r
497         node.parentNode = this;\r
498         var ps = this.childNodes[refIndex-1];\r
499         if(ps){\r
500             node.previousSibling = ps;\r
501             ps.nextSibling = node;\r
502         }else{\r
503             node.previousSibling = null;\r
504         }\r
505         node.nextSibling = refNode;\r
506         refNode.previousSibling = node;\r
507         node.setOwnerTree(this.getOwnerTree());\r
508         this.fireEvent("insert", this.ownerTree, this, node, refNode);\r
509         if(oldParent){\r
510             node.fireEvent("move", this.ownerTree, node, oldParent, this, refIndex, refNode);\r
511         }\r
512         return node;\r
513     },\r
514 \r
515     /**\r
516      * Removes this node from its parent\r
517      * @return {Node} this\r
518      */\r
519     remove : function(){\r
520         this.parentNode.removeChild(this);\r
521         return this;\r
522     },\r
523 \r
524     /**\r
525      * Returns the child node at the specified index.\r
526      * @param {Number} index\r
527      * @return {Node}\r
528      */\r
529     item : function(index){\r
530         return this.childNodes[index];\r
531     },\r
532 \r
533     /**\r
534      * Replaces one child node in this node with another.\r
535      * @param {Node} newChild The replacement node\r
536      * @param {Node} oldChild The node to replace\r
537      * @return {Node} The replaced node\r
538      */\r
539     replaceChild : function(newChild, oldChild){\r
540         var s = oldChild ? oldChild.nextSibling : null;\r
541         this.removeChild(oldChild);\r
542         this.insertBefore(newChild, s);\r
543         return oldChild;\r
544     },\r
545 \r
546     /**\r
547      * Returns the index of a child node\r
548      * @param {Node} node\r
549      * @return {Number} The index of the node or -1 if it was not found\r
550      */\r
551     indexOf : function(child){\r
552         return this.childNodes.indexOf(child);\r
553     },\r
554 \r
555     /**\r
556      * Returns the tree this node is in.\r
557      * @return {Tree}\r
558      */\r
559     getOwnerTree : function(){\r
560         // if it doesn't have one, look for one\r
561         if(!this.ownerTree){\r
562             var p = this;\r
563             while(p){\r
564                 if(p.ownerTree){\r
565                     this.ownerTree = p.ownerTree;\r
566                     break;\r
567                 }\r
568                 p = p.parentNode;\r
569             }\r
570         }\r
571         return this.ownerTree;\r
572     },\r
573 \r
574     /**\r
575      * Returns depth of this node (the root node has a depth of 0)\r
576      * @return {Number}\r
577      */\r
578     getDepth : function(){\r
579         var depth = 0;\r
580         var p = this;\r
581         while(p.parentNode){\r
582             ++depth;\r
583             p = p.parentNode;\r
584         }\r
585         return depth;\r
586     },\r
587 \r
588     // private\r
589     setOwnerTree : function(tree){\r
590         // if it is a move, we need to update everyone\r
591         if(tree != this.ownerTree){\r
592             if(this.ownerTree){\r
593                 this.ownerTree.unregisterNode(this);\r
594             }\r
595             this.ownerTree = tree;\r
596             var cs = this.childNodes;\r
597             for(var i = 0, len = cs.length; i < len; i++) {\r
598                 cs[i].setOwnerTree(tree);\r
599             }\r
600             if(tree){\r
601                 tree.registerNode(this);\r
602             }\r
603         }\r
604     },\r
605     \r
606     /**\r
607      * Changes the id of this node.\r
608      * @param {String} id The new id for the node.\r
609      */\r
610     setId: function(id){\r
611         if(id !== this.id){\r
612             var t = this.ownerTree;\r
613             if(t){\r
614                 t.unregisterNode(this);\r
615             }\r
616             this.id = id;\r
617             if(t){\r
618                 t.registerNode(this);\r
619             }\r
620             this.onIdChange(id);\r
621         }\r
622     },\r
623     \r
624     // private\r
625     onIdChange: Ext.emptyFn,\r
626 \r
627     /**\r
628      * Returns the path for this node. The path can be used to expand or select this node programmatically.\r
629      * @param {String} attr (optional) The attr to use for the path (defaults to the node's id)\r
630      * @return {String} The path\r
631      */\r
632     getPath : function(attr){\r
633         attr = attr || "id";\r
634         var p = this.parentNode;\r
635         var b = [this.attributes[attr]];\r
636         while(p){\r
637             b.unshift(p.attributes[attr]);\r
638             p = p.parentNode;\r
639         }\r
640         var sep = this.getOwnerTree().pathSeparator;\r
641         return sep + b.join(sep);\r
642     },\r
643 \r
644     /**\r
645      * Bubbles up the tree from this node, calling the specified function with each node. The scope (<i>this</i>) of\r
646      * function call will be the scope provided or the current node. The arguments to the function\r
647      * will be the args provided or the current node. If the function returns false at any point,\r
648      * the bubble is stopped.\r
649      * @param {Function} fn The function to call\r
650      * @param {Object} scope (optional) The scope of the function (defaults to current node)\r
651      * @param {Array} args (optional) The args to call the function with (default to passing the current node)\r
652      */\r
653     bubble : function(fn, scope, args){\r
654         var p = this;\r
655         while(p){\r
656             if(fn.apply(scope || p, args || [p]) === false){\r
657                 break;\r
658             }\r
659             p = p.parentNode;\r
660         }\r
661     },\r
662 \r
663     /**\r
664      * Cascades down the tree from this node, calling the specified function with each node. The scope (<i>this</i>) of\r
665      * function call will be the scope provided or the current node. The arguments to the function\r
666      * will be the args provided or the current node. If the function returns false at any point,\r
667      * the cascade is stopped on that branch.\r
668      * @param {Function} fn The function to call\r
669      * @param {Object} scope (optional) The scope of the function (defaults to current node)\r
670      * @param {Array} args (optional) The args to call the function with (default to passing the current node)\r
671      */\r
672     cascade : function(fn, scope, args){\r
673         if(fn.apply(scope || this, args || [this]) !== false){\r
674             var cs = this.childNodes;\r
675             for(var i = 0, len = cs.length; i < len; i++) {\r
676                 cs[i].cascade(fn, scope, args);\r
677             }\r
678         }\r
679     },\r
680 \r
681     /**\r
682      * Interates the child nodes of this node, calling the specified function with each node. The scope (<i>this</i>) of\r
683      * function call will be the scope provided or the current node. The arguments to the function\r
684      * will be the args provided or the current node. If the function returns false at any point,\r
685      * the iteration stops.\r
686      * @param {Function} fn The function to call\r
687      * @param {Object} scope (optional) The scope of the function (defaults to current node)\r
688      * @param {Array} args (optional) The args to call the function with (default to passing the current node)\r
689      */\r
690     eachChild : function(fn, scope, args){\r
691         var cs = this.childNodes;\r
692         for(var i = 0, len = cs.length; i < len; i++) {\r
693                 if(fn.apply(scope || this, args || [cs[i]]) === false){\r
694                     break;\r
695                 }\r
696         }\r
697     },\r
698 \r
699     /**\r
700      * Finds the first child that has the attribute with the specified value.\r
701      * @param {String} attribute The attribute name\r
702      * @param {Mixed} value The value to search for\r
703      * @return {Node} The found child or null if none was found\r
704      */\r
705     findChild : function(attribute, value){\r
706         var cs = this.childNodes;\r
707         for(var i = 0, len = cs.length; i < len; i++) {\r
708                 if(cs[i].attributes[attribute] == value){\r
709                     return cs[i];\r
710                 }\r
711         }\r
712         return null;\r
713     },\r
714 \r
715     /**\r
716      * Finds the first child by a custom function. The child matches if the function passed\r
717      * returns true.\r
718      * @param {Function} fn\r
719      * @param {Object} scope (optional)\r
720      * @return {Node} The found child or null if none was found\r
721      */\r
722     findChildBy : function(fn, scope){\r
723         var cs = this.childNodes;\r
724         for(var i = 0, len = cs.length; i < len; i++) {\r
725                 if(fn.call(scope||cs[i], cs[i]) === true){\r
726                     return cs[i];\r
727                 }\r
728         }\r
729         return null;\r
730     },\r
731 \r
732     /**\r
733      * Sorts this nodes children using the supplied sort function\r
734      * @param {Function} fn\r
735      * @param {Object} scope (optional)\r
736      */\r
737     sort : function(fn, scope){\r
738         var cs = this.childNodes;\r
739         var len = cs.length;\r
740         if(len > 0){\r
741             var sortFn = scope ? function(){fn.apply(scope, arguments);} : fn;\r
742             cs.sort(sortFn);\r
743             for(var i = 0; i < len; i++){\r
744                 var n = cs[i];\r
745                 n.previousSibling = cs[i-1];\r
746                 n.nextSibling = cs[i+1];\r
747                 if(i === 0){\r
748                     this.setFirstChild(n);\r
749                 }\r
750                 if(i == len-1){\r
751                     this.setLastChild(n);\r
752                 }\r
753             }\r
754         }\r
755     },\r
756 \r
757     /**\r
758      * Returns true if this node is an ancestor (at any point) of the passed node.\r
759      * @param {Node} node\r
760      * @return {Boolean}\r
761      */\r
762     contains : function(node){\r
763         return node.isAncestor(this);\r
764     },\r
765 \r
766     /**\r
767      * Returns true if the passed node is an ancestor (at any point) of this node.\r
768      * @param {Node} node\r
769      * @return {Boolean}\r
770      */\r
771     isAncestor : function(node){\r
772         var p = this.parentNode;\r
773         while(p){\r
774             if(p == node){\r
775                 return true;\r
776             }\r
777             p = p.parentNode;\r
778         }\r
779         return false;\r
780     },\r
781 \r
782     toString : function(){\r
783         return "[Node"+(this.id?" "+this.id:"")+"]";\r
784     }\r
785 });