Upgrade to ExtJS 4.0.7 - Released 10/19/2011
[extjs.git] / src / data / Tree.js
index 12923d3..ba22ba6 100644 (file)
-/*!
- * Ext JS Library 3.2.1
- * Copyright(c) 2006-2010 Ext JS, Inc.
- * licensing@extjs.com
- * http://www.extjs.com/license
- */
-/**
- * @class Ext.data.Tree
- * @extends Ext.util.Observable
- * Represents a tree data structure and bubbles all the events for its nodes. The nodes
- * in the tree have most standard DOM functionality.
- * @constructor
- * @param {Node} root (optional) The root node
- */
-Ext.data.Tree = function(root){
-   this.nodeHash = {};
-   /**
-    * The root node for this tree
-    * @type Node
-    */
-   this.root = null;
-   if(root){
-       this.setRootNode(root);
-   }
-   this.addEvents(
-       /**
-        * @event append
-        * Fires when a new child node is appended to a node in this tree.
-        * @param {Tree} tree The owner tree
-        * @param {Node} parent The parent 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 from a node in this tree.
-        * @param {Tree} tree The owner tree
-        * @param {Node} parent The parent node
-        * @param {Node} node The child node removed
-        */
-       "remove",
-       /**
-        * @event move
-        * Fires when a node is moved to a new location in the tree
-        * @param {Tree} tree The owner tree
-        * @param {Node} node The node moved
-        * @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 in a node in this tree.
-        * @param {Tree} tree The owner tree
-        * @param {Node} parent The parent 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 to a node in this tree, return false to cancel the append.
-        * @param {Tree} tree The owner tree
-        * @param {Node} parent The parent node
-        * @param {Node} node The child node to be appended
-        */
-       "beforeappend",
-       /**
-        * @event beforeremove
-        * Fires before a child is removed from a node in this tree, return false to cancel the remove.
-        * @param {Tree} tree The owner tree
-        * @param {Node} parent The parent node
-        * @param {Node} node The child node to be removed
-        */
-       "beforeremove",
-       /**
-        * @event beforemove
-        * Fires before a node is moved to a new location in the tree. Return false to cancel the move.
-        * @param {Tree} tree The owner tree
-        * @param {Node} node The node being moved
-        * @param {Node} oldParent The parent of the node
-        * @param {Node} newParent The new parent the node is moving to
-        * @param {Number} index The index it is being moved to
-        */
-       "beforemove",
-       /**
-        * @event beforeinsert
-        * Fires before a new child is inserted in a node in this tree, return false to cancel the insert.
-        * @param {Tree} tree The owner tree
-        * @param {Node} parent The parent node
-        * @param {Node} node The child node to be inserted
-        * @param {Node} refNode The child node the node is being inserted before
-        */
-       "beforeinsert"
-   );
-
-    Ext.data.Tree.superclass.constructor.call(this);
-};
-
-Ext.extend(Ext.data.Tree, Ext.util.Observable, {
-    /**
-     * @cfg {String} pathSeparator
-     * The token used to separate paths in node ids (defaults to '/').
-     */
-    pathSeparator: "/",
+/*
 
-    // private
-    proxyNodeEvent : function(){
-        return this.fireEvent.apply(this, arguments);
-    },
+This file is part of Ext JS 4
 
-    /**
-     * Returns the root node for this tree.
-     * @return {Node}
-     */
-    getRootNode : function(){
-        return this.root;
-    },
+Copyright (c) 2011 Sencha Inc
 
-    /**
-     * Sets the root node for this tree.
-     * @param {Node} node
-     * @return {Node}
-     */
-    setRootNode : function(node){
-        this.root = node;
-        node.ownerTree = this;
-        node.isRoot = true;
-        this.registerNode(node);
-        return node;
-    },
+Contact:  http://www.sencha.com/contact
 
-    /**
-     * Gets a node in this tree by its id.
-     * @param {String} id
-     * @return {Node}
-     */
-    getNodeById : function(id){
-        return this.nodeHash[id];
-    },
-
-    // private
-    registerNode : function(node){
-        this.nodeHash[node.id] = node;
-    },
+GNU General Public License Usage
+This file may be used under the terms of the GNU General Public License version 3.0 as published by the Free Software Foundation and appearing in the file LICENSE included in the packaging of this file.  Please review the following information to ensure the GNU General Public License version 3.0 requirements will be met: http://www.gnu.org/copyleft/gpl.html.
 
-    // private
-    unregisterNode : function(node){
-        delete this.nodeHash[node.id];
-    },
-
-    toString : function(){
-        return "[Tree"+(this.id?" "+this.id:"")+"]";
-    }
-});
+If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
 
