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.
8 Ext.define('Ext.data.NodeInterface', {
9 requires: ['Ext.data.Field'],
13 * This method allows you to decorate a Record's prototype to implement the NodeInterface.
14 * This adds a set of methods, new events, new properties and new fields on every Record
15 * with the same Model as the passed Record.
16 * @param {Ext.data.Record} record The Record you want to decorate the prototype of.
19 decorate: function(record) {
21 // Apply the methods and fields to the prototype
22 // @TODO: clean this up to use proper class system stuff
23 var mgr = Ext.ModelManager,
24 modelName = record.modelName,
25 modelClass = mgr.getModel(modelName),
26 idName = modelClass.prototype.idProperty,
27 instances = Ext.Array.filter(mgr.all.getArray(), function(item) {
28 return item.modelName == modelName;
30 iln = instances.length,
32 i, instance, jln, j, newField;
34 // Start by adding the NodeInterface methods to the Model's prototype
35 modelClass.override(this.getPrototypeBody());
36 newFields = this.applyFields(modelClass, [
37 {name: idName, type: 'string', defaultValue: null},
38 {name: 'parentId', type: 'string', defaultValue: null},
39 {name: 'index', type: 'int', defaultValue: null},
40 {name: 'depth', type: 'int', defaultValue: 0},
41 {name: 'expanded', type: 'bool', defaultValue: false, persist: false},
42 {name: 'checked', type: 'auto', defaultValue: null},
43 {name: 'leaf', type: 'bool', defaultValue: false, persist: false},
44 {name: 'cls', type: 'string', defaultValue: null, persist: false},
45 {name: 'iconCls', type: 'string', defaultValue: null, persist: false},
46 {name: 'root', type: 'boolean', defaultValue: false, persist: false},
47 {name: 'isLast', type: 'boolean', defaultValue: false, persist: false},
48 {name: 'isFirst', type: 'boolean', defaultValue: false, persist: false},
49 {name: 'allowDrop', type: 'boolean', defaultValue: true, persist: false},
50 {name: 'allowDrag', type: 'boolean', defaultValue: true, persist: false},
51 {name: 'loaded', type: 'boolean', defaultValue: false, persist: false},
52 {name: 'loading', type: 'boolean', defaultValue: false, persist: false},
53 {name: 'href', type: 'string', defaultValue: null, persist: false},
54 {name: 'hrefTarget',type: 'string', defaultValue: null, persist: false},
55 {name: 'qtip', type: 'string', defaultValue: null, persist: false},
56 {name: 'qtitle', type: 'string', defaultValue: null, persist: false}
59 jln = newFields.length;
60 // Set default values to all instances already out there
61 for (i = 0; i < iln; i++) {
62 instance = instances[i];
63 for (j = 0; j < jln; j++) {
64 newField = newFields[j];
65 if (instance.get(newField.name) === undefined) {
66 instance.data[newField.name] = newField.defaultValue;
76 previousSibling: null,
80 // Commit any fields so the record doesn't show as dirty initially
86 * Fires when a new child node is appended
87 * @param {Node} this This node
88 * @param {Node} node The newly appended node
89 * @param {Number} index The index of the newly appended node
95 * Fires when a child node is removed
96 * @param {Node} this This node
97 * @param {Node} node The removed node
103 * Fires when this node is moved to a new location in the tree
104 * @param {Node} this This node
105 * @param {Node} oldParent The old parent of this node
106 * @param {Node} newParent The new parent of this node
107 * @param {Number} index The index it was moved to
113 * Fires when a new child node is inserted.
114 * @param {Node} this This node
115 * @param {Node} node The child node inserted
116 * @param {Node} refNode The child node the node was inserted before
121 * @event beforeappend
122 * Fires before a new child is appended, return false to cancel the append.
123 * @param {Node} this This node
124 * @param {Node} node The child node to be appended
129 * @event beforeremove
130 * Fires before a child is removed, return false to cancel the remove.
131 * @param {Node} this This node
132 * @param {Node} node The child node to be removed
138 * Fires before this node is moved to a new location in the tree. Return false to cancel the move.
139 * @param {Node} this This node
140 * @param {Node} oldParent The parent of this node
141 * @param {Node} newParent The new parent this node is moving to
142 * @param {Number} index The index it is being moved to
147 * @event beforeinsert
148 * Fires before a new child is inserted, return false to cancel the insert.
149 * @param {Node} this This node
150 * @param {Node} node The child node to be inserted
151 * @param {Node} refNode The child node the node is being inserted before
157 * Fires when this node is expanded.
158 * @param {Node} this The expanding node
164 * Fires when this node is collapsed.
165 * @param {Node} this The collapsing node
170 * @event beforeexpand
171 * Fires before this node is expanded.
172 * @param {Node} this The expanding node
177 * @event beforecollapse
178 * Fires before this node is collapsed.
179 * @param {Node} this The collapsing node
184 * @event beforecollapse
185 * Fires before this node is collapsed.
186 * @param {Node} this The collapsing node
194 applyFields: function(modelClass, addFields) {
195 var modelPrototype = modelClass.prototype,
196 fields = modelPrototype.fields,
198 ln = addFields.length,
202 for (i = 0; i < ln; i++) {
203 addField = addFields[i];
204 if (!Ext.Array.contains(keys, addField.name)) {
205 addField = Ext.create('data.field', addField);
207 newFields.push(addField);
208 fields.add(addField);
215 getPrototypeBody: function() {
220 * Ensures that the passed object is an instance of a Record with the NodeInterface applied
223 createNode: function(node) {
224 if (Ext.isObject(node) && !node.isModel) {
225 node = Ext.ModelManager.create(node, this.modelName);
227 // Make sure the node implements the node interface
228 return Ext.data.NodeInterface.decorate(node);
232 * Returns true if this node is a leaf
235 isLeaf : function() {
236 return this.get('leaf') === true;
240 * Sets the first child of this node
242 * @param {Ext.data.NodeInterface} node
244 setFirstChild : function(node) {
245 this.firstChild = node;
249 * Sets the last child of this node
251 * @param {Ext.data.NodeInterface} node
253 setLastChild : function(node) {
254 this.lastChild = node;
258 * Updates general data of this node like isFirst, isLast, depth. This
259 * method is internally called after a node is moved. This shouldn't
260 * have to be called by the developer unless they are creating custom
264 updateInfo: function(silent) {
266 isRoot = me.isRoot(),
267 parentNode = me.parentNode,
268 isFirst = (!parentNode ? true : parentNode.firstChild == me),
269 isLast = (!parentNode ? true : parentNode.lastChild == me),
272 children = me.childNodes,
273 len = children.length,
276 while (parent.parentNode) {
278 parent = parent.parentNode;
286 index: parentNode ? parentNode.indexOf(me) : 0,
287 parentId: parentNode ? parentNode.getId() : null
294 for (i = 0; i < len; i++) {
295 children[i].updateInfo(silent);
300 * Returns true if this node is the last child of its parent
303 isLast : function() {
304 return this.get('isLast');
308 * Returns true if this node is the first child of its parent
311 isFirst : function() {
312 return this.get('isFirst');
316 * Returns true if this node has one or more child nodes, else false.
319 hasChildNodes : function() {
320 return !this.isLeaf() && this.childNodes.length > 0;
324 * Returns true if this node has one or more child nodes, or if the <tt>expandable</tt>
325 * node attribute is explicitly specified as true (see {@link #attributes}), otherwise returns false.
328 isExpandable : function() {
329 return this.get('expandable') || this.hasChildNodes();
333 * <p>Insert node(s) as the last child node of this node.</p>
334 * <p>If the node was previously a child node of another parent node, it will be removed from that node first.</p>
335 * @param {Node/Array} node The node or Array of nodes to append
336 * @return {Node} The appended node if single append, or null if an array was passed
338 appendChild : function(node, suppressEvents, suppressNodeUpdate) {
345 // if passed an array or multiple args do them one by one
346 if (Ext.isArray(node)) {
347 for (i = 0, ln = node.length; i < ln; i++) {
348 me.appendChild(node[i]);
351 // Make sure it is a record
352 node = me.createNode(node);
354 if (suppressEvents !== true && me.fireEvent("beforeappend", me, node) === false) {
358 index = me.childNodes.length;
359 oldParent = node.parentNode;
361 // it's a move, make sure we move it cleanly
363 if (suppressEvents !== true && node.fireEvent("beforemove", node, oldParent, me, index) === false) {
366 oldParent.removeChild(node, null, false, true);
369 index = me.childNodes.length;
371 me.setFirstChild(node);
374 me.childNodes.push(node);
375 node.parentNode = me;
376 node.nextSibling = null;
378 me.setLastChild(node);
380 ps = me.childNodes[index - 1];
382 node.previousSibling = ps;
383 ps.nextSibling = node;
384 ps.updateInfo(suppressNodeUpdate);
386 node.previousSibling = null;
389 node.updateInfo(suppressNodeUpdate);
391 // As soon as we append a child to this node, we are loaded
392 if (!me.isLoaded()) {
393 me.set('loaded', true);
395 // If this node didnt have any childnodes before, update myself
396 else if (me.childNodes.length === 1) {
397 me.set('loaded', me.isLoaded());
400 if (suppressEvents !== true) {
401 me.fireEvent("append", me, node, index);
404 node.fireEvent("move", node, oldParent, me, index);
413 * Returns the bubble target for this node
415 * @return {Object} The bubble target
417 getBubbleTarget: function() {
418 return this.parentNode;
422 * Removes a child node from this node.
423 * @param {Node} node The node to remove
424 * @param {Boolean} destroy <tt>true</tt> to destroy the node upon removal. Defaults to <tt>false</tt>.
425 * @return {Node} The removed node
427 removeChild : function(node, destroy, suppressEvents, suppressNodeUpdate) {
429 index = me.indexOf(node);
431 if (index == -1 || (suppressEvents !== true && me.fireEvent("beforeremove", me, node) === false)) {
435 // remove it from childNodes collection
436 me.childNodes.splice(index, 1);
439 if (me.firstChild == node) {
440 me.setFirstChild(node.nextSibling);
442 if (me.lastChild == node) {
443 me.setLastChild(node.previousSibling);
447 if (node.previousSibling) {
448 node.previousSibling.nextSibling = node.nextSibling;
449 node.previousSibling.updateInfo(suppressNodeUpdate);
451 if (node.nextSibling) {
452 node.nextSibling.previousSibling = node.previousSibling;
453 node.nextSibling.updateInfo(suppressNodeUpdate);
456 if (suppressEvents !== true) {
457 me.fireEvent("remove", me, node);
461 // If this node suddenly doesnt have childnodes anymore, update myself
462 if (!me.childNodes.length) {
463 me.set('loaded', me.isLoaded());
476 * Creates a copy (clone) of this Node.
477 * @param {String} id (optional) A new id, defaults to this Node's id. See <code>{@link #id}</code>.
478 * @param {Boolean} deep (optional) <p>If passed as <code>true</code>, all child Nodes are recursively copied into the new Node.</p>
479 * <p>If omitted or false, the copy will have no child Nodes.</p>
480 * @return {Node} A copy of this Node.
482 copy: function(newId, deep) {
484 result = me.callOverridden(arguments),
485 len = me.childNodes ? me.childNodes.length : 0,
488 // Move child nodes across to the copy if required
490 for (i = 0; i < len; i++) {
491 result.appendChild(me.childNodes[i].copy(true));
500 * @param {Boolean} destroy True to destroy the node.
502 clear : function(destroy) {
505 // clear any references from the node
506 me.parentNode = me.previousSibling = me.nextSibling = null;
508 me.firstChild = me.lastChild = null;
515 destroy : function(silent) {
517 * Silent is to be used in a number of cases
518 * 1) When setRoot is called.
519 * 2) When destroy on the tree is called
520 * 3) For destroying child nodes on a node
523 options = me.destroyOptions;
525 if (silent === true) {
527 Ext.each(me.childNodes, function(n) {
530 me.childNodes = null;
531 delete me.destroyOptions;
532 me.callOverridden([options]);
534 me.destroyOptions = silent;
535 // overridden method will be called, since remove will end up calling destroy(true);
541 * Inserts the first node before the second node in this nodes childNodes collection.
542 * @param {Node} node The node to insert
543 * @param {Node} refNode The node to insert before (if null the node is appended)
544 * @return {Node} The inserted node
546 insertBefore : function(node, refNode, suppressEvents) {
548 index = me.indexOf(refNode),
549 oldParent = node.parentNode,
553 if (!refNode) { // like standard Dom, refNode can be null for append
554 return me.appendChild(node);
558 if (node == refNode) {
562 // Make sure it is a record with the NodeInterface
563 node = me.createNode(node);
565 if (suppressEvents !== true && me.fireEvent("beforeinsert", me, node, refNode) === false) {
569 // when moving internally, indexes will change after remove
570 if (oldParent == me && me.indexOf(node) < index) {
574 // it's a move, make sure we move it cleanly
576 if (suppressEvents !== true && node.fireEvent("beforemove", node, oldParent, me, index, refNode) === false) {
579 oldParent.removeChild(node);
582 if (refIndex === 0) {
583 me.setFirstChild(node);
586 me.childNodes.splice(refIndex, 0, node);
587 node.parentNode = me;
589 node.nextSibling = refNode;
590 refNode.previousSibling = node;
592 ps = me.childNodes[refIndex - 1];
594 node.previousSibling = ps;
595 ps.nextSibling = node;
598 node.previousSibling = null;
603 if (!me.isLoaded()) {
604 me.set('loaded', true);
606 // If this node didnt have any childnodes before, update myself
607 else if (me.childNodes.length === 1) {
608 me.set('loaded', me.isLoaded());
611 if (suppressEvents !== true) {
612 me.fireEvent("insert", me, node, refNode);
615 node.fireEvent("move", node, oldParent, me, refIndex, refNode);
623 * Insert a node into this node
624 * @param {Number} index The zero-based index to insert the node at
625 * @param {Ext.data.Model} node The node to insert
626 * @return {Ext.data.Record} The record you just inserted
628 insertChild: function(index, node) {
629 var sibling = this.childNodes[index];
631 return this.insertBefore(node, sibling);
634 return this.appendChild(node);
639 * Removes this node from its parent
640 * @param {Boolean} destroy <tt>true</tt> to destroy the node upon removal. Defaults to <tt>false</tt>.
641 * @return {Node} this
643 remove : function(destroy, suppressEvents) {
644 var parentNode = this.parentNode;
647 parentNode.removeChild(this, destroy, suppressEvents, true);
653 * Removes all child nodes from this node.
654 * @param {Boolean} destroy <tt>true</tt> to destroy the node upon removal. Defaults to <tt>false</tt>.
655 * @return {Node} this
657 removeAll : function(destroy, suppressEvents) {
658 var cn = this.childNodes,
661 while ((n = cn[0])) {
662 this.removeChild(n, destroy, suppressEvents);
668 * Returns the child node at the specified index.
669 * @param {Number} index
672 getChildAt : function(index) {
673 return this.childNodes[index];
677 * Replaces one child node in this node with another.
678 * @param {Node} newChild The replacement node
679 * @param {Node} oldChild The node to replace
680 * @return {Node} The replaced node
682 replaceChild : function(newChild, oldChild, suppressEvents) {
683 var s = oldChild ? oldChild.nextSibling : null;
685 this.removeChild(oldChild, suppressEvents);
686 this.insertBefore(newChild, s, suppressEvents);
691 * Returns the index of a child node
693 * @return {Number} The index of the node or -1 if it was not found
695 indexOf : function(child) {
696 return Ext.Array.indexOf(this.childNodes, child);
700 * Returns depth of this node (the root node has a depth of 0)
703 getDepth : function() {
704 return this.get('depth');
708 * Bubbles up the tree from this node, calling the specified function with each node. The arguments to the function
709 * will be the args provided or the current node. If the function returns false at any point,
710 * the bubble is stopped.
711 * @param {Function} fn The function to call
712 * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the function is executed. Defaults to the current Node.
713 * @param {Array} args (optional) The args to call the function with (default to passing the current Node)
715 bubble : function(fn, scope, args) {
718 if (fn.apply(scope || p, args || [p]) === false) {
725 //<deprecated since=0.99>
726 cascade: function() {
727 if (Ext.isDefined(Ext.global.console)) {
728 Ext.global.console.warn('Ext.data.Node: cascade has been deprecated. Please use cascadeBy instead.');
730 return this.cascadeBy.apply(this, arguments);
735 * Cascades down the tree from this node, calling the specified function with each node. The arguments to the function
736 * will be the args provided or the current node. If the function returns false at any point,
737 * the cascade is stopped on that branch.
738 * @param {Function} fn The function to call
739 * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the function is executed. Defaults to the current Node.
740 * @param {Array} args (optional) The args to call the function with (default to passing the current Node)
742 cascadeBy : function(fn, scope, args) {
743 if (fn.apply(scope || this, args || [this]) !== false) {
744 var childNodes = this.childNodes,
745 length = childNodes.length,
748 for (i = 0; i < length; i++) {
749 childNodes[i].cascadeBy(fn, scope, args);
755 * Interates the child nodes of this node, calling the specified function with each node. The arguments to the function
756 * will be the args provided or the current node. If the function returns false at any point,
757 * the iteration stops.
758 * @param {Function} fn The function to call
759 * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the function is executed. Defaults to the current Node in the iteration.
760 * @param {Array} args (optional) The args to call the function with (default to passing the current Node)
762 eachChild : function(fn, scope, args) {
763 var childNodes = this.childNodes,
764 length = childNodes.length,
767 for (i = 0; i < length; i++) {
768 if (fn.apply(scope || this, args || [childNodes[i]]) === false) {
775 * Finds the first child that has the attribute with the specified value.
776 * @param {String} attribute The attribute name
777 * @param {Mixed} value The value to search for
778 * @param {Boolean} deep (Optional) True to search through nodes deeper than the immediate children
779 * @return {Node} The found child or null if none was found
781 findChild : function(attribute, value, deep) {
782 return this.findChildBy(function() {
783 return this.get(attribute) == value;
788 * Finds the first child by a custom function. The child matches if the function passed returns <code>true</code>.
789 * @param {Function} fn A function which must return <code>true</code> if the passed Node is the required Node.
790 * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the function is executed. Defaults to the Node being tested.
791 * @param {Boolean} deep (Optional) True to search through nodes deeper than the immediate children
792 * @return {Node} The found child or null if none was found
794 findChildBy : function(fn, scope, deep) {
795 var cs = this.childNodes,
799 for (; i < len; i++) {
801 if (fn.call(scope || n, n) === true) {
805 res = n.findChildBy(fn, scope, deep);
816 * Returns true if this node is an ancestor (at any point) of the passed node.
820 contains : function(node) {
821 return node.isAncestor(this);
825 * Returns true if the passed node is an ancestor (at any point) of this node.
829 isAncestor : function(node) {
830 var p = this.parentNode;
841 * Sorts this nodes children using the supplied sort function.
842 * @param {Function} fn A function which, when passed two Nodes, returns -1, 0 or 1 depending upon required sort order.
843 * @param {Boolean} recursive Whether or not to apply this sort recursively
844 * @param {Boolean} suppressEvent Set to true to not fire a sort event.
846 sort : function(sortFn, recursive, suppressEvent) {
847 var cs = this.childNodes,
852 Ext.Array.sort(cs, sortFn);
853 for (i = 0; i < ln; i++) {
855 n.previousSibling = cs[i-1];
856 n.nextSibling = cs[i+1];
859 this.setFirstChild(n);
863 this.setLastChild(n);
866 if (recursive && !n.isLeaf()) {
867 n.sort(sortFn, true, true);
871 if (suppressEvent !== true) {
872 this.fireEvent('sort', this, cs);
878 * Returns true if this node is expaned
881 isExpanded: function() {
882 return this.get('expanded');
886 * Returns true if this node is loaded
889 isLoaded: function() {
890 return this.get('loaded');
894 * Returns true if this node is loading
897 isLoading: function() {
898 return this.get('loading');
902 * Returns true if this node is the root node
906 return !this.parentNode;
910 * Returns true if this node is visible
913 isVisible: function() {
914 var parent = this.parentNode;
916 if (!parent.isExpanded()) {
919 parent = parent.parentNode;
926 * @param {Function} recursive (Optional) True to recursively expand all the children
927 * @param {Function} callback (Optional) The function to execute once the expand completes
928 * @param {Object} scope (Optional) The scope to run the callback in
930 expand: function(recursive, callback, scope) {
933 // all paths must call the callback (eventually) or things like
936 // First we start by checking if this node is a parent
938 // Now we check if this record is already expanding or expanded
939 if (!me.isLoading() && !me.isExpanded()) {
940 // The TreeStore actually listens for the beforeexpand method and checks
941 // whether we have to asynchronously load the children from the server
942 // first. Thats why we pass a callback function to the event that the
943 // store can call once it has loaded and parsed all the children.
944 me.fireEvent('beforeexpand', me, function(records) {
945 me.set('expanded', true);
946 me.fireEvent('expand', me, me.childNodes, false);
948 // Call the expandChildren method if recursive was set to true
950 me.expandChildren(true, callback, scope);
953 Ext.callback(callback, scope || me, [me.childNodes]);
957 // If it is is already expanded but we want to recursively expand then call expandChildren
958 else if (recursive) {
959 me.expandChildren(true, callback, scope);
962 Ext.callback(callback, scope || me, [me.childNodes]);
965 // TODO - if the node isLoading, we probably need to defer the
966 // callback until it is loaded (e.g., selectPath would need us
967 // to not make the callback until the childNodes exist).
969 // If it's not then we fire the callback right away
971 Ext.callback(callback, scope || me); // leaf = no childNodes
976 * Expand all the children of this node.
977 * @param {Function} recursive (Optional) True to recursively expand all the children
978 * @param {Function} callback (Optional) The function to execute once all the children are expanded
979 * @param {Object} scope (Optional) The scope to run the callback in
981 expandChildren: function(recursive, callback, scope) {
984 nodes = me.childNodes,
989 for (; i < ln; ++i) {
991 if (!node.isLeaf() && !node.isExpanded()) {
993 nodes[i].expand(recursive, function () {
995 if (callback && !expanding) {
996 Ext.callback(callback, scope || me, me.childNodes);
1002 if (!expanding && callback) {
1003 Ext.callback(callback, scope || me, me.childNodes);
1008 * Collapse this node.
1009 * @param {Function} recursive (Optional) True to recursively collapse all the children
1010 * @param {Function} callback (Optional) The function to execute once the collapse completes
1011 * @param {Object} scope (Optional) The scope to run the callback in
1013 collapse: function(recursive, callback, scope) {
1016 // First we start by checking if this node is a parent
1018 // Now we check if this record is already collapsing or collapsed
1019 if (!me.collapsing && me.isExpanded()) {
1020 me.fireEvent('beforecollapse', me, function(records) {
1021 me.set('expanded', false);
1022 me.fireEvent('collapse', me, me.childNodes, false);
1024 // Call the collapseChildren method if recursive was set to true
1026 me.collapseChildren(true, callback, scope);
1029 Ext.callback(callback, scope || me, [me.childNodes]);
1033 // If it is is already collapsed but we want to recursively collapse then call collapseChildren
1034 else if (recursive) {
1035 me.collapseChildren(true, callback, scope);
1038 // If it's not then we fire the callback right away
1040 Ext.callback(callback, scope || me, me.childNodes);
1045 * Collapse all the children of this node.
1046 * @param {Function} recursive (Optional) True to recursively collapse all the children
1047 * @param {Function} callback (Optional) The function to execute once all the children are collapsed
1048 * @param {Object} scope (Optional) The scope to run the callback in
1050 collapseChildren: function(recursive, callback, scope) {
1053 nodes = me.childNodes,
1058 for (; i < ln; ++i) {
1060 if (!node.isLeaf() && node.isExpanded()) {
1062 nodes[i].collapse(recursive, function () {
1064 if (callback && !collapsing) {
1065 Ext.callback(callback, scope || me, me.childNodes);
1071 if (!collapsing && callback) {
1072 Ext.callback(callback, scope || me, me.childNodes);