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