+*/
 /**
- * @class Ext.data.Node
- * @extends Ext.util.Observable
- * @cfg {Boolean} leaf true if this node is a leaf and does not have children
- * @cfg {String} id The id for this node. If one is not specified, one is generated.
- * @constructor
- * @param {Object} attributes The attributes/config for the node
+ * @class Ext.data.Tree
+ *
+ * This class is used as a container for a series of nodes. The nodes themselves maintain
+ * the relationship between parent/child. The tree itself acts as a manager. It gives functionality
+ * to retrieve a node by its identifier: {@link #getNodeById}.
+ *
+ * The tree also relays events from any of it's child nodes, allowing them to be handled in a
+ * centralized fashion. In general this class is not used directly, rather used internally
+ * by other parts of the framework.
+ *
  */
-Ext.data.Node = function(attributes){
-    /**
-     * The attributes supplied for the node. You can use this property to access any custom attributes you supplied.
-     * @type {Object}
-     */
-    this.attributes = attributes || {};
-    this.leaf = this.attributes.leaf;
-    /**
-     * The node id. @type String
-     */
-    this.id = this.attributes.id;
-    if(!this.id){
-        this.id = Ext.id(null, "xnode-");
-        this.attributes.id = this.id;
-    }
-    /**
-     * All child nodes of this node. @type Array
-     */
-    this.childNodes = [];
-    if(!this.childNodes.indexOf){ // indexOf is a must
-        this.childNodes.indexOf = function(o){
-            for(var i = 0, len = this.length; i < len; i++){
-                if(this[i] == o){
-                    return i;
-                }
-            }
-            return -1;
-        };
-    }
-    /**
-     * The parent node for this node. @type Node
-     */
-    this.parentNode = null;
-    /**
-     * The first direct child node of this node, or null if this node has no child nodes. @type Node
-     */
-    this.firstChild = null;
-    /**
-     * The last direct child node of this node, or null if this node has no child nodes. @type Node
-     */
-    this.lastChild = null;
-    /**
-     * The node immediately preceding this node in the tree, or null if there is no sibling node. @type Node
-     */
-    this.previousSibling = null;
-    /**
-     * The node immediately following this node in the tree, or null if there is no sibling node. @type Node
-     */
-    this.nextSibling = null;
-
-    this.addEvents({
-       /**
-        * @event append
-        * Fires when a new child node is appended
-        * @param {Tree} tree The owner tree
-        * @param {Node} this This node
-        * @param {Node} node The newly appended node
-        * @param {Number} index The index of the newly appended node
-        */
-       "append" : true,
-       /**
-        * @event remove
-        * Fires when a child node is removed
-        * @param {Tree} tree The owner tree
-        * @param {Node} this This node
-        * @param {Node} node The removed node
-        */
-       "remove" : true,
-       /**
-        * @event move
-        * Fires when this node is moved to a new location in the tree
-        * @param {Tree} tree The owner 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" : true,
-       /**
-        * @event insert
-        * Fires when a new child node is inserted.
-        * @param {Tree} tree The owner tree
-        * @param {Node} this This node
-        * @param {Node} node The child node inserted
-        * @param {Node} refNode The child node the node was inserted before
-        */
-       "insert" : true,
-       /**
-        * @event beforeappend
-        * Fires before a new child is appended, return false to cancel the append.
-        * @param {Tree} tree The owner tree
-        * @param {Node} this This node
-        * @param {Node} node The child node to be appended
-        */
-       "beforeappend" : true,
-       /**
-        * @event beforeremove
-        * Fires before a child is removed, return false to cancel the remove.
-        * @param {Tree} tree The owner tree
-        * @param {Node} this This node
-        * @param {Node} node The child node to be removed
-        */
-       "beforeremove" : true,
-       /**
-        * @event beforemove
-        * Fires before this node is moved to a new location in the tree. Return false to cancel the move.
-        * @param {Tree} tree The owner tree
-        * @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" : true,
-       /**
-        * @event beforeinsert
-        * Fires before a new child is inserted, return false to cancel the insert.
-        * @param {Tree} tree The owner tree
-        * @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" : true
-   });
-    this.listeners = this.attributes.listeners;
-    Ext.data.Node.superclass.constructor.call(this);
-};
-
-Ext.extend(Ext.data.Node, Ext.util.Observable, {
-    // private
-    fireEvent : function(evtName){
-        // first do standard event for this node
-        if(Ext.data.Node.superclass.fireEvent.apply(this, arguments) === false){
-            return false;
-        }
-        // then bubble it up to the tree if the event wasn't cancelled
-        var ot = this.getOwnerTree();
-        if(ot){
-            if(ot.proxyNodeEvent.apply(ot, arguments) === false){
-                return false;
-            }
-        }
-        return true;
-    },
-
-    /**
-     * Returns true if this node is a leaf
-     * @return {Boolean}
-     */
-    isLeaf : function(){
-        return this.leaf === true;
-    },
+Ext.define('Ext.data.Tree', {
+    alias: 'data.tree',
 
-    // private
-    setFirstChild : function(node){
-        this.firstChild = node;
+    mixins: {
+        observable: "Ext.util.Observable"
     },
 
-    //private
-    setLastChild : function(node){
-        this.lastChild = node;
-    },
-
-
     /**
-     * Returns true if this node is the last child of its parent
-     * @return {Boolean}
+     * @property {Ext.data.NodeInterface}
+     * The root node for this tree
      */
-    isLast : function(){
-       return (!this.parentNode ? true : this.parentNode.lastChild == this);
-    },
+    root: null,
 
     /**
-     * Returns true if this node is the first child of its parent
-     * @return {Boolean}
+     * Creates new Tree object.
+     * @param {Ext.data.NodeInterface} root (optional) The root node
      */
-    isFirst : function(){
-       return (!this.parentNode ? true : this.parentNode.firstChild == this);
-    },
+    constructor: function(root) {
+        var me = this;
 
-    /**
-     * 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.attributes.expandable || this.hasChildNodes();
-    },
+        me.mixins.observable.constructor.call(me);
 
-    /**
-     * Insert node(s) as the last child node of this node.
-     * @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){
-        var multi = false;
-        if(Ext.isArray(node)){
-            multi = node;
-        }else if(arguments.length > 1){
-            multi = arguments;
-        }
-        // if passed an array or multiple args do them one by one
-        if(multi){
-            for(var i = 0, len = multi.length; i < len; i++) {
-                this.appendChild(multi[i]);
-            }
-        }else{
-            if(this.fireEvent("beforeappend", this.ownerTree, this, node) === false){
-                return false;
-            }
-            var index = this.childNodes.length;
-            var oldParent = node.parentNode;
-            // it's a move, make sure we move it cleanly
-            if(oldParent){
-                if(node.fireEvent("beforemove", node.getOwnerTree(), node, oldParent, this, index) === false){
-                    return false;
-                }
-                oldParent.removeChild(node);
-            }
-            index = this.childNodes.length;
-            if(index === 0){
-                this.setFirstChild(node);
-            }
-            this.childNodes.push(node);
-            node.parentNode = this;
-            var ps = this.childNodes[index-1];
-            if(ps){
-                node.previousSibling = ps;
-                ps.nextSibling = node;
-            }else{
-                node.previousSibling = null;
-            }
-            node.nextSibling = null;
-            this.setLastChild(node);
-            node.setOwnerTree(this.getOwnerTree());
-            this.fireEvent("append", this.ownerTree, this, node, index);
-            if(oldParent){
-                node.fireEvent("move", this.ownerTree, node, oldParent, this, index);
-            }
-            return node;
+        if (root) {
+            me.setRootNode(root);
         }
     },
 
     /**
-     * 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
+     * Returns the root node for this tree.
+     * @return {Ext.data.NodeInterface}
      */
-    removeChild : function(node, destroy){
-        var index = this.childNodes.indexOf(node);
-        if(index == -1){
-            return false;
-        }
-        if(this.fireEvent("beforeremove", this.ownerTree, this, node) === false){
-            return false;
-        }
-
-        // remove it from childNodes collection
-        this.childNodes.splice(index, 1);
-
-        // update siblings
-        if(node.previousSibling){
-            node.previousSibling.nextSibling = node.nextSibling;
-        }
-        if(node.nextSibling){
-            node.nextSibling.previousSibling = node.previousSibling;
-        }
-
-        // update child refs
-        if(this.firstChild == node){
-            this.setFirstChild(node.nextSibling);
-        }
-        if(this.lastChild == node){
-            this.setLastChild(node.previousSibling);
-        }
-
-        this.fireEvent("remove", this.ownerTree, this, node);
-        if(destroy){
-            node.destroy(true);
-        }else{
-            node.clear();
-        }
-        return node;
-    },
-
-    // private
-    clear : function(destroy){
-        // clear any references from the node
-        this.setOwnerTree(null, destroy);
-        this.parentNode = this.previousSibling = this.nextSibling = null;
-        if(destroy){
-            this.firstChild = this.lastChild = null;
-        }
+    getRootNode : function() {
+        return this.root;
     },
 
     /**
-     * Destroys the node.
-     */
-    destroy : function(/* private */ silent){
-        /*
-         * Silent is to be used in a number of cases
-         * 1) When setRootNode is called.
-         * 2) When destroy on the tree is called
-         * 3) For destroying child nodes on a node
-         */
-        if(silent === true){
-            this.purgeListeners();
-            this.clear(true);
-            Ext.each(this.childNodes, function(n){
-                n.destroy(true);
+     * Sets the root node for this tree.
+     * @param {Ext.data.NodeInterface} node
+     * @return {Ext.data.NodeInterface} The root node
+     */
+    setRootNode : function(node) {
+        var me = this;
+
+        me.root = node;
+        Ext.data.NodeInterface.decorate(node);
+
+        if (me.fireEvent('beforeappend', null, node) !== false) {
+            node.set('root', true);
+            node.updateInfo();
+
+            me.relayEvents(node, [
+                /**
+                 * @event append
+                 * @alias Ext.data.NodeInterface#append
+                 */
+                "append",
+
+                /**
+                 * @event remove
+                 * @alias Ext.data.NodeInterface#remove
+                 */
+                "remove",
+
+                /**
+                 * @event move
+                 * @alias Ext.data.NodeInterface#move
+                 */
+                "move",
+
+                /**
+                 * @event insert
+                 * @alias Ext.data.NodeInterface#insert
+                 */
+                "insert",
+
+                /**
+                 * @event beforeappend
+                 * @alias Ext.data.NodeInterface#beforeappend
+                 */
+                "beforeappend",
+
+                /**
+                 * @event beforeremove
+                 * @alias Ext.data.NodeInterface#beforeremove
+                 */
+                "beforeremove",
+
+                /**
+                 * @event beforemove
+                 * @alias Ext.data.NodeInterface#beforemove
+                 */
+                "beforemove",
+
+                /**
+                 * @event beforeinsert
+                 * @alias Ext.data.NodeInterface#beforeinsert
+                 */
+                "beforeinsert",
+
+                 /**
+                  * @event expand
+                  * @alias Ext.data.NodeInterface#expand
+                  */
+                 "expand",
+
+                 /**
+                  * @event collapse
+                  * @alias Ext.data.NodeInterface#collapse
+                  */
+                 "collapse",
+
+                 /**
+                  * @event beforeexpand
+                  * @alias Ext.data.NodeInterface#beforeexpand
+                  */
+                 "beforeexpand",
+
+                 /**
+                  * @event beforecollapse
+                  * @alias Ext.data.NodeInterface#beforecollapse
+                  */
+                 "beforecollapse" ,
+
+                 /**
+                  * @event rootchange
+                  * Fires whenever the root node is changed in the tree.
+                  * @param {Ext.data.Model} root The new root
+                  */
+                 "rootchange"
+            ]);
+
+            node.on({
+                scope: me,
+                insert: me.onNodeInsert,
+                append: me.onNodeAppend,
+                remove: me.onNodeRemove
             });
-            this.childNodes = null;
-        }else{
-            this.remove(true);
-        }
-    },
-
-    /**
-     * 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){
-        if(!refNode){ // like standard Dom, refNode can be null for append
-            return this.appendChild(node);
-        }
-        // nothing to do
-        if(node == refNode){
-            return false;
-        }
 
-        if(this.fireEvent("beforeinsert", this.ownerTree, this, node, refNode) === false){
-            return false;
+            me.nodeHash = {};
+            me.registerNode(node);
+            me.fireEvent('append', null, node);
+            me.fireEvent('rootchange', node);
         }
-        var index = this.childNodes.indexOf(refNode);
-        var oldParent = node.parentNode;
-        var refIndex = index;
 
-        // when moving internally, indexes will change after remove
-        if(oldParent == this && this.childNodes.indexOf(node) < index){
-            refIndex--;
-        }
-
-        // it's a move, make sure we move it cleanly
-        if(oldParent){
-            if(node.fireEvent("beforemove", node.getOwnerTree(), node, oldParent, this, index, refNode) === false){
-                return false;
-            }
-            oldParent.removeChild(node);
-        }
-        if(refIndex === 0){
-            this.setFirstChild(node);
-        }
-        this.childNodes.splice(refIndex, 0, node);
-        node.parentNode = this;
-        var ps = this.childNodes[refIndex-1];
-        if(ps){
-            node.previousSibling = ps;
-            ps.nextSibling = node;
-        }else{
-            node.previousSibling = null;
-        }
-        node.nextSibling = refNode;
-        refNode.previousSibling = node;
-        node.setOwnerTree(this.getOwnerTree());
-        this.fireEvent("insert", this.ownerTree, this, node, refNode);
-        if(oldParent){
-            node.fireEvent("move", this.ownerTree, node, oldParent, this, refIndex, refNode);
-        }
         return 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){
-        if (this.parentNode) {
-            this.parentNode.removeChild(this, destroy);
-        }
-        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){
-        var cn = this.childNodes,
-            n;
-        while((n = cn[0])){
-            this.removeChild(n, destroy);
-        }
-        return this;
-    },
-
-    /**
-     * Returns the child node at the specified index.
-     * @param {Number} index
-     * @return {Node}
-     */
-    item : 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){
-        var s = oldChild ? oldChild.nextSibling : null;
-        this.removeChild(oldChild);
-        this.insertBefore(newChild, s);
-        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 this.childNodes.indexOf(child);
-    },
-
-    /**
-     * Returns the tree this node is in.
-     * @return {Tree}
+     * Flattens all the nodes in the tree into an array.
+     * @private
+     * @return {Ext.data.NodeInterface[]} The flattened nodes.
      */
-    getOwnerTree : function(){
-        // if it doesn't have one, look for one
-        if(!this.ownerTree){
-            var p = this;
-            while(p){
-                if(p.ownerTree){
-                    this.ownerTree = p.ownerTree;
-                    break;
-                }
-                p = p.parentNode;
-            }
-        }
-        return this.ownerTree;
-    },
-
-    /**
-     * Returns depth of this node (the root node has a depth of 0)
-     * @return {Number}
-     */
-    getDepth : function(){
-        var depth = 0;
-        var p = this;
-        while(p.parentNode){
-            ++depth;
-            p = p.parentNode;
-        }
-        return depth;
-    },
+    flatten: function(){
+        var nodes = [],
+            hash = this.nodeHash,
+            key;
 
-    // private
-    setOwnerTree : function(tree, destroy){
-        // if it is a move, we need to update everyone
-        if(tree != this.ownerTree){
-            if(this.ownerTree){
-                this.ownerTree.unregisterNode(this);
-            }
-            this.ownerTree = tree;
-            // If we're destroying, we don't need to recurse since it will be called on each child node
-            if(destroy !== true){
-                Ext.each(this.childNodes, function(n){
-                    n.setOwnerTree(tree);
-                });
-            }
-            if(tree){
-                tree.registerNode(this);
+        for (key in hash) {
+            if (hash.hasOwnProperty(key)) {
+                nodes.push(hash[key]);
             }
         }
+        return nodes;
     },
 
     /**
-     * Changes the id of this node.
-     * @param {String} id The new id for the node.
+     * Fired when a node is inserted into the root or one of it's children
+     * @private
+     * @param {Ext.data.NodeInterface} parent The parent node
+     * @param {Ext.data.NodeInterface} node The inserted node
      */
-    setId: function(id){
-        if(id !== this.id){
-            var t = this.ownerTree;
-            if(t){
-                t.unregisterNode(this);
-            }
-            this.id = this.attributes.id = id;
-            if(t){
-                t.registerNode(this);
-            }
-            this.onIdChange(id);
-        }
+    onNodeInsert: function(parent, node) {
+        this.registerNode(node, true);
     },
 
-    // private
-    onIdChange: Ext.emptyFn,
-
     /**
-     * Returns the path for this node. The path can be used to expand or select this node programmatically.
-     * @param {String} attr (optional) The attr to use for the path (defaults to the node's id)
-     * @return {String} The path
+     * Fired when a node is appended into the root or one of it's children
+     * @private
+     * @param {Ext.data.NodeInterface} parent The parent node
+     * @param {Ext.data.NodeInterface} node The appended node
      */
-    getPath : function(attr){
-        attr = attr || "id";
-        var p = this.parentNode;
-        var b = [this.attributes[attr]];
-        while(p){
-            b.unshift(p.attributes[attr]);
-            p = p.parentNode;
-        }
-        var sep = this.getOwnerTree().pathSeparator;
-        return sep + b.join(sep);
+    onNodeAppend: function(parent, node) {
+        this.registerNode(node, true);
     },
 
     /**
-     * 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)
+     * Fired when a node is removed from the root or one of it's children
+     * @private
+     * @param {Ext.data.NodeInterface} parent The parent node
+     * @param {Ext.data.NodeInterface} node The removed node
      */
-    bubble : function(fn, scope, args){
-        var p = this;
-        while(p){
-            if(fn.apply(scope || p, args || [p]) === false){
-                break;
-            }
-            p = p.parentNode;
-        }
+    onNodeRemove: function(parent, node) {
+        this.unregisterNode(node, true);
     },
 
     /**
-     * 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)
-     */
-    cascade : function(fn, scope, args){
-        if(fn.apply(scope || this, args || [this]) !== false){
-            var cs = this.childNodes;
-            for(var i = 0, len = cs.length; i < len; i++) {
-                cs[i].cascade(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 cs = this.childNodes;
-        for(var i = 0, len = cs.length; i < len; i++) {
-            if(fn.apply(scope || this, args || [cs[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
+     * Gets a node in this tree by its id.
+     * @param {String} id
+     * @return {Ext.data.NodeInterface} The match node.
      */
-    findChild : function(attribute, value, deep){
-        return this.findChildBy(function(){
-            return this.attributes[attribute] == value;
-        }, null, deep);
+    getNodeById : function(id) {
+        return this.nodeHash[id];
     },
 
     /**
-     * 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
+     * Registers a node with the tree
+     * @private
+     * @param {Ext.data.NodeInterface} The node to register
+     * @param {Boolean} [includeChildren] True to unregister any child nodes
      */
-    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;
-                }
-            }
-            
+    registerNode : function(node, includeChildren) {
+        this.nodeHash[node.getId() || node.internalId] = node;
+        if (includeChildren === true) {
+            node.eachChild(function(child){
+                this.registerNode(child, true);
+            }, this);
         }
-        return null;
     },
 
     /**
-     * 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 {Object} scope (optional)The scope (<code>this</code> reference) in which the function is executed. Defaults to the browser window.
+     * Unregisters a node with the tree
+     * @private
+     * @param {Ext.data.NodeInterface} The node to unregister
+     * @param {Boolean} [includeChildren] True to unregister any child nodes
      */
-    sort : function(fn, scope){
-        var cs = this.childNodes;
-        var len = cs.length;
-        if(len > 0){
-            var sortFn = scope ? function(){fn.apply(scope, arguments);} : fn;
-            cs.sort(sortFn);
-            for(var i = 0; i < len; i++){
-                var n = cs[i];
-                n.previousSibling = cs[i-1];
-                n.nextSibling = cs[i+1];
-                if(i === 0){
-                    this.setFirstChild(n);
-                }
-                if(i == len-1){
-                    this.setLastChild(n);
-                }
-            }
+    unregisterNode : function(node, includeChildren) {
+        delete this.nodeHash[node.getId() || node.internalId];
+        if (includeChildren === true) {
+            node.eachChild(function(child){
+                this.unregisterNode(child, true);
+            }, this);
         }
     },
 
     /**
-     * Returns true if this node is an ancestor (at any point) of the passed node.
-     * @param {Node} node
-     * @return {Boolean}
+     * Sorts this tree
+     * @private
+     * @param {Function} sorterFn The function to use for sorting
+     * @param {Boolean} recursive True to perform recursive sorting
      */
-    contains : function(node){
-        return node.isAncestor(this);
+    sort: function(sorterFn, recursive) {
+        this.getRootNode().sort(sorterFn, recursive);
     },
 
-    /**
-     * Returns true if the passed node is an ancestor (at any point) of this node.
-     * @param {Node} node
-     * @return {Boolean}
+     /**
+     * Filters this tree
+     * @private
+     * @param {Function} sorterFn The function to use for filtering
+     * @param {Boolean} recursive True to perform recursive filtering
      */
-    isAncestor : function(node){
-        var p = this.parentNode;
-        while(p){
-            if(p == node){
-                return true;
-            }
-            p = p.parentNode;
-        }
-        return false;
-    },
-
-    toString : function(){
-        return "[Node"+(this.id?" "+this.id:"")+"]";
+    filter: function(filters, recursive) {
+        this.getRootNode().filter(filters, recursive);
     }
-});
\ No newline at end of file
+});