X-Git-Url: http://git.ithinksw.org/extjs.git/blobdiff_plain/6e39d509471fe9b4e2660e0d1631b350d0c66f40..7a654f8d43fdb43d78b63d90528bed6e86b608cc:/docs/source/NodeInterface.html diff --git a/docs/source/NodeInterface.html b/docs/source/NodeInterface.html new file mode 100644 index 00000000..55f38f77 --- /dev/null +++ b/docs/source/NodeInterface.html @@ -0,0 +1,1075 @@ +Sencha Documentation Project
/**
+ * @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 <tt>expandable</tt>
+                 * node attribute is explicitly specified as true (see {@link #attributes}), otherwise returns false.
+                 * @return {Boolean}
+                 */
+                isExpandable : function() {
+                    return this.get('expandable') || this.hasChildNodes();
+                },
+
+                /**
+                 * <p>Insert node(s) as the last child node of this node.</p>
+                 * <p>If the node was previously a child node of another parent node, it will be removed from that node first.</p>
+                 * @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 <tt>true</tt> to destroy the node upon removal. Defaults to <tt>false</tt>.
+                 * @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 <code>{@link #id}</code>.
+                 * @param {Boolean} deep (optional) <p>If passed as <code>true</code>, all child Nodes are recursively copied into the new Node.</p>
+                 * <p>If omitted or false, the copy will have no child Nodes.</p>
+                 * @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 <tt>true</tt> to destroy the node upon removal. Defaults to <tt>false</tt>.
+                 * @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 <tt>true</tt> to destroy the node upon removal. Defaults to <tt>false</tt>.
+                 * @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 (<code>this</code> 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;
+                    }
+                },
+
+                //<deprecated since=0.99>
+                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);
+                },
+                //</deprecated>
+
+                /**
+                 * 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 (<code>this</code> 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 (<code>this</code> 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 <code>true</code>.
+                 * @param {Function} fn A function which must return <code>true</code> if the passed Node is the required Node.
+                 * @param {Object} scope (optional) The scope (<code>this</code> 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