Upgrade to ExtJS 3.1.1 - Released 02/08/2010
[extjs.git] / src / data / Tree.js
1 /*!
2  * Ext JS Library 3.1.1
3  * Copyright(c) 2006-2010 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      * @param {Boolean} destroy <tt>true</tt> to destroy the node upon removal. Defaults to <tt>false</tt>.\r
421      * @return {Node} The removed node\r
422      */\r
423     removeChild : function(node, destroy){\r
424         var index = this.childNodes.indexOf(node);\r
425         if(index == -1){\r
426             return false;\r
427         }\r
428         if(this.fireEvent("beforeremove", this.ownerTree, this, node) === false){\r
429             return false;\r
430         }\r
431 \r
432         // remove it from childNodes collection\r
433         this.childNodes.splice(index, 1);\r
434 \r
435         // update siblings\r
436         if(node.previousSibling){\r
437             node.previousSibling.nextSibling = node.nextSibling;\r
438         }\r
439         if(node.nextSibling){\r
440             node.nextSibling.previousSibling = node.previousSibling;\r
441         }\r
442 \r
443         // update child refs\r
444         if(this.firstChild == node){\r
445             this.setFirstChild(node.nextSibling);\r
446         }\r
447         if(this.lastChild == node){\r
448             this.setLastChild(node.previousSibling);\r
449         }\r
450 \r
451         node.clear();\r
452         this.fireEvent("remove", this.ownerTree, this, node);\r
453         if(destroy){\r
454             node.destroy();\r
455         }\r
456         return node;\r
457     },\r
458     \r
459     // private\r
460     clear : function(destroy){\r
461         // clear any references from the node\r
462         this.setOwnerTree(null, destroy);\r
463         this.parentNode = this.previousSibling = this.nextSibling = null\r
464         if(destroy){\r
465             this.firstChild = this.lastChild = null; \r
466         }\r
467     },\r
468     \r
469     /**\r
470      * Destroys the node.\r
471      */\r
472     destroy : function(){\r
473         this.purgeListeners();\r
474         this.clear(true);  \r
475         Ext.each(this.childNodes, function(n){\r
476             n.destroy();\r
477         });\r
478         this.childNodes = null;\r
479     },\r
480 \r
481     /**\r
482      * Inserts the first node before the second node in this nodes childNodes collection.\r
483      * @param {Node} node The node to insert\r
484      * @param {Node} refNode The node to insert before (if null the node is appended)\r
485      * @return {Node} The inserted node\r
486      */\r
487     insertBefore : function(node, refNode){\r
488         if(!refNode){ // like standard Dom, refNode can be null for append\r
489             return this.appendChild(node);\r
490         }\r
491         // nothing to do\r
492         if(node == refNode){\r
493             return false;\r
494         }\r
495 \r
496         if(this.fireEvent("beforeinsert", this.ownerTree, this, node, refNode) === false){\r
497             return false;\r
498         }\r
499         var index = this.childNodes.indexOf(refNode);\r
500         var oldParent = node.parentNode;\r
501         var refIndex = index;\r
502 \r
503         // when moving internally, indexes will change after remove\r
504         if(oldParent == this && this.childNodes.indexOf(node) < index){\r
505             refIndex--;\r
506         }\r
507 \r
508         // it's a move, make sure we move it cleanly\r
509         if(oldParent){\r
510             if(node.fireEvent("beforemove", node.getOwnerTree(), node, oldParent, this, index, refNode) === false){\r
511                 return false;\r
512             }\r
513             oldParent.removeChild(node);\r
514         }\r
515         if(refIndex === 0){\r
516             this.setFirstChild(node);\r
517         }\r
518         this.childNodes.splice(refIndex, 0, node);\r
519         node.parentNode = this;\r
520         var ps = this.childNodes[refIndex-1];\r
521         if(ps){\r
522             node.previousSibling = ps;\r
523             ps.nextSibling = node;\r
524         }else{\r
525             node.previousSibling = null;\r
526         }\r
527         node.nextSibling = refNode;\r
528         refNode.previousSibling = node;\r
529         node.setOwnerTree(this.getOwnerTree());\r
530         this.fireEvent("insert", this.ownerTree, this, node, refNode);\r
531         if(oldParent){\r
532             node.fireEvent("move", this.ownerTree, node, oldParent, this, refIndex, refNode);\r
533         }\r
534         return node;\r
535     },\r
536 \r
537     /**\r
538      * Removes this node from its parent\r
539      * @param {Boolean} destroy <tt>true</tt> to destroy the node upon removal. Defaults to <tt>false</tt>.\r
540      * @return {Node} this\r
541      */\r
542     remove : function(destroy){\r
543         this.parentNode.removeChild(this, destroy);\r
544         return this;\r
545     },\r
546     \r
547     /**\r
548      * Removes all child nodes from this node.\r
549      * @param {Boolean} destroy <tt>true</tt> to destroy the node upon removal. Defaults to <tt>false</tt>.\r
550      * @return {Node} this\r
551      */\r
552     removeAll : function(destroy){\r
553         var cn = this.childNodes,\r
554             n;\r
555         while((n = cn[0])){\r
556             this.removeChild(n, destroy);\r
557         }\r
558         return this;\r
559     },\r
560 \r
561     /**\r
562      * Returns the child node at the specified index.\r
563      * @param {Number} index\r
564      * @return {Node}\r
565      */\r
566     item : function(index){\r
567         return this.childNodes[index];\r
568     },\r
569 \r
570     /**\r
571      * Replaces one child node in this node with another.\r
572      * @param {Node} newChild The replacement node\r
573      * @param {Node} oldChild The node to replace\r
574      * @return {Node} The replaced node\r
575      */\r
576     replaceChild : function(newChild, oldChild){\r
577         var s = oldChild ? oldChild.nextSibling : null;\r
578         this.removeChild(oldChild);\r
579         this.insertBefore(newChild, s);\r
580         return oldChild;\r
581     },\r
582 \r
583     /**\r
584      * Returns the index of a child node\r
585      * @param {Node} node\r
586      * @return {Number} The index of the node or -1 if it was not found\r
587      */\r
588     indexOf : function(child){\r
589         return this.childNodes.indexOf(child);\r
590     },\r
591 \r
592     /**\r
593      * Returns the tree this node is in.\r
594      * @return {Tree}\r
595      */\r
596     getOwnerTree : function(){\r
597         // if it doesn't have one, look for one\r
598         if(!this.ownerTree){\r
599             var p = this;\r
600             while(p){\r
601                 if(p.ownerTree){\r
602                     this.ownerTree = p.ownerTree;\r
603                     break;\r
604                 }\r
605                 p = p.parentNode;\r
606             }\r
607         }\r
608         return this.ownerTree;\r
609     },\r
610 \r
611     /**\r
612      * Returns depth of this node (the root node has a depth of 0)\r
613      * @return {Number}\r
614      */\r
615     getDepth : function(){\r
616         var depth = 0;\r
617         var p = this;\r
618         while(p.parentNode){\r
619             ++depth;\r
620             p = p.parentNode;\r
621         }\r
622         return depth;\r
623     },\r
624 \r
625     // private\r
626     setOwnerTree : function(tree, destroy){\r
627         // if it is a move, we need to update everyone\r
628         if(tree != this.ownerTree){\r
629             if(this.ownerTree){\r
630                 this.ownerTree.unregisterNode(this);\r
631             }\r
632             this.ownerTree = tree;\r
633             // If we're destroying, we don't need to recurse since it will be called on each child node\r
634             if(destroy !== true){\r
635                 Ext.each(this.childNodes, function(n){\r
636                     n.setOwnerTree(tree);\r
637                 });\r
638             }\r
639             if(tree){\r
640                 tree.registerNode(this);\r
641             }\r
642         }\r
643     },\r
644     \r
645     /**\r
646      * Changes the id of this node.\r
647      * @param {String} id The new id for the node.\r
648      */\r
649     setId: function(id){\r
650         if(id !== this.id){\r
651             var t = this.ownerTree;\r
652             if(t){\r
653                 t.unregisterNode(this);\r
654             }\r
655             this.id = this.attributes.id = id;\r
656             if(t){\r
657                 t.registerNode(this);\r
658             }\r
659             this.onIdChange(id);\r
660         }\r
661     },\r
662     \r
663     // private\r
664     onIdChange: Ext.emptyFn,\r
665 \r
666     /**\r
667      * Returns the path for this node. The path can be used to expand or select this node programmatically.\r
668      * @param {String} attr (optional) The attr to use for the path (defaults to the node's id)\r
669      * @return {String} The path\r
670      */\r
671     getPath : function(attr){\r
672         attr = attr || "id";\r
673         var p = this.parentNode;\r
674         var b = [this.attributes[attr]];\r
675         while(p){\r
676             b.unshift(p.attributes[attr]);\r
677             p = p.parentNode;\r
678         }\r
679         var sep = this.getOwnerTree().pathSeparator;\r
680         return sep + b.join(sep);\r
681     },\r
682 \r
683     /**\r
684      * Bubbles up the tree from this node, calling the specified function with each node. The arguments to the function\r
685      * will be the args provided or the current node. If the function returns false at any point,\r
686      * the bubble is stopped.\r
687      * @param {Function} fn The function to call\r
688      * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the function is executed. Defaults to the current Node.\r
689      * @param {Array} args (optional) The args to call the function with (default to passing the current Node)\r
690      */\r
691     bubble : function(fn, scope, args){\r
692         var p = this;\r
693         while(p){\r
694             if(fn.apply(scope || p, args || [p]) === false){\r
695                 break;\r
696             }\r
697             p = p.parentNode;\r
698         }\r
699     },\r
700 \r
701     /**\r
702      * Cascades down the tree from this node, calling the specified function with each node. The arguments to the function\r
703      * will be the args provided or the current node. If the function returns false at any point,\r
704      * the cascade is stopped on that branch.\r
705      * @param {Function} fn The function to call\r
706      * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the function is executed. Defaults to the current Node.\r
707      * @param {Array} args (optional) The args to call the function with (default to passing the current Node)\r
708      */\r
709     cascade : function(fn, scope, args){\r
710         if(fn.apply(scope || this, args || [this]) !== false){\r
711             var cs = this.childNodes;\r
712             for(var i = 0, len = cs.length; i < len; i++) {\r
713                 cs[i].cascade(fn, scope, args);\r
714             }\r
715         }\r
716     },\r
717 \r
718     /**\r
719      * Interates the child nodes of this node, calling the specified function with each node. The arguments to the function\r
720      * will be the args provided or the current node. If the function returns false at any point,\r
721      * the iteration stops.\r
722      * @param {Function} fn The function to call\r
723      * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the function is executed. Defaults to the current Node in the iteration.\r
724      * @param {Array} args (optional) The args to call the function with (default to passing the current Node)\r
725      */\r
726     eachChild : function(fn, scope, args){\r
727         var cs = this.childNodes;\r
728         for(var i = 0, len = cs.length; i < len; i++) {\r
729                 if(fn.apply(scope || this, args || [cs[i]]) === false){\r
730                     break;\r
731                 }\r
732         }\r
733     },\r
734 \r
735     /**\r
736      * Finds the first child that has the attribute with the specified value.\r
737      * @param {String} attribute The attribute name\r
738      * @param {Mixed} value The value to search for\r
739      * @return {Node} The found child or null if none was found\r
740      */\r
741     findChild : function(attribute, value){\r
742         var cs = this.childNodes;\r
743         for(var i = 0, len = cs.length; i < len; i++) {\r
744                 if(cs[i].attributes[attribute] == value){\r
745                     return cs[i];\r
746                 }\r
747         }\r
748         return null;\r
749     },\r
750 \r
751     /**\r
752      * Finds the first child by a custom function. The child matches if the function passed returns <code>true</code>.\r
753      * @param {Function} fn A function which must return <code>true</code> if the passed Node is the required Node.\r
754      * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the function is executed. Defaults to the Node being tested.\r
755      * @return {Node} The found child or null if none was found\r
756      */\r
757     findChildBy : function(fn, scope){\r
758         var cs = this.childNodes;\r
759         for(var i = 0, len = cs.length; i < len; i++) {\r
760                 if(fn.call(scope||cs[i], cs[i]) === true){\r
761                     return cs[i];\r
762                 }\r
763         }\r
764         return null;\r
765     },\r
766 \r
767     /**\r
768      * Sorts this nodes children using the supplied sort function.\r
769      * @param {Function} fn A function which, when passed two Nodes, returns -1, 0 or 1 depending upon required sort order.\r
770      * @param {Object} scope (optional)The scope (<code>this</code> reference) in which the function is executed. Defaults to the browser window.\r
771      */\r
772     sort : function(fn, scope){\r
773         var cs = this.childNodes;\r
774         var len = cs.length;\r
775         if(len > 0){\r
776             var sortFn = scope ? function(){fn.apply(scope, arguments);} : fn;\r
777             cs.sort(sortFn);\r
778             for(var i = 0; i < len; i++){\r
779                 var n = cs[i];\r
780                 n.previousSibling = cs[i-1];\r
781                 n.nextSibling = cs[i+1];\r
782                 if(i === 0){\r
783                     this.setFirstChild(n);\r
784                 }\r
785                 if(i == len-1){\r
786                     this.setLastChild(n);\r
787                 }\r
788             }\r
789         }\r
790     },\r
791 \r
792     /**\r
793      * Returns true if this node is an ancestor (at any point) of the passed node.\r
794      * @param {Node} node\r
795      * @return {Boolean}\r
796      */\r
797     contains : function(node){\r
798         return node.isAncestor(this);\r
799     },\r
800 \r
801     /**\r
802      * Returns true if the passed node is an ancestor (at any point) of this node.\r
803      * @param {Node} node\r
804      * @return {Boolean}\r
805      */\r
806     isAncestor : function(node){\r
807         var p = this.parentNode;\r
808         while(p){\r
809             if(p == node){\r
810                 return true;\r
811             }\r
812             p = p.parentNode;\r
813         }\r
814         return false;\r
815     },\r
816 \r
817     toString : function(){\r
818         return "[Node"+(this.id?" "+this.id:"")+"]";\r
819     }\r
820 });