Upgrade to ExtJS 4.0.1 - Released 05/18/2011
[extjs.git] / src / data / NodeInterface.js
1 /**
2  * @class Ext.data.NodeInterface
3  * This class is meant to be used as a set of methods that are applied to the prototype of a
4  * Record to decorate it with a Node API. This means that models used in conjunction with a tree
5  * will have all of the tree related methods available on the model. In general this class will
6  * not be used directly by the developer.
7  */
8 Ext.define('Ext.data.NodeInterface', {
9     requires: ['Ext.data.Field'],
10     
11     statics: {
12         /**
13          * This method allows you to decorate a Record's prototype to implement the NodeInterface.
14          * This adds a set of methods, new events, new properties and new fields on every Record
15          * with the same Model as the passed Record.
16          * @param {Ext.data.Record} record The Record you want to decorate the prototype of.
17          * @static
18          */
19         decorate: function(record) {
20             if (!record.isNode) {
21                 // Apply the methods and fields to the prototype
22                 // @TODO: clean this up to use proper class system stuff
23                 var mgr = Ext.ModelManager,
24                     modelName = record.modelName,
25                     modelClass = mgr.getModel(modelName),
26                     idName = modelClass.prototype.idProperty,
27                     instances = Ext.Array.filter(mgr.all.getArray(), function(item) {
28                         return item.modelName == modelName;
29                     }),
30                     iln = instances.length,
31                     newFields = [],
32                     i, instance, jln, j, newField;
33
34                 // Start by adding the NodeInterface methods to the Model's prototype
35                 modelClass.override(this.getPrototypeBody());
36                 newFields = this.applyFields(modelClass, [
37                     {name: idName,      type: 'string',  defaultValue: null},
38                     {name: 'parentId',  type: 'string',  defaultValue: null},
39                     {name: 'index',     type: 'int',     defaultValue: null},
40                     {name: 'depth',     type: 'int',     defaultValue: 0}, 
41                     {name: 'expanded',  type: 'bool',    defaultValue: false, persist: false},
42                     {name: 'checked',   type: 'auto',    defaultValue: null},
43                     {name: 'leaf',      type: 'bool',    defaultValue: false, persist: false},
44                     {name: 'cls',       type: 'string',  defaultValue: null, persist: false},
45                     {name: 'iconCls',   type: 'string',  defaultValue: null, persist: false},
46                     {name: 'root',      type: 'boolean', defaultValue: false, persist: false},
47                     {name: 'isLast',    type: 'boolean', defaultValue: false, persist: false},
48                     {name: 'isFirst',   type: 'boolean', defaultValue: false, persist: false},
49                     {name: 'allowDrop', type: 'boolean', defaultValue: true, persist: false},
50                     {name: 'allowDrag', type: 'boolean', defaultValue: true, persist: false},
51                     {name: 'loaded',    type: 'boolean', defaultValue: false, persist: false},
52                     {name: 'loading',   type: 'boolean', defaultValue: false, persist: false},
53                     {name: 'href',      type: 'string',  defaultValue: null, persist: false},
54                     {name: 'hrefTarget',type: 'string',  defaultValue: null, persist: false},
55                     {name: 'qtip',      type: 'string',  defaultValue: null, persist: false},
56                     {name: 'qtitle',    type: 'string',  defaultValue: null, persist: false}
57                 ]);
58
59                 jln = newFields.length;
60                 // Set default values to all instances already out there
61                 for (i = 0; i < iln; i++) {
62                     instance = instances[i];
63                     for (j = 0; j < jln; j++) {
64                         newField = newFields[j];
65                         if (instance.get(newField.name) === undefined) {
66                             instance.data[newField.name] = newField.defaultValue;
67                         }
68                     }
69                 }
70             }
71             
72             Ext.applyIf(record, {
73                 firstChild: null,
74                 lastChild: null,
75                 parentNode: null,
76                 previousSibling: null,
77                 nextSibling: null,
78                 childNodes: []
79             });
80             // Commit any fields so the record doesn't show as dirty initially
81             record.commit(true);
82             
83             record.enableBubble([
84                 /**
85                  * @event append
86                  * Fires when a new child node is appended
87                  * @param {Node} this This node
88                  * @param {Node} node The newly appended node
89                  * @param {Number} index The index of the newly appended node
90                  */
91                 "append",
92
93                 /**
94                  * @event remove
95                  * Fires when a child node is removed
96                  * @param {Node} this This node
97                  * @param {Node} node The removed node
98                  */
99                 "remove",
100
101                 /**
102                  * @event move
103                  * Fires when this node is moved to a new location in the tree
104                  * @param {Node} this This node
105                  * @param {Node} oldParent The old parent of this node
106                  * @param {Node} newParent The new parent of this node
107                  * @param {Number} index The index it was moved to
108                  */
109                 "move",
110
111                 /**
112                  * @event insert
113                  * Fires when a new child node is inserted.
114                  * @param {Node} this This node
115                  * @param {Node} node The child node inserted
116                  * @param {Node} refNode The child node the node was inserted before
117                  */
118                 "insert",
119
120                 /**
121                  * @event beforeappend
122                  * Fires before a new child is appended, return false to cancel the append.
123                  * @param {Node} this This node
124                  * @param {Node} node The child node to be appended
125                  */
126                 "beforeappend",
127
128                 /**
129                  * @event beforeremove
130                  * Fires before a child is removed, return false to cancel the remove.
131                  * @param {Node} this This node
132                  * @param {Node} node The child node to be removed
133                  */
134                 "beforeremove",
135
136                 /**
137                  * @event beforemove
138                  * Fires before this node is moved to a new location in the tree. Return false to cancel the move.
139                  * @param {Node} this This node
140                  * @param {Node} oldParent The parent of this node
141                  * @param {Node} newParent The new parent this node is moving to
142                  * @param {Number} index The index it is being moved to
143                  */
144                 "beforemove",
145
146                  /**
147                   * @event beforeinsert
148                   * Fires before a new child is inserted, return false to cancel the insert.
149                   * @param {Node} this This node
150                   * @param {Node} node The child node to be inserted
151                   * @param {Node} refNode The child node the node is being inserted before
152                   */
153                 "beforeinsert",
154                 
155                 /**
156                  * @event expand
157                  * Fires when this node is expanded.
158                  * @param {Node} this The expanding node
159                  */
160                 "expand",
161                 
162                 /**
163                  * @event collapse
164                  * Fires when this node is collapsed.
165                  * @param {Node} this The collapsing node
166                  */
167                 "collapse",
168                 
169                 /**
170                  * @event beforeexpand
171                  * Fires before this node is expanded.
172                  * @param {Node} this The expanding node
173                  */
174                 "beforeexpand",
175                 
176                 /**
177                  * @event beforecollapse
178                  * Fires before this node is collapsed.
179                  * @param {Node} this The collapsing node
180                  */
181                 "beforecollapse",
182                 
183                 /**
184                  * @event beforecollapse
185                  * Fires before this node is collapsed.
186                  * @param {Node} this The collapsing node
187                  */
188                 "sort"
189             ]);
190             
191             return record;
192         },
193         
194         applyFields: function(modelClass, addFields) {
195             var modelPrototype = modelClass.prototype,
196                 fields = modelPrototype.fields,
197                 keys = fields.keys,
198                 ln = addFields.length,
199                 addField, i, name,
200                 newFields = [];
201                 
202             for (i = 0; i < ln; i++) {
203                 addField = addFields[i];
204                 if (!Ext.Array.contains(keys, addField.name)) {
205                     addField = Ext.create('data.field', addField);
206                     
207                     newFields.push(addField);
208                     fields.add(addField);
209                 }
210             }
211             
212             return newFields;
213         },
214         
215         getPrototypeBody: function() {
216             return {
217                 isNode: true,
218
219                 /**
220                  * Ensures that the passed object is an instance of a Record with the NodeInterface applied
221                  * @return {Boolean}
222                  */
223                 createNode: function(node) {
224                     if (Ext.isObject(node) && !node.isModel) {
225                         node = Ext.ModelManager.create(node, this.modelName);
226                     }
227                     // Make sure the node implements the node interface
228                     return Ext.data.NodeInterface.decorate(node);
229                 },
230                 
231                 /**
232                  * Returns true if this node is a leaf
233                  * @return {Boolean}
234                  */
235                 isLeaf : function() {
236                     return this.get('leaf') === true;
237                 },
238
239                 /**
240                  * Sets the first child of this node
241                  * @private
242                  * @param {Ext.data.NodeInterface} node
243                  */
244                 setFirstChild : function(node) {
245                     this.firstChild = node;
246                 },
247
248                 /**
249                  * Sets the last child of this node
250                  * @private
251                  * @param {Ext.data.NodeInterface} node
252                  */
253                 setLastChild : function(node) {
254                     this.lastChild = node;
255                 },
256
257                 /**
258                  * Updates general data of this node like isFirst, isLast, depth. This
259                  * method is internally called after a node is moved. This shouldn't
260                  * have to be called by the developer unless they are creating custom
261                  * Tree plugins.
262                  * @return {Boolean}
263                  */
264                 updateInfo: function(silent) {
265                     var me = this,
266                         isRoot = me.isRoot(),
267                         parentNode = me.parentNode,
268                         isFirst = (!parentNode ? true : parentNode.firstChild == me),
269                         isLast = (!parentNode ? true : parentNode.lastChild == me),
270                         depth = 0,
271                         parent = me,
272                         children = me.childNodes,
273                         len = children.length,
274                         i = 0;
275
276                     while (parent.parentNode) {
277                         ++depth;
278                         parent = parent.parentNode;
279                     }                                            
280                     
281                     me.beginEdit();
282                     me.set({
283                         isFirst: isFirst,
284                         isLast: isLast,
285                         depth: depth,
286                         index: parentNode ? parentNode.indexOf(me) : 0,
287                         parentId: parentNode ? parentNode.getId() : null
288                     });
289                     me.endEdit(silent);
290                     if (silent) {
291                         me.commit();
292                     }
293                     
294                     for (i = 0; i < len; i++) {
295                         children[i].updateInfo(silent);
296                     }
297                 },
298
299                 /**
300                  * Returns true if this node is the last child of its parent
301                  * @return {Boolean}
302                  */
303                 isLast : function() {
304                    return this.get('isLast');
305                 },
306
307                 /**
308                  * Returns true if this node is the first child of its parent
309                  * @return {Boolean}
310                  */
311                 isFirst : function() {
312                    return this.get('isFirst');
313                 },
314
315                 /**
316                  * Returns true if this node has one or more child nodes, else false.
317                  * @return {Boolean}
318                  */
319                 hasChildNodes : function() {
320                     return !this.isLeaf() && this.childNodes.length > 0;
321                 },
322
323                 /**
324                  * Returns true if this node has one or more child nodes, or if the <tt>expandable</tt>
325                  * node attribute is explicitly specified as true (see {@link #attributes}), otherwise returns false.
326                  * @return {Boolean}
327                  */
328                 isExpandable : function() {
329                     return this.get('expandable') || this.hasChildNodes();
330                 },
331
332                 /**
333                  * <p>Insert node(s) as the last child node of this node.</p>
334                  * <p>If the node was previously a child node of another parent node, it will be removed from that node first.</p>
335                  * @param {Node/Array} node The node or Array of nodes to append
336                  * @return {Node} The appended node if single append, or null if an array was passed
337                  */
338                 appendChild : function(node, suppressEvents, suppressNodeUpdate) {
339                     var me = this,
340                         i, ln,
341                         index,
342                         oldParent,
343                         ps;
344
345                     // if passed an array or multiple args do them one by one
346                     if (Ext.isArray(node)) {
347                         for (i = 0, ln = node.length; i < ln; i++) {
348                             me.appendChild(node[i]);
349                         }
350                     } else {
351                         // Make sure it is a record
352                         node = me.createNode(node);
353                         
354                         if (suppressEvents !== true && me.fireEvent("beforeappend", me, node) === false) {
355                             return false;                         
356                         }
357
358                         index = me.childNodes.length;
359                         oldParent = node.parentNode;
360
361                         // it's a move, make sure we move it cleanly
362                         if (oldParent) {
363                             if (suppressEvents !== true && node.fireEvent("beforemove", node, oldParent, me, index) === false) {
364                                 return false;
365                             }
366                             oldParent.removeChild(node, null, false, true);
367                         }
368
369                         index = me.childNodes.length;
370                         if (index === 0) {
371                             me.setFirstChild(node);
372                         }
373
374                         me.childNodes.push(node);
375                         node.parentNode = me;
376                         node.nextSibling = null;
377
378                         me.setLastChild(node);
379                                                 
380                         ps = me.childNodes[index - 1];
381                         if (ps) {
382                             node.previousSibling = ps;
383                             ps.nextSibling = node;
384                             ps.updateInfo(suppressNodeUpdate);
385                         } else {
386                             node.previousSibling = null;
387                         }
388
389                         node.updateInfo(suppressNodeUpdate);
390                         
391                         // As soon as we append a child to this node, we are loaded
392                         if (!me.isLoaded()) {
393                             me.set('loaded', true);                            
394                         }
395                         // If this node didnt have any childnodes before, update myself
396                         else if (me.childNodes.length === 1) {
397                             me.set('loaded', me.isLoaded());
398                         }
399                         
400                         if (suppressEvents !== true) {
401                             me.fireEvent("append", me, node, index);
402
403                             if (oldParent) {
404                                 node.fireEvent("move", node, oldParent, me, index);
405                             }                            
406                         }
407
408                         return node;
409                     }
410                 },
411                 
412                 /**
413                  * Returns the bubble target for this node
414                  * @private
415                  * @return {Object} The bubble target
416                  */
417                 getBubbleTarget: function() {
418                     return this.parentNode;
419                 },
420
421                 /**
422                  * Removes a child node from this node.
423                  * @param {Node} node The node to remove
424                  * @param {Boolean} destroy <tt>true</tt> to destroy the node upon removal. Defaults to <tt>false</tt>.
425                  * @return {Node} The removed node
426                  */
427                 removeChild : function(node, destroy, suppressEvents, suppressNodeUpdate) {
428                     var me = this,
429                         index = me.indexOf(node);
430                     
431                     if (index == -1 || (suppressEvents !== true && me.fireEvent("beforeremove", me, node) === false)) {
432                         return false;
433                     }
434
435                     // remove it from childNodes collection
436                     me.childNodes.splice(index, 1);
437
438                     // update child refs
439                     if (me.firstChild == node) {
440                         me.setFirstChild(node.nextSibling);
441                     }
442                     if (me.lastChild == node) {
443                         me.setLastChild(node.previousSibling);
444                     }
445                     
446                     // update siblings
447                     if (node.previousSibling) {
448                         node.previousSibling.nextSibling = node.nextSibling;
449                         node.previousSibling.updateInfo(suppressNodeUpdate);
450                     }
451                     if (node.nextSibling) {
452                         node.nextSibling.previousSibling = node.previousSibling;
453                         node.nextSibling.updateInfo(suppressNodeUpdate);
454                     }
455
456                     if (suppressEvents !== true) {
457                         me.fireEvent("remove", me, node);
458                     }
459                     
460                     
461                     // If this node suddenly doesnt have childnodes anymore, update myself
462                     if (!me.childNodes.length) {
463                         me.set('loaded', me.isLoaded());
464                     }
465                     
466                     if (destroy) {
467                         node.destroy(true);
468                     } else {
469                         node.clear();
470                     }
471
472                     return node;
473                 },
474
475                 /**
476                  * Creates a copy (clone) of this Node.
477                  * @param {String} id (optional) A new id, defaults to this Node's id. See <code>{@link #id}</code>.
478                  * @param {Boolean} deep (optional) <p>If passed as <code>true</code>, all child Nodes are recursively copied into the new Node.</p>
479                  * <p>If omitted or false, the copy will have no child Nodes.</p>
480                  * @return {Node} A copy of this Node.
481                  */
482                 copy: function(newId, deep) {
483                     var me = this,
484                         result = me.callOverridden(arguments),
485                         len = me.childNodes ? me.childNodes.length : 0,
486                         i;
487
488                     // Move child nodes across to the copy if required
489                     if (deep) {
490                         for (i = 0; i < len; i++) {
491                             result.appendChild(me.childNodes[i].copy(true));
492                         }
493                     }
494                     return result;
495                 },
496
497                 /**
498                  * Clear the node.
499                  * @private
500                  * @param {Boolean} destroy True to destroy the node.
501                  */
502                 clear : function(destroy) {
503                     var me = this;
504                     
505                     // clear any references from the node
506                     me.parentNode = me.previousSibling = me.nextSibling = null;
507                     if (destroy) {
508                         me.firstChild = me.lastChild = null;
509                     }
510                 },
511
512                 /**
513                  * Destroys the node.
514                  */
515                 destroy : function(silent) {
516                     /*
517                      * Silent is to be used in a number of cases
518                      * 1) When setRoot is called.
519                      * 2) When destroy on the tree is called
520                      * 3) For destroying child nodes on a node
521                      */
522                     var me = this,
523                         options = me.destroyOptions;
524                     
525                     if (silent === true) {
526                         me.clear(true);
527                         Ext.each(me.childNodes, function(n) {
528                             n.destroy(true);
529                         });
530                         me.childNodes = null;
531                         delete me.destroyOptions;
532                         me.callOverridden([options]);
533                     } else {
534                         me.destroyOptions = silent;
535                         // overridden method will be called, since remove will end up calling destroy(true);
536                         me.remove(true);
537                     }
538                 },
539
540                 /**
541                  * Inserts the first node before the second node in this nodes childNodes collection.
542                  * @param {Node} node The node to insert
543                  * @param {Node} refNode The node to insert before (if null the node is appended)
544                  * @return {Node} The inserted node
545                  */
546                 insertBefore : function(node, refNode, suppressEvents) {
547                     var me = this,
548                         index     = me.indexOf(refNode),
549                         oldParent = node.parentNode,
550                         refIndex  = index,
551                         ps;
552                     
553                     if (!refNode) { // like standard Dom, refNode can be null for append
554                         return me.appendChild(node);
555                     }
556                     
557                     // nothing to do
558                     if (node == refNode) {
559                         return false;
560                     }
561
562                     // Make sure it is a record with the NodeInterface
563                     node = me.createNode(node);
564                     
565                     if (suppressEvents !== true && me.fireEvent("beforeinsert", me, node, refNode) === false) {
566                         return false;
567                     }
568                     
569                     // when moving internally, indexes will change after remove
570                     if (oldParent == me && me.indexOf(node) < index) {
571                         refIndex--;
572                     }
573
574                     // it's a move, make sure we move it cleanly
575                     if (oldParent) {
576                         if (suppressEvents !== true && node.fireEvent("beforemove", node, oldParent, me, index, refNode) === false) {
577                             return false;
578                         }
579                         oldParent.removeChild(node);
580                     }
581
582                     if (refIndex === 0) {
583                         me.setFirstChild(node);
584                     }
585
586                     me.childNodes.splice(refIndex, 0, node);
587                     node.parentNode = me;
588                     
589                     node.nextSibling = refNode;
590                     refNode.previousSibling = node;
591                     
592                     ps = me.childNodes[refIndex - 1];
593                     if (ps) {
594                         node.previousSibling = ps;
595                         ps.nextSibling = node;
596                         ps.updateInfo();
597                     } else {
598                         node.previousSibling = null;
599                     }
600                     
601                     node.updateInfo();
602                     
603                     if (!me.isLoaded()) {
604                         me.set('loaded', true);                            
605                     }    
606                     // If this node didnt have any childnodes before, update myself
607                     else if (me.childNodes.length === 1) {
608                         me.set('loaded', me.isLoaded());
609                     }
610
611                     if (suppressEvents !== true) {
612                         me.fireEvent("insert", me, node, refNode);
613
614                         if (oldParent) {
615                             node.fireEvent("move", node, oldParent, me, refIndex, refNode);
616                         }                        
617                     }
618
619                     return node;
620                 },
621                 
622                 /**
623                  * Insert a node into this node
624                  * @param {Number} index The zero-based index to insert the node at
625                  * @param {Ext.data.Model} node The node to insert
626                  * @return {Ext.data.Record} The record you just inserted
627                  */    
628                 insertChild: function(index, node) {
629                     var sibling = this.childNodes[index];
630                     if (sibling) {
631                         return this.insertBefore(node, sibling);
632                     }
633                     else {
634                         return this.appendChild(node);
635                     }
636                 },
637
638                 /**
639                  * Removes this node from its parent
640                  * @param {Boolean} destroy <tt>true</tt> to destroy the node upon removal. Defaults to <tt>false</tt>.
641                  * @return {Node} this
642                  */
643                 remove : function(destroy, suppressEvents) {
644                     var parentNode = this.parentNode;
645
646                     if (parentNode) {
647                         parentNode.removeChild(this, destroy, suppressEvents, true);
648                     }
649                     return this;
650                 },
651
652                 /**
653                  * Removes all child nodes from this node.
654                  * @param {Boolean} destroy <tt>true</tt> to destroy the node upon removal. Defaults to <tt>false</tt>.
655                  * @return {Node} this
656                  */
657                 removeAll : function(destroy, suppressEvents) {
658                     var cn = this.childNodes,
659                         n;
660
661                     while ((n = cn[0])) {
662                         this.removeChild(n, destroy, suppressEvents);
663                     }
664                     return this;
665                 },
666
667                 /**
668                  * Returns the child node at the specified index.
669                  * @param {Number} index
670                  * @return {Node}
671                  */
672                 getChildAt : function(index) {
673                     return this.childNodes[index];
674                 },
675
676                 /**
677                  * Replaces one child node in this node with another.
678                  * @param {Node} newChild The replacement node
679                  * @param {Node} oldChild The node to replace
680                  * @return {Node} The replaced node
681                  */
682                 replaceChild : function(newChild, oldChild, suppressEvents) {
683                     var s = oldChild ? oldChild.nextSibling : null;
684                     
685                     this.removeChild(oldChild, suppressEvents);
686                     this.insertBefore(newChild, s, suppressEvents);
687                     return oldChild;
688                 },
689
690                 /**
691                  * Returns the index of a child node
692                  * @param {Node} node
693                  * @return {Number} The index of the node or -1 if it was not found
694                  */
695                 indexOf : function(child) {
696                     return Ext.Array.indexOf(this.childNodes, child);
697                 },
698
699                 /**
700                  * Returns depth of this node (the root node has a depth of 0)
701                  * @return {Number}
702                  */
703                 getDepth : function() {
704                     return this.get('depth');
705                 },
706
707                 /**
708                  * Bubbles up the tree from this node, calling the specified function with each node. The arguments to the function
709                  * will be the args provided or the current node. If the function returns false at any point,
710                  * the bubble is stopped.
711                  * @param {Function} fn The function to call
712                  * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the function is executed. Defaults to the current Node.
713                  * @param {Array} args (optional) The args to call the function with (default to passing the current Node)
714                  */
715                 bubble : function(fn, scope, args) {
716                     var p = this;
717                     while (p) {
718                         if (fn.apply(scope || p, args || [p]) === false) {
719                             break;
720                         }
721                         p = p.parentNode;
722                     }
723                 },
724
725                 //<deprecated since=0.99>
726                 cascade: function() {
727                     if (Ext.isDefined(Ext.global.console)) {
728                         Ext.global.console.warn('Ext.data.Node: cascade has been deprecated. Please use cascadeBy instead.');
729                     }
730                     return this.cascadeBy.apply(this, arguments);
731                 },
732                 //</deprecated>
733
734                 /**
735                  * Cascades down the tree from this node, calling the specified function with each node. The arguments to the function
736                  * will be the args provided or the current node. If the function returns false at any point,
737                  * the cascade is stopped on that branch.
738                  * @param {Function} fn The function to call
739                  * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the function is executed. Defaults to the current Node.
740                  * @param {Array} args (optional) The args to call the function with (default to passing the current Node)
741                  */
742                 cascadeBy : function(fn, scope, args) {
743                     if (fn.apply(scope || this, args || [this]) !== false) {
744                         var childNodes = this.childNodes,
745                             length     = childNodes.length,
746                             i;
747
748                         for (i = 0; i < length; i++) {
749                             childNodes[i].cascadeBy(fn, scope, args);
750                         }
751                     }
752                 },
753
754                 /**
755                  * Interates the child nodes of this node, calling the specified function with each node. The arguments to the function
756                  * will be the args provided or the current node. If the function returns false at any point,
757                  * the iteration stops.
758                  * @param {Function} fn The function to call
759                  * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the function is executed. Defaults to the current Node in the iteration.
760                  * @param {Array} args (optional) The args to call the function with (default to passing the current Node)
761                  */
762                 eachChild : function(fn, scope, args) {
763                     var childNodes = this.childNodes,
764                         length     = childNodes.length,
765                         i;
766
767                     for (i = 0; i < length; i++) {
768                         if (fn.apply(scope || this, args || [childNodes[i]]) === false) {
769                             break;
770                         }
771                     }
772                 },
773
774                 /**
775                  * Finds the first child that has the attribute with the specified value.
776                  * @param {String} attribute The attribute name
777                  * @param {Mixed} value The value to search for
778                  * @param {Boolean} deep (Optional) True to search through nodes deeper than the immediate children
779                  * @return {Node} The found child or null if none was found
780                  */
781                 findChild : function(attribute, value, deep) {
782                     return this.findChildBy(function() {
783                         return this.get(attribute) == value;
784                     }, null, deep);
785                 },
786
787                 /**
788                  * Finds the first child by a custom function. The child matches if the function passed returns <code>true</code>.
789                  * @param {Function} fn A function which must return <code>true</code> if the passed Node is the required Node.
790                  * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the function is executed. Defaults to the Node being tested.
791                  * @param {Boolean} deep (Optional) True to search through nodes deeper than the immediate children
792                  * @return {Node} The found child or null if none was found
793                  */
794                 findChildBy : function(fn, scope, deep) {
795                     var cs = this.childNodes,
796                         len = cs.length,
797                         i = 0, n, res;
798
799                     for (; i < len; i++) {
800                         n = cs[i];
801                         if (fn.call(scope || n, n) === true) {
802                             return n;
803                         }
804                         else if (deep) {
805                             res = n.findChildBy(fn, scope, deep);
806                             if (res !== null) {
807                                 return res;
808                             }
809                         }
810                     }
811
812                     return null;
813                 },
814
815                 /**
816                  * Returns true if this node is an ancestor (at any point) of the passed node.
817                  * @param {Node} node
818                  * @return {Boolean}
819                  */
820                 contains : function(node) {
821                     return node.isAncestor(this);
822                 },
823
824                 /**
825                  * Returns true if the passed node is an ancestor (at any point) of this node.
826                  * @param {Node} node
827                  * @return {Boolean}
828                  */
829                 isAncestor : function(node) {
830                     var p = this.parentNode;
831                     while (p) {
832                         if (p == node) {
833                             return true;
834                         }
835                         p = p.parentNode;
836                     }
837                     return false;
838                 },
839
840                 /**
841                  * Sorts this nodes children using the supplied sort function.
842                  * @param {Function} fn A function which, when passed two Nodes, returns -1, 0 or 1 depending upon required sort order.
843                  * @param {Boolean} recursive Whether or not to apply this sort recursively
844                  * @param {Boolean} suppressEvent Set to true to not fire a sort event.
845                  */
846                 sort : function(sortFn, recursive, suppressEvent) {
847                     var cs  = this.childNodes,
848                         ln = cs.length,
849                         i, n;
850                     
851                     if (ln > 0) {
852                         Ext.Array.sort(cs, sortFn);
853                         for (i = 0; i < ln; i++) {
854                             n = cs[i];
855                             n.previousSibling = cs[i-1];
856                             n.nextSibling = cs[i+1];
857                         
858                             if (i === 0) {
859                                 this.setFirstChild(n);
860                                 n.updateInfo();
861                             }
862                             if (i == ln - 1) {
863                                 this.setLastChild(n);
864                                 n.updateInfo();
865                             }
866                             if (recursive && !n.isLeaf()) {
867                                 n.sort(sortFn, true, true);
868                             }
869                         }
870                         
871                         if (suppressEvent !== true) {
872                             this.fireEvent('sort', this, cs);
873                         }
874                     }
875                 },
876                         
877                 /**
878                  * Returns true if this node is expaned
879                  * @return {Boolean}
880                  */        
881                 isExpanded: function() {
882                     return this.get('expanded');
883                 },
884                 
885                 /**
886                  * Returns true if this node is loaded
887                  * @return {Boolean}
888                  */ 
889                 isLoaded: function() {
890                     return this.get('loaded');
891                 },
892
893                 /**
894                  * Returns true if this node is loading
895                  * @return {Boolean}
896                  */ 
897                 isLoading: function() {
898                     return this.get('loading');
899                 },
900                                 
901                 /**
902                  * Returns true if this node is the root node
903                  * @return {Boolean}
904                  */ 
905                 isRoot: function() {
906                     return !this.parentNode;
907                 },
908                 
909                 /**
910                  * Returns true if this node is visible
911                  * @return {Boolean}
912                  */ 
913                 isVisible: function() {
914                     var parent = this.parentNode;
915                     while (parent) {
916                         if (!parent.isExpanded()) {
917                             return false;
918                         }
919                         parent = parent.parentNode;
920                     }
921                     return true;
922                 },
923                 
924                 /**
925                  * Expand this node.
926                  * @param {Function} recursive (Optional) True to recursively expand all the children
927                  * @param {Function} callback (Optional) The function to execute once the expand completes
928                  * @param {Object} scope (Optional) The scope to run the callback in
929                  */
930                 expand: function(recursive, callback, scope) {
931                     var me = this;
932
933                     // all paths must call the callback (eventually) or things like
934                     // selectPath fail
935
936                     // First we start by checking if this node is a parent
937                     if (!me.isLeaf()) {
938                         // Now we check if this record is already expanding or expanded
939                         if (!me.isLoading() && !me.isExpanded()) {
940                             // The TreeStore actually listens for the beforeexpand method and checks
941                             // whether we have to asynchronously load the children from the server
942                             // first. Thats why we pass a callback function to the event that the
943                             // store can call once it has loaded and parsed all the children.
944                             me.fireEvent('beforeexpand', me, function(records) {
945                                 me.set('expanded', true); 
946                                 me.fireEvent('expand', me, me.childNodes, false);
947                                 
948                                 // Call the expandChildren method if recursive was set to true 
949                                 if (recursive) {
950                                     me.expandChildren(true, callback, scope);
951                                 }
952                                 else {
953                                     Ext.callback(callback, scope || me, [me.childNodes]);                                
954                                 }
955                             }, me);                            
956                         }
957                         // If it is is already expanded but we want to recursively expand then call expandChildren
958                         else if (recursive) {
959                             me.expandChildren(true, callback, scope);
960                         }
961                         else {
962                             Ext.callback(callback, scope || me, [me.childNodes]);
963                         }
964
965                         // TODO - if the node isLoading, we probably need to defer the
966                         // callback until it is loaded (e.g., selectPath would need us
967                         // to not make the callback until the childNodes exist).
968                     }
969                     // If it's not then we fire the callback right away
970                     else {
971                         Ext.callback(callback, scope || me); // leaf = no childNodes
972                     }
973                 },
974                 
975                 /**
976                  * Expand all the children of this node.
977                  * @param {Function} recursive (Optional) True to recursively expand all the children
978                  * @param {Function} callback (Optional) The function to execute once all the children are expanded
979                  * @param {Object} scope (Optional) The scope to run the callback in
980                  */
981                 expandChildren: function(recursive, callback, scope) {
982                     var me = this,
983                         i = 0,
984                         nodes = me.childNodes,
985                         ln = nodes.length,
986                         node,
987                         expanding = 0;
988
989                     for (; i < ln; ++i) {
990                         node = nodes[i];
991                         if (!node.isLeaf() && !node.isExpanded()) {
992                             expanding++;
993                             nodes[i].expand(recursive, function () {
994                                 expanding--;
995                                 if (callback && !expanding) {
996                                     Ext.callback(callback, scope || me, me.childNodes); 
997                                 }
998                             });                            
999                         }
1000                     }
1001                     
1002                     if (!expanding && callback) {
1003                         Ext.callback(callback, scope || me, me.childNodes);
1004                     }
1005                 },
1006
1007                 /**
1008                  * Collapse this node.
1009                  * @param {Function} recursive (Optional) True to recursively collapse all the children
1010                  * @param {Function} callback (Optional) The function to execute once the collapse completes
1011                  * @param {Object} scope (Optional) The scope to run the callback in
1012                  */
1013                 collapse: function(recursive, callback, scope) {
1014                     var me = this;
1015
1016                     // First we start by checking if this node is a parent
1017                     if (!me.isLeaf()) {
1018                         // Now we check if this record is already collapsing or collapsed
1019                         if (!me.collapsing && me.isExpanded()) {
1020                             me.fireEvent('beforecollapse', me, function(records) {
1021                                 me.set('expanded', false); 
1022                                 me.fireEvent('collapse', me, me.childNodes, false);
1023                                 
1024                                 // Call the collapseChildren method if recursive was set to true 
1025                                 if (recursive) {
1026                                     me.collapseChildren(true, callback, scope);
1027                                 }
1028                                 else {
1029                                     Ext.callback(callback, scope || me, [me.childNodes]);                                
1030                                 }
1031                             }, me);                            
1032                         }
1033                         // If it is is already collapsed but we want to recursively collapse then call collapseChildren
1034                         else if (recursive) {
1035                             me.collapseChildren(true, callback, scope);
1036                         }
1037                     }
1038                     // If it's not then we fire the callback right away
1039                     else {
1040                         Ext.callback(callback, scope || me, me.childNodes); 
1041                     }
1042                 },
1043                 
1044                 /**
1045                  * Collapse all the children of this node.
1046                  * @param {Function} recursive (Optional) True to recursively collapse all the children
1047                  * @param {Function} callback (Optional) The function to execute once all the children are collapsed
1048                  * @param {Object} scope (Optional) The scope to run the callback in
1049                  */
1050                 collapseChildren: function(recursive, callback, scope) {
1051                     var me = this,
1052                         i = 0,
1053                         nodes = me.childNodes,
1054                         ln = nodes.length,
1055                         node,
1056                         collapsing = 0;
1057
1058                     for (; i < ln; ++i) {
1059                         node = nodes[i];
1060                         if (!node.isLeaf() && node.isExpanded()) {
1061                             collapsing++;
1062                             nodes[i].collapse(recursive, function () {
1063                                 collapsing--;
1064                                 if (callback && !collapsing) {
1065                                     Ext.callback(callback, scope || me, me.childNodes); 
1066                                 }
1067                             });                            
1068                         }
1069                     }
1070                     
1071                     if (!collapsing && callback) {
1072                         Ext.callback(callback, scope || me, me.childNodes);
1073                     }
1074                 }
1075             };
1076         }
1077     }
1078 });