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