3 * Copyright(c) 2006-2010 Ext JS, Inc.
5 * http://www.extjs.com/license
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.
13 * @param {Node} root (optional) The root node
15 Ext.data.Tree = Ext.extend(Ext.util.Observable, {
17 constructor: function(root){
20 * The root node for this tree
25 this.setRootNode(root);
30 * Fires when a new child node is appended to a node in this tree.
31 * @param {Tree} tree The owner tree
32 * @param {Node} parent The parent node
33 * @param {Node} node The newly appended node
34 * @param {Number} index The index of the newly appended node
39 * Fires when a child node is removed from a node in this tree.
40 * @param {Tree} tree The owner tree
41 * @param {Node} parent The parent node
42 * @param {Node} node The child node removed
47 * Fires when a node is moved to a new location in the tree
48 * @param {Tree} tree The owner tree
49 * @param {Node} node The node moved
50 * @param {Node} oldParent The old parent of this node
51 * @param {Node} newParent The new parent of this node
52 * @param {Number} index The index it was moved to
57 * Fires when a new child node is inserted in a node in this tree.
58 * @param {Tree} tree The owner tree
59 * @param {Node} parent The parent node
60 * @param {Node} node The child node inserted
61 * @param {Node} refNode The child node the node was inserted before
66 * Fires before a new child is appended to a node in this tree, return false to cancel the append.
67 * @param {Tree} tree The owner tree
68 * @param {Node} parent The parent node
69 * @param {Node} node The child node to be appended
74 * Fires before a child is removed from a node in this tree, return false to cancel the remove.
75 * @param {Tree} tree The owner tree
76 * @param {Node} parent The parent node
77 * @param {Node} node The child node to be removed
82 * Fires before a node is moved to a new location in the tree. Return false to cancel the move.
83 * @param {Tree} tree The owner tree
84 * @param {Node} node The node being moved
85 * @param {Node} oldParent The parent of the node
86 * @param {Node} newParent The new parent the node is moving to
87 * @param {Number} index The index it is being moved to
92 * Fires before a new child is inserted in a node in this tree, return false to cancel the insert.
93 * @param {Tree} tree The owner tree
94 * @param {Node} parent The parent node
95 * @param {Node} node The child node to be inserted
96 * @param {Node} refNode The child node the node is being inserted before
100 Ext.data.Tree.superclass.constructor.call(this);
104 * @cfg {String} pathSeparator
105 * The token used to separate paths in node ids (defaults to '/').
110 proxyNodeEvent : function(){
111 return this.fireEvent.apply(this, arguments);
115 * Returns the root node for this tree.
118 getRootNode : function(){
123 * Sets the root node for this tree.
127 setRootNode : function(node){
129 node.ownerTree = this;
131 this.registerNode(node);
136 * Gets a node in this tree by its id.
140 getNodeById : function(id){
141 return this.nodeHash[id];
145 registerNode : function(node){
146 this.nodeHash[node.id] = node;
150 unregisterNode : function(node){
151 delete this.nodeHash[node.id];
154 toString : function(){
155 return "[Tree"+(this.id?" "+this.id:"")+"]";
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.
165 * @param {Object} attributes The attributes/config for the node
167 Ext.data.Node = Ext.extend(Ext.util.Observable, {
169 constructor: function(attributes){
171 * The attributes supplied for the node. You can use this property to access any custom attributes you supplied.
174 this.attributes = attributes || {};
175 this.leaf = this.attributes.leaf;
177 * The node id. @type String
179 this.id = this.attributes.id;
181 this.id = Ext.id(null, "xnode-");
182 this.attributes.id = this.id;
185 * All child nodes of this node. @type Array
187 this.childNodes = [];
189 * The parent node for this node. @type Node
191 this.parentNode = null;
193 * The first direct child node of this node, or null if this node has no child nodes. @type Node
195 this.firstChild = null;
197 * The last direct child node of this node, or null if this node has no child nodes. @type Node
199 this.lastChild = null;
201 * The node immediately preceding this node in the tree, or null if there is no sibling node. @type Node
203 this.previousSibling = null;
205 * The node immediately following this node in the tree, or null if there is no sibling node. @type Node
207 this.nextSibling = null;
212 * Fires when a new child node is appended
213 * @param {Tree} tree The owner tree
214 * @param {Node} this This node
215 * @param {Node} node The newly appended node
216 * @param {Number} index The index of the newly appended node
221 * Fires when a child node is removed
222 * @param {Tree} tree The owner tree
223 * @param {Node} this This node
224 * @param {Node} node The removed node
229 * Fires when this node is moved to a new location in the tree
230 * @param {Tree} tree The owner tree
231 * @param {Node} this This node
232 * @param {Node} oldParent The old parent of this node
233 * @param {Node} newParent The new parent of this node
234 * @param {Number} index The index it was moved to
239 * Fires when a new child node is inserted.
240 * @param {Tree} tree The owner tree
241 * @param {Node} this This node
242 * @param {Node} node The child node inserted
243 * @param {Node} refNode The child node the node was inserted before
247 * @event beforeappend
248 * Fires before a new child is appended, return false to cancel the append.
249 * @param {Tree} tree The owner tree
250 * @param {Node} this This node
251 * @param {Node} node The child node to be appended
253 "beforeappend" : true,
255 * @event beforeremove
256 * Fires before a child is removed, return false to cancel the remove.
257 * @param {Tree} tree The owner tree
258 * @param {Node} this This node
259 * @param {Node} node The child node to be removed
261 "beforeremove" : true,
264 * Fires before this node is moved to a new location in the tree. Return false to cancel the move.
265 * @param {Tree} tree The owner tree
266 * @param {Node} this This node
267 * @param {Node} oldParent The parent of this node
268 * @param {Node} newParent The new parent this node is moving to
269 * @param {Number} index The index it is being moved to
273 * @event beforeinsert
274 * Fires before a new child is inserted, return false to cancel the insert.
275 * @param {Tree} tree The owner tree
276 * @param {Node} this This node
277 * @param {Node} node The child node to be inserted
278 * @param {Node} refNode The child node the node is being inserted before
280 "beforeinsert" : true
282 this.listeners = this.attributes.listeners;
283 Ext.data.Node.superclass.constructor.call(this);
287 fireEvent : function(evtName){
288 // first do standard event for this node
289 if(Ext.data.Node.superclass.fireEvent.apply(this, arguments) === false){
292 // then bubble it up to the tree if the event wasn't cancelled
293 var ot = this.getOwnerTree();
295 if(ot.proxyNodeEvent.apply(ot, arguments) === false){
303 * Returns true if this node is a leaf
307 return this.leaf === true;
311 setFirstChild : function(node){
312 this.firstChild = node;
316 setLastChild : function(node){
317 this.lastChild = node;
322 * Returns true if this node is the last child of its parent
326 return (!this.parentNode ? true : this.parentNode.lastChild == this);
330 * Returns true if this node is the first child of its parent
333 isFirst : function(){
334 return (!this.parentNode ? true : this.parentNode.firstChild == this);
338 * Returns true if this node has one or more child nodes, else false.
341 hasChildNodes : function(){
342 return !this.isLeaf() && this.childNodes.length > 0;
346 * Returns true if this node has one or more child nodes, or if the <tt>expandable</tt>
347 * node attribute is explicitly specified as true (see {@link #attributes}), otherwise returns false.
350 isExpandable : function(){
351 return this.attributes.expandable || this.hasChildNodes();
355 * Insert node(s) as the last child node of this node.
356 * @param {Node/Array} node The node or Array of nodes to append
357 * @return {Node} The appended node if single append, or null if an array was passed
359 appendChild : function(node){
361 if(Ext.isArray(node)){
363 }else if(arguments.length > 1){
366 // if passed an array or multiple args do them one by one
368 for(var i = 0, len = multi.length; i < len; i++) {
369 this.appendChild(multi[i]);
372 if(this.fireEvent("beforeappend", this.ownerTree, this, node) === false){
375 var index = this.childNodes.length;
376 var oldParent = node.parentNode;
377 // it's a move, make sure we move it cleanly
379 if(node.fireEvent("beforemove", node.getOwnerTree(), node, oldParent, this, index) === false){
382 oldParent.removeChild(node);
384 index = this.childNodes.length;
386 this.setFirstChild(node);
388 this.childNodes.push(node);
389 node.parentNode = this;
390 var ps = this.childNodes[index-1];
392 node.previousSibling = ps;
393 ps.nextSibling = node;
395 node.previousSibling = null;
397 node.nextSibling = null;
398 this.setLastChild(node);
399 node.setOwnerTree(this.getOwnerTree());
400 this.fireEvent("append", this.ownerTree, this, node, index);
402 node.fireEvent("move", this.ownerTree, node, oldParent, this, index);
409 * Removes a child node from this node.
410 * @param {Node} node The node to remove
411 * @param {Boolean} destroy <tt>true</tt> to destroy the node upon removal. Defaults to <tt>false</tt>.
412 * @return {Node} The removed node
414 removeChild : function(node, destroy){
415 var index = this.childNodes.indexOf(node);
419 if(this.fireEvent("beforeremove", this.ownerTree, this, node) === false){
423 // remove it from childNodes collection
424 this.childNodes.splice(index, 1);
427 if(node.previousSibling){
428 node.previousSibling.nextSibling = node.nextSibling;
430 if(node.nextSibling){
431 node.nextSibling.previousSibling = node.previousSibling;
435 if(this.firstChild == node){
436 this.setFirstChild(node.nextSibling);
438 if(this.lastChild == node){
439 this.setLastChild(node.previousSibling);
442 this.fireEvent("remove", this.ownerTree, this, node);
452 clear : function(destroy){
453 // clear any references from the node
454 this.setOwnerTree(null, destroy);
455 this.parentNode = this.previousSibling = this.nextSibling = null;
457 this.firstChild = this.lastChild = null;
464 destroy : function(/* private */ silent){
466 * Silent is to be used in a number of cases
467 * 1) When setRootNode is called.
468 * 2) When destroy on the tree is called
469 * 3) For destroying child nodes on a node
472 this.purgeListeners();
474 Ext.each(this.childNodes, function(n){
477 this.childNodes = null;
484 * Inserts the first node before the second node in this nodes childNodes collection.
485 * @param {Node} node The node to insert
486 * @param {Node} refNode The node to insert before (if null the node is appended)
487 * @return {Node} The inserted node
489 insertBefore : function(node, refNode){
490 if(!refNode){ // like standard Dom, refNode can be null for append
491 return this.appendChild(node);
498 if(this.fireEvent("beforeinsert", this.ownerTree, this, node, refNode) === false){
501 var index = this.childNodes.indexOf(refNode);
502 var oldParent = node.parentNode;
503 var refIndex = index;
505 // when moving internally, indexes will change after remove
506 if(oldParent == this && this.childNodes.indexOf(node) < index){
510 // it's a move, make sure we move it cleanly
512 if(node.fireEvent("beforemove", node.getOwnerTree(), node, oldParent, this, index, refNode) === false){
515 oldParent.removeChild(node);
518 this.setFirstChild(node);
520 this.childNodes.splice(refIndex, 0, node);
521 node.parentNode = this;
522 var ps = this.childNodes[refIndex-1];
524 node.previousSibling = ps;
525 ps.nextSibling = node;
527 node.previousSibling = null;
529 node.nextSibling = refNode;
530 refNode.previousSibling = node;
531 node.setOwnerTree(this.getOwnerTree());
532 this.fireEvent("insert", this.ownerTree, this, node, refNode);
534 node.fireEvent("move", this.ownerTree, node, oldParent, this, refIndex, refNode);
540 * Removes this node from its parent
541 * @param {Boolean} destroy <tt>true</tt> to destroy the node upon removal. Defaults to <tt>false</tt>.
542 * @return {Node} this
544 remove : function(destroy){
545 if (this.parentNode) {
546 this.parentNode.removeChild(this, destroy);
552 * Removes all child nodes from this node.
553 * @param {Boolean} destroy <tt>true</tt> to destroy the node upon removal. Defaults to <tt>false</tt>.
554 * @return {Node} this
556 removeAll : function(destroy){
557 var cn = this.childNodes,
560 this.removeChild(n, destroy);
566 * Returns the child node at the specified index.
567 * @param {Number} index
570 item : function(index){
571 return this.childNodes[index];
575 * Replaces one child node in this node with another.
576 * @param {Node} newChild The replacement node
577 * @param {Node} oldChild The node to replace
578 * @return {Node} The replaced node
580 replaceChild : function(newChild, oldChild){
581 var s = oldChild ? oldChild.nextSibling : null;
582 this.removeChild(oldChild);
583 this.insertBefore(newChild, s);
588 * Returns the index of a child node
590 * @return {Number} The index of the node or -1 if it was not found
592 indexOf : function(child){
593 return this.childNodes.indexOf(child);
597 * Returns the tree this node is in.
600 getOwnerTree : function(){
601 // if it doesn't have one, look for one
606 this.ownerTree = p.ownerTree;
612 return this.ownerTree;
616 * Returns depth of this node (the root node has a depth of 0)
619 getDepth : function(){
630 setOwnerTree : function(tree, destroy){
631 // if it is a move, we need to update everyone
632 if(tree != this.ownerTree){
634 this.ownerTree.unregisterNode(this);
636 this.ownerTree = tree;
637 // If we're destroying, we don't need to recurse since it will be called on each child node
638 if(destroy !== true){
639 Ext.each(this.childNodes, function(n){
640 n.setOwnerTree(tree);
644 tree.registerNode(this);
650 * Changes the id of this node.
651 * @param {String} id The new id for the node.
655 var t = this.ownerTree;
657 t.unregisterNode(this);
659 this.id = this.attributes.id = id;
661 t.registerNode(this);
668 onIdChange: Ext.emptyFn,
671 * Returns the path for this node. The path can be used to expand or select this node programmatically.
672 * @param {String} attr (optional) The attr to use for the path (defaults to the node's id)
673 * @return {String} The path
675 getPath : function(attr){
677 var p = this.parentNode;
678 var b = [this.attributes[attr]];
680 b.unshift(p.attributes[attr]);
683 var sep = this.getOwnerTree().pathSeparator;
684 return sep + b.join(sep);
688 * Bubbles up the tree from this node, calling the specified function with each node. The arguments to the function
689 * will be the args provided or the current node. If the function returns false at any point,
690 * the bubble is stopped.
691 * @param {Function} fn The function to call
692 * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the function is executed. Defaults to the current Node.
693 * @param {Array} args (optional) The args to call the function with (default to passing the current Node)
695 bubble : function(fn, scope, args){
698 if(fn.apply(scope || p, args || [p]) === false){
706 * Cascades down the tree from this node, calling the specified function with each node. The arguments to the function
707 * will be the args provided or the current node. If the function returns false at any point,
708 * the cascade is stopped on that branch.
709 * @param {Function} fn The function to call
710 * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the function is executed. Defaults to the current Node.
711 * @param {Array} args (optional) The args to call the function with (default to passing the current Node)
713 cascade : function(fn, scope, args){
714 if(fn.apply(scope || this, args || [this]) !== false){
715 var cs = this.childNodes;
716 for(var i = 0, len = cs.length; i < len; i++) {
717 cs[i].cascade(fn, scope, args);
723 * Interates the child nodes of this node, calling the specified function with each node. The arguments to the function
724 * will be the args provided or the current node. If the function returns false at any point,
725 * the iteration stops.
726 * @param {Function} fn The function to call
727 * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the function is executed. Defaults to the current Node in the iteration.
728 * @param {Array} args (optional) The args to call the function with (default to passing the current Node)
730 eachChild : function(fn, scope, args){
731 var cs = this.childNodes;
732 for(var i = 0, len = cs.length; i < len; i++) {
733 if(fn.apply(scope || cs[i], args || [cs[i]]) === false){
740 * Finds the first child that has the attribute with the specified value.
741 * @param {String} attribute The attribute name
742 * @param {Mixed} value The value to search for
743 * @param {Boolean} deep (Optional) True to search through nodes deeper than the immediate children
744 * @return {Node} The found child or null if none was found
746 findChild : function(attribute, value, deep){
747 return this.findChildBy(function(){
748 return this.attributes[attribute] == value;
753 * Finds the first child by a custom function. The child matches if the function passed returns <code>true</code>.
754 * @param {Function} fn A function which must return <code>true</code> if the passed Node is the required Node.
755 * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the function is executed. Defaults to the Node being tested.
756 * @param {Boolean} deep (Optional) True to search through nodes deeper than the immediate children
757 * @return {Node} The found child or null if none was found
759 findChildBy : function(fn, scope, deep){
760 var cs = this.childNodes,
767 if(fn.call(scope || n, n) === true){
770 res = n.findChildBy(fn, scope, deep);
781 * Sorts this nodes children using the supplied sort function.
782 * @param {Function} fn A function which, when passed two Nodes, returns -1, 0 or 1 depending upon required sort order.
783 * @param {Object} scope (optional)The scope (<code>this</code> reference) in which the function is executed. Defaults to the browser window.
785 sort : function(fn, scope){
786 var cs = this.childNodes;
789 var sortFn = scope ? function(){fn.apply(scope, arguments);} : fn;
791 for(var i = 0; i < len; i++){
793 n.previousSibling = cs[i-1];
794 n.nextSibling = cs[i+1];
796 this.setFirstChild(n);
799 this.setLastChild(n);
806 * Returns true if this node is an ancestor (at any point) of the passed node.
810 contains : function(node){
811 return node.isAncestor(this);
815 * Returns true if the passed node is an ancestor (at any point) of this node.
819 isAncestor : function(node){
820 var p = this.parentNode;
830 toString : function(){
831 return "[Node"+(this.id?" "+this.id:"")+"]";