X-Git-Url: http://git.ithinksw.org/extjs.git/blobdiff_plain/0494b8d9b9bb03ab6c22b34dae81261e3cd7e3e6..7a654f8d43fdb43d78b63d90528bed6e86b608cc:/src/data/NodeInterface.js diff --git a/src/data/NodeInterface.js b/src/data/NodeInterface.js new file mode 100644 index 00000000..16a324d6 --- /dev/null +++ b/src/data/NodeInterface.js @@ -0,0 +1,1075 @@ +/** + * @class Ext.data.NodeInterface + * This class is meant to be used as a set of methods that are applied to the prototype of a + * Record to decorate it with a Node API. This means that models used in conjunction with a tree + * will have all of the tree related methods available on the model. In general this class will + * not be used directly by the developer. + */ +Ext.define('Ext.data.NodeInterface', { + requires: ['Ext.data.Field'], + + statics: { + /** + * This method allows you to decorate a Record's prototype to implement the NodeInterface. + * This adds a set of methods, new events, new properties and new fields on every Record + * with the same Model as the passed Record. + * @param {Ext.data.Record} record The Record you want to decorate the prototype of. + * @static + */ + decorate: function(record) { + if (!record.isNode) { + // Apply the methods and fields to the prototype + // @TODO: clean this up to use proper class system stuff + var mgr = Ext.ModelManager, + modelName = record.modelName, + modelClass = mgr.getModel(modelName), + idName = modelClass.prototype.idProperty, + instances = Ext.Array.filter(mgr.all.getArray(), function(item) { + return item.modelName == modelName; + }), + iln = instances.length, + newFields = [], + i, instance, jln, j, newField; + + // Start by adding the NodeInterface methods to the Model's prototype + modelClass.override(this.getPrototypeBody()); + newFields = this.applyFields(modelClass, [ + {name: idName, type: 'string', defaultValue: null}, + {name: 'parentId', type: 'string', defaultValue: null}, + {name: 'index', type: 'int', defaultValue: null}, + {name: 'depth', type: 'int', defaultValue: 0}, + {name: 'expanded', type: 'bool', defaultValue: false, persist: false}, + {name: 'checked', type: 'auto', defaultValue: null}, + {name: 'leaf', type: 'bool', defaultValue: false, persist: false}, + {name: 'cls', type: 'string', defaultValue: null, persist: false}, + {name: 'iconCls', type: 'string', defaultValue: null, persist: false}, + {name: 'root', type: 'boolean', defaultValue: false, persist: false}, + {name: 'isLast', type: 'boolean', defaultValue: false, persist: false}, + {name: 'isFirst', type: 'boolean', defaultValue: false, persist: false}, + {name: 'allowDrop', type: 'boolean', defaultValue: true, persist: false}, + {name: 'allowDrag', type: 'boolean', defaultValue: true, persist: false}, + {name: 'loaded', type: 'boolean', defaultValue: false, persist: false}, + {name: 'loading', type: 'boolean', defaultValue: false, persist: false}, + {name: 'href', type: 'string', defaultValue: null, persist: false}, + {name: 'hrefTarget',type: 'string', defaultValue: null, persist: false}, + {name: 'qtip', type: 'string', defaultValue: null, persist: false}, + {name: 'qtitle', type: 'string', defaultValue: null, persist: false} + ]); + + jln = newFields.length; + // Set default values to all instances already out there + for (i = 0; i < iln; i++) { + instance = instances[i]; + for (j = 0; j < jln; j++) { + newField = newFields[j]; + if (instance.get(newField.name) === undefined) { + instance.data[newField.name] = newField.defaultValue; + } + } + } + } + + Ext.applyIf(record, { + firstChild: null, + lastChild: null, + parentNode: null, + previousSibling: null, + nextSibling: null, + childNodes: [] + }); + // Commit any fields so the record doesn't show as dirty initially + record.commit(true); + + record.enableBubble([ + /** + * @event append + * Fires when a new child node is appended + * @param {Node} this This node + * @param {Node} node The newly appended node + * @param {Number} index The index of the newly appended node + */ + "append", + + /** + * @event remove + * Fires when a child node is removed + * @param {Node} this This node + * @param {Node} node The removed node + */ + "remove", + + /** + * @event move + * Fires when this node is moved to a new location in the tree + * @param {Node} this This node + * @param {Node} oldParent The old parent of this node + * @param {Node} newParent The new parent of this node + * @param {Number} index The index it was moved to + */ + "move", + + /** + * @event insert + * Fires when a new child node is inserted. + * @param {Node} this This node + * @param {Node} node The child node inserted + * @param {Node} refNode The child node the node was inserted before + */ + "insert", + + /** + * @event beforeappend + * Fires before a new child is appended, return false to cancel the append. + * @param {Node} this This node + * @param {Node} node The child node to be appended + */ + "beforeappend", + + /** + * @event beforeremove + * Fires before a child is removed, return false to cancel the remove. + * @param {Node} this This node + * @param {Node} node The child node to be removed + */ + "beforeremove", + + /** + * @event beforemove + * Fires before this node is moved to a new location in the tree. Return false to cancel the move. + * @param {Node} this This node + * @param {Node} oldParent The parent of this node + * @param {Node} newParent The new parent this node is moving to + * @param {Number} index The index it is being moved to + */ + "beforemove", + + /** + * @event beforeinsert + * Fires before a new child is inserted, return false to cancel the insert. + * @param {Node} this This node + * @param {Node} node The child node to be inserted + * @param {Node} refNode The child node the node is being inserted before + */ + "beforeinsert", + + /** + * @event expand + * Fires when this node is expanded. + * @param {Node} this The expanding node + */ + "expand", + + /** + * @event collapse + * Fires when this node is collapsed. + * @param {Node} this The collapsing node + */ + "collapse", + + /** + * @event beforeexpand + * Fires before this node is expanded. + * @param {Node} this The expanding node + */ + "beforeexpand", + + /** + * @event beforecollapse + * Fires before this node is collapsed. + * @param {Node} this The collapsing node + */ + "beforecollapse", + + /** + * @event beforecollapse + * Fires before this node is collapsed. + * @param {Node} this The collapsing node + */ + "sort" + ]); + + return record; + }, + + applyFields: function(modelClass, addFields) { + var modelPrototype = modelClass.prototype, + fields = modelPrototype.fields, + keys = fields.keys, + ln = addFields.length, + addField, i, name, + newFields = []; + + for (i = 0; i < ln; i++) { + addField = addFields[i]; + if (!Ext.Array.contains(keys, addField.name)) { + addField = Ext.create('data.field', addField); + + newFields.push(addField); + fields.add(addField); + } + } + + return newFields; + }, + + getPrototypeBody: function() { + return { + isNode: true, + + /** + * Ensures that the passed object is an instance of a Record with the NodeInterface applied + * @return {Boolean} + */ + createNode: function(node) { + if (Ext.isObject(node) && !node.isModel) { + node = Ext.ModelManager.create(node, this.modelName); + } + // Make sure the node implements the node interface + return Ext.data.NodeInterface.decorate(node); + }, + + /** + * Returns true if this node is a leaf + * @return {Boolean} + */ + isLeaf : function() { + return this.get('leaf') === true; + }, + + /** + * Sets the first child of this node + * @private + * @param {Ext.data.NodeInterface} node + */ + setFirstChild : function(node) { + this.firstChild = node; + }, + + /** + * Sets the last child of this node + * @private + * @param {Ext.data.NodeInterface} node + */ + setLastChild : function(node) { + this.lastChild = node; + }, + + /** + * Updates general data of this node like isFirst, isLast, depth. This + * method is internally called after a node is moved. This shouldn't + * have to be called by the developer unless they are creating custom + * Tree plugins. + * @return {Boolean} + */ + updateInfo: function(silent) { + var me = this, + isRoot = me.isRoot(), + parentNode = me.parentNode, + isFirst = (!parentNode ? true : parentNode.firstChild == me), + isLast = (!parentNode ? true : parentNode.lastChild == me), + depth = 0, + parent = me, + children = me.childNodes, + len = children.length, + i = 0; + + while (parent.parentNode) { + ++depth; + parent = parent.parentNode; + } + + me.beginEdit(); + me.set({ + isFirst: isFirst, + isLast: isLast, + depth: depth, + index: parentNode ? parentNode.indexOf(me) : 0, + parentId: parentNode ? parentNode.getId() : null + }); + me.endEdit(silent); + if (silent) { + me.commit(); + } + + for (i = 0; i < len; i++) { + children[i].updateInfo(silent); + } + }, + + /** + * Returns true if this node is the last child of its parent + * @return {Boolean} + */ + isLast : function() { + return this.get('isLast'); + }, + + /** + * Returns true if this node is the first child of its parent + * @return {Boolean} + */ + isFirst : function() { + return this.get('isFirst'); + }, + + /** + * Returns true if this node has one or more child nodes, else false. + * @return {Boolean} + */ + hasChildNodes : function() { + return !this.isLeaf() && this.childNodes.length > 0; + }, + + /** + * Returns true if this node has one or more child nodes, or if the expandable + * node attribute is explicitly specified as true (see {@link #attributes}), otherwise returns false. + * @return {Boolean} + */ + isExpandable : function() { + return this.get('expandable') || this.hasChildNodes(); + }, + + /** + *

