commit extjs-2.2.1
[extjs.git] / source / data / Tree.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.data.Tree\r
11  * @extends Ext.util.Observable\r
12  * Represents a tree data structure and bubbles all the events for its nodes. The nodes\r
13  * in the tree have most standard DOM functionality.\r
14  * @constructor\r
15  * @param {Node} root (optional) The root node\r
16  */\r
17 Ext.data.Tree = function(root){\r
18    this.nodeHash = {};\r
19    /**\r
20     * The root node for this tree\r
21     * @type Node\r
22     */\r
23    this.root = null;\r
24    if(root){\r
25        this.setRootNode(root);\r
26    }\r
27    this.addEvents(\r
28        /**\r
29         * @event append\r
30         * Fires when a new child node is appended to a node in this tree.\r
31         * @param {Tree} tree The owner tree\r
32         * @param {Node} parent The parent node\r
33         * @param {Node} node The newly appended node\r
34         * @param {Number} index The index of the newly appended node\r
35         */\r
36        "append",\r
37        /**\r
38         * @event remove\r
39         * Fires when a child node is removed from a node in this tree.\r
40         * @param {Tree} tree The owner tree\r
41         * @param {Node} parent The parent node\r
42         * @param {Node} node The child node removed\r
43         */\r
44        "remove",\r
45        /**\r
46         * @event move\r
47         * Fires when a node is moved to a new location in the tree\r
48         * @param {Tree} tree The owner tree\r
49         * @param {Node} node The node moved\r
50         * @param {Node} oldParent The old parent of this node\r
51         * @param {Node} newParent The new parent of this node\r
52         * @param {Number} index The index it was moved to\r
53         */\r
54        "move",\r
55        /**\r
56         * @event insert\r
57         * Fires when a new child node is inserted in a node in this tree.\r
58         * @param {Tree} tree The owner tree\r
59         * @param {Node} parent The parent node\r
60         * @param {Node} node The child node inserted\r
61         * @param {Node} refNode The child node the node was inserted before\r
62         */\r
63        "insert",\r
64        /**\r
65         * @event beforeappend\r
66         * Fires before a new child is appended to a node in this tree, return false to cancel the append.\r
67         * @param {Tree} tree The owner tree\r
68         * @param {Node} parent The parent node\r
69         * @param {Node} node The child node to be appended\r
70         */\r
71        "beforeappend",\r
72        /**\r
73         * @event beforeremove\r
74         * Fires before a child is removed from a node in this tree, return false to cancel the remove.\r
75         * @param {Tree} tree The owner tree\r
76         * @param {Node} parent The parent node\r
77         * @param {Node} node The child node to be removed\r
78         */\r
79        "beforeremove",\r
80        /**\r
81         * @event beforemove\r
82         * Fires before a node is moved to a new location in the tree. Return false to cancel the move.\r
83         * @param {Tree} tree The owner tree\r
84         * @param {Node} node The node being moved\r
85         * @param {Node} oldParent The parent of the node\r
86         * @param {Node} newParent The new parent the node is moving to\r
87         * @param {Number} index The index it is being moved to\r
88         */\r
89        "beforemove",\r
90        /**\r
91         * @event beforeinsert\r
92         * Fires before a new child is inserted in a node in this tree, return false to cancel the insert.\r
93         * @param {Tree} tree The owner tree\r
94         * @param {Node} parent The parent node\r
95         * @param {Node} node The child node to be inserted\r
96         * @param {Node} refNode The child node the node is being inserted before\r
97         */\r
98        "beforeinsert"\r
99    );\r
100 \r
101     Ext.data.Tree.superclass.constructor.call(this);\r
102 };\r
103 \r
104 Ext.extend(Ext.data.Tree, Ext.util.Observable, {\r
105     /**\r
106      * @cfg {String} pathSeparator\r
107      * The token used to separate paths in node ids (defaults to '/').\r
108      */\r
109     pathSeparator: "/",\r
110 \r
111     // private\r
112     proxyNodeEvent : function(){\r
113         return this.fireEvent.apply(this, arguments);\r
114     },\r
115 \r
116     /**\r
117      * Returns the root node for this tree.\r
118      * @return {Node}\r
119      */\r
120     getRootNode : function(){\r
121         return this.root;\r
122     },\r
123 \r
124     /**\r
125      * Sets the root node for this tree.\r
126      * @param {Node} node\r
127      * @return {Node}\r
128      */\r
129     setRootNode : function(node){\r
130         this.root = node;\r
131         node.ownerTree = this;\r
132         node.isRoot = true;\r
133         this.registerNode(node);\r
134         return node;\r
135     },\r
136 \r
137     /**\r
138      * Gets a node in this tree by its id.\r
139      * @param {String} id\r
140      * @return {Node}\r
141      */\r
142     getNodeById : function(id){\r
143         return this.nodeHash[id];\r
144     },\r
145 \r
146     // private\r
147     registerNode : function(node){\r
148         this.nodeHash[node.id] = node;\r
149     },\r
150 \r
151     // private\r
152     unregisterNode : function(node){\r
153         delete this.nodeHash[node.id];\r
154     },\r
155 \r
156     toString : function(){\r
157         return "[Tree"+(this.id?" "+this.id:"")+"]";\r
158     }\r
159 });\r
160 \r
161 /**\r
162  * @class Ext.data.Node\r
163  * @extends Ext.util.Observable\r
164  * @cfg {Boolean} leaf true if this node is a leaf and does not have children\r
165  * @cfg {String} id The id for this node. If one is not specified, one is generated.\r
166  * @constructor\r
167  * @param {Object} attributes The attributes/config for the node\r
168  */\r
169 Ext.data.Node = function(attributes){\r
170     /**\r
171      * The attributes supplied for the node. You can use this property to access any custom attributes you supplied.\r
172      * @type {Object}\r
173      */\r
174     this.attributes = attributes || {};\r
175     this.leaf = this.attributes.leaf;\r
176     /**\r
177      * The node id. @type String\r
178      */\r
179     this.id = this.attributes.id;\r
180     if(!this.id){\r
181         this.id = Ext.id(null, "ynode-");\r
182         this.attributes.id = this.id;\r
183     }\r
184     /**\r
185      * All child nodes of this node. @type Array\r
186      */\r
187     this.childNodes = [];\r
188     if(!this.childNodes.indexOf){ // indexOf is a must\r
189         this.childNodes.indexOf = function(o){\r
190             for(var i = 0, len = this.length; i < len; i++){\r
191                 if(this[i] == o) return i;\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 it's 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's 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      * Returns the path for this node. The path can be used to expand or select this node programmatically.\r
608      * @param {String} attr (optional) The attr to use for the path (defaults to the node's id)\r
609      * @return {String} The path\r
610      */\r
611     getPath : function(attr){\r
612         attr = attr || "id";\r
613         var p = this.parentNode;\r
614         var b = [this.attributes[attr]];\r
615         while(p){\r
616             b.unshift(p.attributes[attr]);\r
617             p = p.parentNode;\r
618         }\r
619         var sep = this.getOwnerTree().pathSeparator;\r
620         return sep + b.join(sep);\r
621     },\r
622 \r
623     /**\r
624      * Bubbles up the tree from this node, calling the specified function with each node. The scope (<i>this</i>) of\r
625      * function call will be the scope provided or the current node. The arguments to the function\r
626      * will be the args provided or the current node. If the function returns false at any point,\r
627      * the bubble is stopped.\r
628      * @param {Function} fn The function to call\r
629      * @param {Object} scope (optional) The scope of the function (defaults to current node)\r
630      * @param {Array} args (optional) The args to call the function with (default to passing the current node)\r
631      */\r
632     bubble : function(fn, scope, args){\r
633         var p = this;\r
634         while(p){\r
635             if(fn.apply(scope || p, args || [p]) === false){\r
636                 break;\r
637             }\r
638             p = p.parentNode;\r
639         }\r
640     },\r
641 \r
642     /**\r
643      * Cascades down the tree from this node, calling the specified function with each node. The scope (<i>this</i>) of\r
644      * function call will be the scope provided or the current node. The arguments to the function\r
645      * will be the args provided or the current node. If the function returns false at any point,\r
646      * the cascade is stopped on that branch.\r
647      * @param {Function} fn The function to call\r
648      * @param {Object} scope (optional) The scope of the function (defaults to current node)\r
649      * @param {Array} args (optional) The args to call the function with (default to passing the current node)\r
650      */\r
651     cascade : function(fn, scope, args){\r
652         if(fn.apply(scope || this, args || [this]) !== false){\r
653             var cs = this.childNodes;\r
654             for(var i = 0, len = cs.length; i < len; i++) {\r
655                 cs[i].cascade(fn, scope, args);\r
656             }\r
657         }\r
658     },\r
659 \r
660     /**\r
661      * Iterates the child nodes of this node, calling the specified function with each node. The scope (<i>this</i>) of\r
662      * function call will be the scope provided or the current node. The arguments to the function\r
663      * will be the args provided or the current node. If the function returns false at any point,\r
664      * the iteration stops.\r
665      * @param {Function} fn The function to call\r
666      * @param {Object} scope (optional) The scope of the function (defaults to current node)\r
667      * @param {Array} args (optional) The args to call the function with (default to passing the current node)\r
668      */\r
669     eachChild : function(fn, scope, args){\r
670         var cs = this.childNodes;\r
671         for(var i = 0, len = cs.length; i < len; i++) {\r
672                 if(fn.apply(scope || this, args || [cs[i]]) === false){\r
673                     break;\r
674                 }\r
675         }\r
676     },\r
677 \r
678     /**\r
679      * Finds the first child that has the attribute with the specified value.\r
680      * @param {String} attribute The attribute name\r
681      * @param {Mixed} value The value to search for\r
682      * @return {Node} The found child or null if none was found\r
683      */\r
684     findChild : function(attribute, value){\r
685         var cs = this.childNodes;\r
686         for(var i = 0, len = cs.length; i < len; i++) {\r
687                 if(cs[i].attributes[attribute] == value){\r
688                     return cs[i];\r
689                 }\r
690         }\r
691         return null;\r
692     },\r
693 \r
694     /**\r
695      * Finds the first child by a custom function. The child matches if the function passed\r
696      * returns true.\r
697      * @param {Function} fn\r
698      * @param {Object} scope (optional)\r
699      * @return {Node} The found child or null if none was found\r
700      */\r
701     findChildBy : function(fn, scope){\r
702         var cs = this.childNodes;\r
703         for(var i = 0, len = cs.length; i < len; i++) {\r
704                 if(fn.call(scope||cs[i], cs[i]) === true){\r
705                     return cs[i];\r
706                 }\r
707         }\r
708         return null;\r
709     },\r
710 \r
711     /**\r
712      * Sorts this nodes children using the supplied sort function\r
713      * @param {Function} fn\r
714      * @param {Object} scope (optional)\r
715      */\r
716     sort : function(fn, scope){\r
717         var cs = this.childNodes;\r
718         var len = cs.length;\r
719         if(len > 0){\r
720             var sortFn = scope ? function(){fn.apply(scope, arguments);} : fn;\r
721             cs.sort(sortFn);\r
722             for(var i = 0; i < len; i++){\r
723                 var n = cs[i];\r
724                 n.previousSibling = cs[i-1];\r
725                 n.nextSibling = cs[i+1];\r
726                 if(i == 0){\r
727                     this.setFirstChild(n);\r
728                 }\r
729                 if(i == len-1){\r
730                     this.setLastChild(n);\r
731                 }\r
732             }\r
733         }\r
734     },\r
735 \r
736     /**\r
737      * Returns true if this node is an ancestor (at any point) of the passed node.\r
738      * @param {Node} node\r
739      * @return {Boolean}\r
740      */\r
741     contains : function(node){\r
742         return node.isAncestor(this);\r
743     },\r
744 \r
745     /**\r
746      * Returns true if the passed node is an ancestor (at any point) of this node.\r
747      * @param {Node} node\r
748      * @return {Boolean}\r
749      */\r
750     isAncestor : function(node){\r
751         var p = this.parentNode;\r
752         while(p){\r
753             if(p == node){\r
754                 return true;\r
755             }\r
756             p = p.parentNode;\r
757         }\r
758         return false;\r
759     },\r
760 \r
761     toString : function(){\r
762         return "[Node"+(this.id?" "+this.id:"")+"]";\r
763     }\r
764 });