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