Insert node(s) as the last child node of this node.

+ *

If the node was previously a child node of another parent node, it will be removed from that node first.

+ * @param {Node/Array} node The node or Array of nodes to append + * @return {Node} The appended node if single append, or null if an array was passed + */ + appendChild : function(node, suppressEvents, suppressNodeUpdate) { + var me = this, + i, ln, + index, + oldParent, + ps; + + // if passed an array or multiple args do them one by one + if (Ext.isArray(node)) { + for (i = 0, ln = node.length; i < ln; i++) { + me.appendChild(node[i]); + } + } else { + // Make sure it is a record + node = me.createNode(node); + + if (suppressEvents !== true && me.fireEvent("beforeappend", me, node) === false) { + return false; + } + + index = me.childNodes.length; + oldParent = node.parentNode; + + // it's a move, make sure we move it cleanly + if (oldParent) { + if (suppressEvents !== true && node.fireEvent("beforemove", node, oldParent, me, index) === false) { + return false; + } + oldParent.removeChild(node, null, false, true); + } + + index = me.childNodes.length; + if (index === 0) { + me.setFirstChild(node); + } + + me.childNodes.push(node); + node.parentNode = me; + node.nextSibling = null; + + me.setLastChild(node); + + ps = me.childNodes[index - 1]; + if (ps) { + node.previousSibling = ps; + ps.nextSibling = node; + ps.updateInfo(suppressNodeUpdate); + } else { + node.previousSibling = null; + } + + node.updateInfo(suppressNodeUpdate); + + // As soon as we append a child to this node, we are loaded + if (!me.isLoaded()) { + me.set('loaded', true); + } + // If this node didnt have any childnodes before, update myself + else if (me.childNodes.length === 1) { + me.set('loaded', me.isLoaded()); + } + + if (suppressEvents !== true) { + me.fireEvent("append", me, node, index); + + if (oldParent) { + node.fireEvent("move", node, oldParent, me, index); + } + } + + return node; + } + }, + + /** + * Returns the bubble target for this node + * @private + * @return {Object} The bubble target + */ + getBubbleTarget: function() { + return this.parentNode; + }, + + /** + * Removes a child node from this node. + * @param {Node} node The node to remove + * @param {Boolean} destroy true to destroy the node upon removal. Defaults to false. + * @return {Node} The removed node + */ + removeChild : function(node, destroy, suppressEvents, suppressNodeUpdate) { + var me = this, + index = me.indexOf(node); + + if (index == -1 || (suppressEvents !== true && me.fireEvent("beforeremove", me, node) === false)) { + return false; + } + + // remove it from childNodes collection + me.childNodes.splice(index, 1); + + // update child refs + if (me.firstChild == node) { + me.setFirstChild(node.nextSibling); + } + if (me.lastChild == node) { + me.setLastChild(node.previousSibling); + } + + // update siblings + if (node.previousSibling) { + node.previousSibling.nextSibling = node.nextSibling; + node.previousSibling.updateInfo(suppressNodeUpdate); + } + if (node.nextSibling) { + node.nextSibling.previousSibling = node.previousSibling; + node.nextSibling.updateInfo(suppressNodeUpdate); + } + + if (suppressEvents !== true) { + me.fireEvent("remove", me, node); + } + + + // If this node suddenly doesnt have childnodes anymore, update myself + if (!me.childNodes.length) { + me.set('loaded', me.isLoaded()); + } + + if (destroy) { + node.destroy(true); + } else { + node.clear(); + } + + return node; + }, + + /** + * Creates a copy (clone) of this Node. + * @param {String} id (optional) A new id, defaults to this Node's id. See {@link #id}. + * @param {Boolean} deep (optional)

If passed as true, all child Nodes are recursively copied into the new Node.

+ *

If omitted or false, the copy will have no child Nodes.

+ * @return {Node} A copy of this Node. + */ + copy: function(newId, deep) { + var me = this, + result = me.callOverridden(arguments), + len = me.childNodes ? me.childNodes.length : 0, + i; + + // Move child nodes across to the copy if required + if (deep) { + for (i = 0; i < len; i++) { + result.appendChild(me.childNodes[i].copy(true)); + } + } + return result; + }, + + /** + * Clear the node. + * @private + * @param {Boolean} destroy True to destroy the node. + */ + clear : function(destroy) { + var me = this; + + // clear any references from the node + me.parentNode = me.previousSibling = me.nextSibling = null; + if (destroy) { + me.firstChild = me.lastChild = null; + } + }, + + /** + * Destroys the node. + */ + destroy : function(silent) { + /* + * Silent is to be used in a number of cases + * 1) When setRoot is called. + * 2) When destroy on the tree is called + * 3) For destroying child nodes on a node + */ + var me = this; + + if (silent === true) { + me.clear(true); + Ext.each(me.childNodes, function(n) { + n.destroy(true); + }); + me.childNodes = null; + } else { + me.remove(true); + } + + me.callOverridden(); + }, + + /** + * Inserts the first node before the second node in this nodes childNodes collection. + * @param {Node} node The node to insert + * @param {Node} refNode The node to insert before (if null the node is appended) + * @return {Node} The inserted node + */ + insertBefore : function(node, refNode, suppressEvents) { + var me = this, + index = me.indexOf(refNode), + oldParent = node.parentNode, + refIndex = index, + ps; + + if (!refNode) { // like standard Dom, refNode can be null for append + return me.appendChild(node); + } + + // nothing to do + if (node == refNode) { + return false; + } + + // Make sure it is a record with the NodeInterface + node = me.createNode(node); + + if (suppressEvents !== true && me.fireEvent("beforeinsert", me, node, refNode) === false) { + return false; + } + + // when moving internally, indexes will change after remove + if (oldParent == me && me.indexOf(node) < index) { + refIndex--; + } + + // it's a move, make sure we move it cleanly + if (oldParent) { + if (suppressEvents !== true && node.fireEvent("beforemove", node, oldParent, me, index, refNode) === false) { + return false; + } + oldParent.removeChild(node); + } + + if (refIndex === 0) { + me.setFirstChild(node); + } + + me.childNodes.splice(refIndex, 0, node); + node.parentNode = me; + + node.nextSibling = refNode; + refNode.previousSibling = node; + + ps = me.childNodes[refIndex - 1]; + if (ps) { + node.previousSibling = ps; + ps.nextSibling = node; + ps.updateInfo(); + } else { + node.previousSibling = null; + } + + node.updateInfo(); + + if (!me.isLoaded()) { + me.set('loaded', true); + } + // If this node didnt have any childnodes before, update myself + else if (me.childNodes.length === 1) { + me.set('loaded', me.isLoaded()); + } + + if (suppressEvents !== true) { + me.fireEvent("insert", me, node, refNode); + + if (oldParent) { + node.fireEvent("move", node, oldParent, me, refIndex, refNode); + } + } + + return node; + }, + + /** + * Insert a node into this node + * @param {Number} index The zero-based index to insert the node at + * @param {Ext.data.Model} node The node to insert + * @return {Ext.data.Record} The record you just inserted + */ + insertChild: function(index, node) { + var sibling = this.childNodes[index]; + if (sibling) { + return this.insertBefore(node, sibling); + } + else { + return this.appendChild(node); + } + }, + + /** + * Removes this node from its parent + * @param {Boolean} destroy true to destroy the node upon removal. Defaults to false. + * @return {Node} this + */ + remove : function(destroy, suppressEvents) { + var parentNode = this.parentNode; + + if (parentNode) { + parentNode.removeChild(this, destroy, suppressEvents, true); + } + return this; + }, + + /** + * Removes all child nodes from this node. + * @param {Boolean} destroy true to destroy the node upon removal. Defaults to false. + * @return {Node} this + */ + removeAll : function(destroy, suppressEvents) { + var cn = this.childNodes, + n; + + while ((n = cn[0])) { + this.removeChild(n, destroy, suppressEvents); + } + return this; + }, + + /** + * Returns the child node at the specified index. + * @param {Number} index + * @return {Node} + */ + getChildAt : function(index) { + return this.childNodes[index]; + }, + + /** + * Replaces one child node in this node with another. + * @param {Node} newChild The replacement node + * @param {Node} oldChild The node to replace + * @return {Node} The replaced node + */ + replaceChild : function(newChild, oldChild, suppressEvents) { + var s = oldChild ? oldChild.nextSibling : null; + + this.removeChild(oldChild, suppressEvents); + this.insertBefore(newChild, s, suppressEvents); + return oldChild; + }, + + /** + * Returns the index of a child node + * @param {Node} node + * @return {Number} The index of the node or -1 if it was not found + */ + indexOf : function(child) { + return Ext.Array.indexOf(this.childNodes, child); + }, + + /** + * Returns depth of this node (the root node has a depth of 0) + * @return {Number} + */ + getDepth : function() { + return this.get('depth'); + }, + + /** + * Bubbles up the tree from this node, calling the specified function with each node. The arguments to the function + * will be the args provided or the current node. If the function returns false at any point, + * the bubble is stopped. + * @param {Function} fn The function to call + * @param {Object} scope (optional) The scope (this reference) in which the function is executed. Defaults to the current Node. + * @param {Array} args (optional) The args to call the function with (default to passing the current Node) + */ + bubble : function(fn, scope, args) { + var p = this; + while (p) { + if (fn.apply(scope || p, args || [p]) === false) { + break; + } + p = p.parentNode; + } + }, + + // + cascade: function() { + if (Ext.isDefined(Ext.global.console)) { + Ext.global.console.warn('Ext.data.Node: cascade has been deprecated. Please use cascadeBy instead.'); + } + return this.cascadeBy.apply(this, arguments); + }, + // + + /** + * Cascades down the tree from this node, calling the specified function with each node. The arguments to the function + * will be the args provided or the current node. If the function returns false at any point, + * the cascade is stopped on that branch. + * @param {Function} fn The function to call + * @param {Object} scope (optional) The scope (this reference) in which the function is executed. Defaults to the current Node. + * @param {Array} args (optional) The args to call the function with (default to passing the current Node) + */ + cascadeBy : function(fn, scope, args) { + if (fn.apply(scope || this, args || [this]) !== false) { + var childNodes = this.childNodes, + length = childNodes.length, + i; + + for (i = 0; i < length; i++) { + childNodes[i].cascadeBy(fn, scope, args); + } + } + }, + + /** + * Interates the child nodes of this node, calling the specified function with each node. The arguments to the function + * will be the args provided or the current node. If the function returns false at any point, + * the iteration stops. + * @param {Function} fn The function to call + * @param {Object} scope (optional) The scope (this reference) in which the function is executed. Defaults to the current Node in the iteration. + * @param {Array} args (optional) The args to call the function with (default to passing the current Node) + */ + eachChild : function(fn, scope, args) { + var childNodes = this.childNodes, + length = childNodes.length, + i; + + for (i = 0; i < length; i++) { + if (fn.apply(scope || this, args || [childNodes[i]]) === false) { + break; + } + } + }, + + /** + * Finds the first child that has the attribute with the specified value. + * @param {String} attribute The attribute name + * @param {Mixed} value The value to search for + * @param {Boolean} deep (Optional) True to search through nodes deeper than the immediate children + * @return {Node} The found child or null if none was found + */ + findChild : function(attribute, value, deep) { + return this.findChildBy(function() { + return this.get(attribute) == value; + }, null, deep); + }, + + /** + * Finds the first child by a custom function. The child matches if the function passed returns true. + * @param {Function} fn A function which must return true if the passed Node is the required Node. + * @param {Object} scope (optional) The scope (this reference) in which the function is executed. Defaults to the Node being tested. + * @param {Boolean} deep (Optional) True to search through nodes deeper than the immediate children + * @return {Node} The found child or null if none was found + */ + findChildBy : function(fn, scope, deep) { + var cs = this.childNodes, + len = cs.length, + i = 0, n, res; + + for (; i < len; i++) { + n = cs[i]; + if (fn.call(scope || n, n) === true) { + return n; + } + else if (deep) { + res = n.findChildBy(fn, scope, deep); + if (res !== null) { + return res; + } + } + } + + return null; + }, + + /** + * Returns true if this node is an ancestor (at any point) of the passed node. + * @param {Node} node + * @return {Boolean} + */ + contains : function(node) { + return node.isAncestor(this); + }, + + /** + * Returns true if the passed node is an ancestor (at any point) of this node. + * @param {Node} node + * @return {Boolean} + */ + isAncestor : function(node) { + var p = this.parentNode; + while (p) { + if (p == node) { + return true; + } + p = p.parentNode; + } + return false; + }, + + /** + * Sorts this nodes children using the supplied sort function. + * @param {Function} fn A function which, when passed two Nodes, returns -1, 0 or 1 depending upon required sort order. + * @param {Boolean} recursive Whether or not to apply this sort recursively + * @param {Boolean} suppressEvent Set to true to not fire a sort event. + */ + sort : function(sortFn, recursive, suppressEvent) { + var cs = this.childNodes, + ln = cs.length, + i, n; + + if (ln > 0) { + Ext.Array.sort(cs, sortFn); + for (i = 0; i < ln; i++) { + n = cs[i]; + n.previousSibling = cs[i-1]; + n.nextSibling = cs[i+1]; + + if (i === 0) { + this.setFirstChild(n); + n.updateInfo(); + } + if (i == ln - 1) { + this.setLastChild(n); + n.updateInfo(); + } + if (recursive && !n.isLeaf()) { + n.sort(sortFn, true, true); + } + } + + if (suppressEvent !== true) { + this.fireEvent('sort', this, cs); + } + } + }, + + /** + * Returns true if this node is expaned + * @return {Boolean} + */ + isExpanded: function() { + return this.get('expanded'); + }, + + /** + * Returns true if this node is loaded + * @return {Boolean} + */ + isLoaded: function() { + return this.get('loaded'); + }, + + /** + * Returns true if this node is loading + * @return {Boolean} + */ + isLoading: function() { + return this.get('loading'); + }, + + /** + * Returns true if this node is the root node + * @return {Boolean} + */ + isRoot: function() { + return !this.parentNode; + }, + + /** + * Returns true if this node is visible + * @return {Boolean} + */ + isVisible: function() { + var parent = this.parentNode; + while (parent) { + if (!parent.isExpanded()) { + return false; + } + parent = parent.parentNode; + } + return true; + }, + + /** + * Expand this node. + * @param {Function} recursive (Optional) True to recursively expand all the children + * @param {Function} callback (Optional) The function to execute once the expand completes + * @param {Object} scope (Optional) The scope to run the callback in + */ + expand: function(recursive, callback, scope) { + var me = this; + + // all paths must call the callback (eventually) or things like + // selectPath fail + + // First we start by checking if this node is a parent + if (!me.isLeaf()) { + // Now we check if this record is already expanding or expanded + if (!me.isLoading() && !me.isExpanded()) { + // The TreeStore actually listens for the beforeexpand method and checks + // whether we have to asynchronously load the children from the server + // first. Thats why we pass a callback function to the event that the + // store can call once it has loaded and parsed all the children. + me.fireEvent('beforeexpand', me, function(records) { + me.set('expanded', true); + me.fireEvent('expand', me, me.childNodes, false); + + // Call the expandChildren method if recursive was set to true + if (recursive) { + me.expandChildren(true, callback, scope); + } + else { + Ext.callback(callback, scope || me, [me.childNodes]); + } + }, me); + } + // If it is is already expanded but we want to recursively expand then call expandChildren + else if (recursive) { + me.expandChildren(true, callback, scope); + } + else { + Ext.callback(callback, scope || me, [me.childNodes]); + } + + // TODO - if the node isLoading, we probably need to defer the + // callback until it is loaded (e.g., selectPath would need us + // to not make the callback until the childNodes exist). + } + // If it's not then we fire the callback right away + else { + Ext.callback(callback, scope || me); // leaf = no childNodes + } + }, + + /** + * Expand all the children of this node. + * @param {Function} recursive (Optional) True to recursively expand all the children + * @param {Function} callback (Optional) The function to execute once all the children are expanded + * @param {Object} scope (Optional) The scope to run the callback in + */ + expandChildren: function(recursive, callback, scope) { + var me = this, + i = 0, + nodes = me.childNodes, + ln = nodes.length, + node, + expanding = 0; + + for (; i < ln; ++i) { + node = nodes[i]; + if (!node.isLeaf() && !node.isExpanded()) { + expanding++; + nodes[i].expand(recursive, function () { + expanding--; + if (callback && !expanding) { + Ext.callback(callback, scope || me, me.childNodes); + } + }); + } + } + + if (!expanding && callback) { + Ext.callback(callback, scope || me, me.childNodes); + } + }, + + /** + * Collapse this node. + * @param {Function} recursive (Optional) True to recursively collapse all the children + * @param {Function} callback (Optional) The function to execute once the collapse completes + * @param {Object} scope (Optional) The scope to run the callback in + */ + collapse: function(recursive, callback, scope) { + var me = this; + + // First we start by checking if this node is a parent + if (!me.isLeaf()) { + // Now we check if this record is already collapsing or collapsed + if (!me.collapsing && me.isExpanded()) { + me.fireEvent('beforecollapse', me, function(records) { + me.set('expanded', false); + me.fireEvent('collapse', me, me.childNodes, false); + + // Call the collapseChildren method if recursive was set to true + if (recursive) { + me.collapseChildren(true, callback, scope); + } + else { + Ext.callback(callback, scope || me, [me.childNodes]); + } + }, me); + } + // If it is is already collapsed but we want to recursively collapse then call collapseChildren + else if (recursive) { + me.collapseChildren(true, callback, scope); + } + } + // If it's not then we fire the callback right away + else { + Ext.callback(callback, scope || me, me.childNodes); + } + }, + + /** + * Collapse all the children of this node. + * @param {Function} recursive (Optional) True to recursively collapse all the children + * @param {Function} callback (Optional) The function to execute once all the children are collapsed + * @param {Object} scope (Optional) The scope to run the callback in + */ + collapseChildren: function(recursive, callback, scope) { + var me = this, + i = 0, + nodes = me.childNodes, + ln = nodes.length, + node, + collapsing = 0; + + for (; i < ln; ++i) { + node = nodes[i]; + if (!node.isLeaf() && node.isExpanded()) { + collapsing++; + nodes[i].collapse(recursive, function () { + collapsing--; + if (callback && !collapsing) { + Ext.callback(callback, scope || me, me.childNodes); + } + }); + } + } + + if (!collapsing && callback) { + Ext.callback(callback, scope || me, me.childNodes); + } + } + }; + } + } +}); \ No newline at end of file