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