/** 
 * This class is used as a set of methods that are applied to the prototype of a
 * {@link Ext.data.Model Model} 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.
 *
 * This class also creates extra {@link Ext.data.Field fields} on the model, if they do
 * not exist, to help maintain the tree state and UI. These fields are documented as
 * config options.
 *
 * The data fields used to render a tree node are: {@link #text}{@link #leaf},
 * {@link #children}, and {@link #expanded}.  Once a node is loaded to the tree store
 * you can use {@link Ext.data.Model#get get()} to fetch the value of a given field
 * name (provided there is not a convenience accessor on the Node for that field).
 *
 *     @example
 *     Ext.tip.QuickTipManager.init(); // not required when using Ext.application()
 *
 *     var root = {
 *         expanded: true,
 *         children: [{
 *             text: "Leaf node (<i>no folder/arrow icon</i>)",
 *             leaf: true,
 *             qtitle: 'Sample Tip Title',
 *             qtip: 'Tip body'
 *         }, {
 *             text: "Parent node expanded",
 *             expanded: true,
 *             children: [{
 *                 text: "Expanded leaf node 1",
 *                 leaf: true
 *             }, {
 *                 text: "Expanded leaf node 2",
 *                 leaf: true
 *             }]
 *         }, {
 *             text: "Parent node collapsed",
 *             children: [{
 *                 text: "Collapsed leaf node 1",
 *                 leaf: true
 *             }, {
 *                 text: "Collapsed leaf node 2",
 *                 leaf: true
 *             }]
 *         }]
 *     };
 *
 *     var tree = Ext.create('Ext.tree.Panel', {
 *         title: 'TreePanel',
 *         width: 260,
 *         height: 200,
 *         root: root,
 *         rootVisible: false,
 *         renderTo: document.body,
 *         bbar: ['The first node ', {
 *             text: 'is a leaf?',
 *             handler: function() {
 *                 var firstChild = tree.getRootNode().getChildAt(0);
 *                 Ext.Msg.alert('Is Leaf?', firstChild.isLeaf());
 *             }
 *         }, {
 *             text: 'has text?',
 *             handler: function() {
 *                 var firstChild = tree.getRootNode().getChildAt(0);
 *                 Ext.Msg.alert('Has Text:', firstChild.get('text'));
 *             }
 *         }]
 *     });
 *
 * The following configs have methods used to set the value / state of the node at
 * runtime:
 *
 * **{@link #children} / {@link #leaf}**
 *
 *  - {@link #appendChild}
 *  - {@link #hasChildNodes}
 *  - {@link #insertBefore}
 *  - {@link #insertChild}
 *  - {@link #method-remove}
 *  - {@link #removeAll}
 *  - {@link #removeChild}
 *  - {@link #replaceChild}
 *
 * **{@link #expanded}**
 *
 *  - {@link #method-expand}
 *  - {@link #expandChildren}
 *  - {@link #method-collapse}
 *  - {@link #collapseChildren}
 *
 * The remaining configs may be set using {@link Ext.data.Model#method-set set()}.
 *
 *     node.set('text', 'Changed Text'); // example showing how to change the node label
 *
 * The {@link #qtip}{@link #qtitle}, and {@link #qshowDelay} use QuickTips and
 * requires initializing {@link Ext.tip.QuickTipManager} unless the application is
 * created using {@link Ext#method-application}.
 *
 *     Ext.tip.QuickTipManager.init();
 *
 * For additional information and examples see the description for
 * {@link Ext.tree.Panel}.
 */
Ext.define('Ext.data.NodeInterface', {
    requires: [
        'Ext.data.field.Boolean',
        'Ext.data.field.Integer',
        'Ext.data.field.String',
        'Ext.data.writer.Json',
        'Ext.mixin.Observable'
    ],
 
    /**
     * @cfg {Boolean} [expanded=false]
     * True if the node is expanded.
     *
     * When the tree is asynchronously remote loaded, expanding a collapsed node loads
     * the children of that node (if the node has not already been loaded previously).
     *
     * See also: {@link #isExpanded}.
     */
 
    /**
     * @cfg {Boolean} [expandable=true]
     * False to prevent expanding/collapsing of this node.
     *
     * See also: {@link #isExpandable}.
     */
 
    /**
     * @cfg {Boolean} [checked=null]
     * Set to true or false to show a checkbox alongside this node.
     *
     * To fetch an array of checked nodes use {@link Ext.tree.Panel#method-getChecked
     * getChecked()}.
     */
 
    /**
     * @cfg {Boolean} [leaf=false]
     * Set to true to indicate that this child can have no children. The expand icon/arrow will
     * then not be rendered for this node.
     *
     * See also: {@link #isLeaf}.
     */
 
    /**
     * @cfg {String} cls
     * CSS class to apply to this node.
     */
 
    /**
     * @cfg iconCls
     * @inheritdoc Ext.panel.Header#cfg-iconCls
     * @localdoc Use {@link #icon} to set the icon src path directly.
     */
 
    /**
     * @cfg icon
     * @inheritdoc Ext.panel.Header#cfg-icon
     */
 
    /**
     * @cfg {Number/String} glyph
     *
     * A numeric unicode character code to use as the icon.  The default font-family 
     * for glyphs can be set globally using 
     * {@link Ext.app.Application#glyphFontFamily glyphFontFamily} application 
     * config or the {@link Ext#setGlyphFontFamily Ext.setGlyphFontFamily()} method.
     * It is initially set to `'Pictos'`.
     * 
     * The following shows how to set the glyph using the font icons provided in the 
     * SDK (assuming the font-family has been configured globally):
     *
     *     // assumes the glyphFontFamily is "Pictos"
     *     glyph: 'x48'       // the "home" icon (H character)
     *
     *     // assumes the glyphFontFamily is "Pictos"
     *     glyph: 72          // The "home" icon (H character)
     *
     *     // assumes the glyphFontFamily is "Pictos"
     *     glyph: 'H'         // the "home" icon
     * 
     * Alternatively, this config option accepts a string with the charCode and 
     * font-family separated by the `@` symbol.
     * 
     *     // using Font Awesome
     *     glyph: '[email protected]'     // the "home" icon
     * 
     *     // using Pictos
     *     glyph: '[email protected]'              // the "home" icon
     *
     * Depending on the theme you're using, you may need include the font icon 
     * packages in your application in order to use the icons included in the 
     * SDK.  For more information see:
     * 
     *  - [Font Awesome icons](http://fortawesome.github.io/Font-Awesome/cheatsheet/)
     *  - [Pictos icons](../guides/core_concepts/font_ext.html)
     *  - [Theming Guide](../guides/core_concepts/theming.html)
     * @since 6.2.0
     */
 
    /**
     * @cfg {Boolean} [allowDrop=true]
     * Set to false to deny dropping on this node.
     *
     * Applicable when using the {@link Ext.tree.plugin.TreeViewDragDrop
     * TreeViewDragDrop} plugin.
     */
 
    /**
     * @cfg {Boolean} [allowDrag=true]
     * Set to false to deny dragging of this node.
     *
     * Applicable when using the {@link Ext.tree.plugin.TreeViewDragDrop
     * TreeViewDragDrop} plugin.
     */
 
    /**
     * @cfg {String} href
     * A URL for a link that's created when this config is specified.
     *
     * See also {@link #hrefTarget}.
     */
 
    /**
     * @cfg {String} hrefTarget
     * Target for link. Only applicable when {@link #href} is also specified.
     */
 
    /**
     * @cfg {String} qtip
     * Tooltip text to show on this node.
     *
     * See also {@link #qtitle}.
     * See also {@link #qshowDelay}.
     */
 
    /**
     * @cfg {String} qtitle
     * Tooltip title.
     *
     * See also {@link #qtip}.
     * See also {@link #qshowDelay}.
     */
 
    /**
     * @cfg {Number} qshowDelay
     * Tooltip showDelay.
     *
     * See also {@link #qtip}.
     * See also {@link #qtitle}.
     */
 
    /**
     * @cfg {String} text
     * The text to show on node label (_html tags are accepted_).
     * The default text for the root node is `ROOT`.  All other nodes default to ''.
     *
     * **Note:** By default the node label is `text`, but can be set using the tree's
     * {@link Ext.tree.Panel#cfg-displayField displayField} config.
     */
 
    /**
     * @cfg {Ext.data.NodeInterface[]} children
     * Array of child nodes.
     *
     * **Note:** By default the child nodes root is `children`, but can be set using the
     * reader {@link Ext.data.reader.Reader#cfg-rootProperty rootProperty} config on the
     * {@link Ext.data.TreeStore TreeStore's} {@link Ext.data.TreeStore#cfg-proxy proxy}.
     */
 
    /**
     * @cfg {Boolean} [loaded=false]
     * @private
     * True if the node has finished loading.
     *
     * See {@link #isLoaded}.
     */
 
    /**
     * @cfg {Boolean} [loading=false]
     * @private
     * True if the node is currently loading.
     *
     * See {@link #isLoading}.
     */
 
    /**
     * @cfg {Boolean} root
     * @private
     * True if this is the root node.
     *
     * See {@link #isRoot}.
     */
 
    /**
     * @cfg {Boolean} isLast
     * @private
     * True if this is the last node.
     *
     * See {@link #method-isLast}.
     */
 
    /**
     * @cfg {Boolean} isFirst
     * @private
     * True if this is the first node.
     *
     * See {@link #method-isFirst}.
     */
 
    /**
     * @cfg {String} parentId
     * @private
     * ID of parent node.
     *
     * See {@link #parentNode}.
     */
 
    /**
     * @cfg {Number} index
     * @private
     * The position of the node inside its parent. When parent has 4 children and the node is third
     * amongst them, index will be 2.
     *
     * See {@link #indexOf} and {@link #indexOfId}.
     */
 
    /**
     * @cfg {Number} depth
     * @private
     * The number of parents this node has. A root node has depth 0, a child of it depth 1, and
     * so on...
     *
     * See {@link #getDepth}.
     */
 
    /**
     * @property {Ext.data.NodeInterface} nextSibling
     * A reference to this node's next sibling node. `null` if this node does not have a next
     * sibling.
     */
 
    /**
     * @property {Ext.data.NodeInterface} previousSibling
     * A reference to this node's previous sibling node. `null` if this node does not have a
     * previous sibling.
     */
 
    /**
     * @property {Ext.data.NodeInterface} parentNode
     * A reference to this node's parent node. `null` if this node is the root node.
     */
 
    /**
     * @property {Ext.data.NodeInterface} lastChild
     * A reference to this node's last child node. `null` if this node has no children.
     */
 
    /**
     * @property {Ext.data.NodeInterface} firstChild
     * A reference to this node's first child node. `null` if this node has no children.
     */
 
    /**
     * @property {Ext.data.NodeInterface[]} childNodes
     * An array of this nodes children.  Array will be empty if this node has no children.
     */
 
    /**
     * @method onRegisterTreeNode
     * Implement this method in a tree record subclass if it needs to track whenever it is
     * registered with a {@link Ext.data.TreeStore TreeStore}.
     * @param {Ext.data.TreeStore} treeStore The TreeStore to which the node is being registered.
     * @template
     */
 
    /**
     * @method onUnregisterTreeNode
     * Implement this method in a tree record subclass if it needs to track whenever it is
     * unregistered from a {@link Ext.data.TreeStore TreeStore}.
     * @param {Ext.data.TreeStore} treeStore The TreeStore from which the node is being
     * unregistered.
     * @template
     */
 
    statics: {
        /**
         * This method decorates a Model class such that it implements the interface of
         * a tree node. That is, it adds a set of methods, events, properties and fields
         * on every record.
         * @param {Ext.Class/Ext.data.Model} modelClass The Model class or an instance of
         * the Model class you want to decorate. In either case, this method decorates
         * the class so all instances of that type will have the new capabilities.
         * @static
         */
        decorate: function(modelClass) {
            var model = Ext.data.schema.Schema.lookupEntity(modelClass),
                proto = model.prototype,
                idName, idField, idType;
 
            if (!model.prototype.isObservable) {
                model.mixin(Ext.mixin.Observable.prototype.mixinId, Ext.mixin.Observable);
            }
 
            if (proto.isNode) { // if (already decorated)
                return;
            }
 
            idName = proto.idProperty;
            idField = model.getField(idName);
            idType = idField.type;
 
            model.override(this.getPrototypeBody());
 
            /* eslint-disable max-len, no-multi-spaces */
            model.addFields([
                { name: 'parentId',   type: idType,    defaultValue: null,  allowNull: idField.allowNull  },
                { name: 'index',      type: 'int',     defaultValue: -1,    persist: false, convert: null },
                { name: 'depth',      type: 'int',     defaultValue: 0,     persist: false, convert: null },
                { name: 'expanded',   type: 'bool',    defaultValue: false, persist: false, convert: null },
                { name: 'expandable', type: 'bool',    defaultValue: true,  persist: false, convert: null },
                { name: 'checked',    type: 'auto',    defaultValue: null,  persist: false, convert: null },
                { name: 'leaf',       type: 'bool',    defaultValue: false                                },
                { name: 'cls',        type: 'string',  defaultValue: '',    persist: false, convert: null },
                { name: 'iconCls',    type: 'string',  defaultValue: '',    persist: false, convert: null },
                { name: 'icon',       type: 'string',  defaultValue: '',    persist: false, convert: null },
                { name: 'glyph',      type: 'string',  defaultValue: '',    persist: false, convert: null },
                { name: 'root',       type: 'boolean', defaultValue: false, persist: false, convert: null },
                { name: 'isLast',     type: 'boolean', defaultValue: false, persist: false, convert: null },
                { name: 'isFirst',    type: 'boolean', defaultValue: false, persist: false, convert: null },
                { name: 'allowDrop',  type: 'boolean', defaultValue: true,  persist: false, convert: null },
                { name: 'allowDrag',  type: 'boolean', defaultValue: true,  persist: false, convert: null },
                { name: 'loaded',     type: 'boolean', defaultValue: false, persist: false, convert: null },
                { name: 'loading',    type: 'boolean', defaultValue: false, persist: false, convert: null },
                { name: 'href',       type: 'string',  defaultValue: '',    persist: false, convert: null },
                { name: 'hrefTarget', type: 'string',  defaultValue: '',    persist: false, convert: null },
                { name: 'qtip',       type: 'string',  defaultValue: '',    persist: false, convert: null },
                { name: 'qtitle',     type: 'string',  defaultValue: '',    persist: false, convert: null },
                { name: 'qshowDelay', type: 'int',     defaultValue: 0,     persist: false, convert: null },
                { name: 'children',   type: 'auto',    defaultValue: null,  persist: false, convert: null },
                { name: 'visible',    type: 'boolean', defaultValue: true,  persist: false                },
                { name: 'text',       type: 'string',                       persist: false                }
            ]);
            /* eslint-enable max-len, no-multi-spaces */
        },
 
        getPrototypeBody: function() {
            var bubbledEvents = {
                    idchanged: true,
                    append: true,
                    remove: true,
                    move: true,
                    insert: true,
                    beforeappend: true,
                    beforeremove: true,
                    beforemove: true,
                    beforeinsert: true,
                    expand: true,
                    collapse: true,
                    beforeexpand: true,
                    beforecollapse: true,
                    sort: true
                },
                silently = {
                    silent: true
                };
 
            // bulkUpdate usage:
            // This is used in 3 contexts:
            // a) When registering nodes. When bulk updating, we don't want to descend down the tree
            // recursively making calls to register which is redundant. We do need to call it for
            // each node because they need to be findable via id as soon as append events fire,
            // so we only do the minimum needed.
            // b) When setting a data property on the model. We only need to go through set
            // (and the subsequent event chain) so that the UI can update. If we're doing a bulk
            // update, the UI will update regardless.
            // c) triggerUIUpdate. This is because we know "something has changed", but not exactly
            // what, so we allow the UI to redraw itself. It has no purpose as far as data goes, so
            // skip it when we can
 
            return {
                /**
                 * @property {Boolean} isNode
                 * `true` in this class to identify an object as an instantiated Node, or subclass
                 * thereof.
                 */
                isNode: true,
 
                firstChild: null,
                lastChild: null,
                parentNode: null,
                previousSibling: null,
                nextSibling: null,
 
                constructor: function() {
                    var me = this;
 
                    me.mixins.observable.constructor.call(me);
                    me.callParent(arguments);
                    me.childNodes = [];
 
                    // These events are fired on this node, and programmatically bubble 
                    // up the parentNode axis, ending up walking off the top and firing 
                    // on the owning Ext.data.TreeStore
                    /**
                     * @event append
                     * Fires when a new child node is appended
                     * @param {Ext.data.NodeInterface} this This node
                     * @param {Ext.data.NodeInterface} node The newly appended node
                     * @param {Number} index The index of the newly appended node
                     */
                    /**
                     * @event remove
                     * Fires when a child node is removed
                     * @param {Ext.data.NodeInterface} this This node
                     * @param {Ext.data.NodeInterface} node The removed node
                     * @param {Boolean} isMove `true` if the child node is being removed so it can
                     * be moved to another position in the tree.
                     * @param {Object} context An object providing information about where the
                     * removed node came from. It contains the following properties:
                     * @param {Ext.data.NodeInterface} context.parentNode The node from which the
                     * removed node was removed.
                     * @param {Ext.data.NodeInterface} context.previousSibling The removed node's
                     * former previous sibling.
                     * @param {Ext.data.NodeInterface} context.nextSibling The removed node's former
                     * next sibling. (a side effect of calling
                     * {@link Ext.data.NodeInterface#appendChild appendChild} or
                     * {@link Ext.data.NodeInterface#insertBefore insertBefore} with a node that
                     * already has a parentNode)
                     */
                    /**
                     * @event move
                     * Fires when this node is moved to a new location in the tree
                     * @param {Ext.data.NodeInterface} this This node
                     * @param {Ext.data.NodeInterface} oldParent The old parent of this node
                     * @param {Ext.data.NodeInterface} newParent The new parent of this node
                     * @param {Number} index The index it was moved to
                     */
                    /**
                     * @event insert
                     * Fires when a new child node is inserted.
                     * @param {Ext.data.NodeInterface} this This node
                     * @param {Ext.data.NodeInterface} node The child node inserted
                     * @param {Ext.data.NodeInterface} refNode The child node the node was inserted
                     * before
                     */
                    /**
                     * @event beforeappend
                     * Fires before a new child is appended, return false to cancel the append.
                     * @param {Ext.data.NodeInterface} this This node
                     * @param {Ext.data.NodeInterface} node The child node to be appended
                     */
                    /**
                     * @event beforeremove
                     * Fires before a child is removed, return false to cancel the remove.
                     * @param {Ext.data.NodeInterface} this This node
                     * @param {Ext.data.NodeInterface} node The child node to be removed
                     * @param {Boolean} isMove `true` if the child node is being removed so it can
                     * be moved to another position in the tree. (a side effect of calling
                     * {@link Ext.data.NodeInterface#appendChild appendChild} or
                     * {@link Ext.data.NodeInterface#insertBefore insertBefore} with a node that
                     * already has a parentNode)
                     */
                    /**
                     * @event beforemove
                     * Fires before this node is moved to a new location in the tree. Return false
                     * to cancel the move.
                     * @param {Ext.data.NodeInterface} this This node
                     * @param {Ext.data.NodeInterface} oldParent The parent of this node
                     * @param {Ext.data.NodeInterface} newParent The new parent this node is moving
                     * to
                     * @param {Number} index The index it is being moved to
                     */
                    /**
                     * @event beforeinsert
                     * Fires before a new child is inserted, return false to cancel the insert.
                     * @param {Ext.data.NodeInterface} this This node
                     * @param {Ext.data.NodeInterface} node The child node to be inserted
                     * @param {Ext.data.NodeInterface} refNode The child node the node is being
                     * inserted before
                     */
                    /**
                     * @event expand
                     * Fires when this node is expanded.
                     * @param {Ext.data.NodeInterface} this The expanding node
                     */
                    /**
                     * @event collapse
                     * Fires when this node is collapsed.
                     * @param {Ext.data.NodeInterface} this The collapsing node
                     */
                    /**
                     * @event beforeexpand
                     * Fires before this node is expanded.
                     * @param {Ext.data.NodeInterface} this The expanding node
                     */
                    /**
                     * @event beforecollapse
                     * Fires before this node is collapsed.
                     * @param {Ext.data.NodeInterface} this The collapsing node
                     */
                    /**
                     * @event sort
                     * Fires when this node's childNodes are sorted.
                     * @param {Ext.data.NodeInterface} this This node.
                     * @param {Ext.data.NodeInterface[]} childNodes The childNodes of this node.
                     */
                    return me;
                },
 
                /**
                 * Ensures that the passed object is an instance of a Record with the NodeInterface
                 * applied
                 * @return {Ext.data.NodeInterface} 
                 */
                createNode: function(node) {
                    var me = this,
                        childType = me.childType,
                        store,
                        storeReader,
                        nodeProxy,
                        nodeReader,
                        reader,
                        typeProperty,
                        T = me.self;
 
                    // Passed node's internal data object
                    if (!node.isModel) {
                        // Check this node type's childType configuration
                        if (childType) {
                            T = me.schema.getEntity(childType);
                        }
                        // See if the reader has a typeProperty and use it if possible
                        else {
                            store = me.getTreeStore();
                            storeReader = store && store.getProxy().getReader();
                            nodeProxy = me.getProxy();
                            nodeReader = nodeProxy ? nodeProxy.getReader() : null;
 
                            // If the node's proxy's reader was configured with a special
                            // typeProperty (property name which defines the child type name)
                            // use that.
                            /* eslint-disable indent */
                            reader = !storeReader ||
                                    (nodeReader && nodeReader.initialConfig.typeProperty)
                                        ? nodeReader
                                        : storeReader;
                            /* eslint-enable indent */
 
                            if (reader) {
                                typeProperty = reader.getTypeProperty();
 
                                if (typeProperty) {
                                    T = reader.getChildType(me.schema, node, typeProperty);
                                }
                            }
                        }
 
                        node = new T(node);
                    }
 
                    // The node may already decorated, but may not have been
                    // so when the model constructor was called. If not,
                    // setup defaults here
                    if (!node.childNodes) {
                        node.firstChild = node.lastChild = node.parentNode =
                        node.previousSibling = node.nextSibling = null;
 
                        node.childNodes = [];
                    }
 
                    return 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.
                 * @protected
                 * @param {Boolean} commit 
                 * @param {Object} info The info to update. May contain any of the following
                 *  @param {Object} info.isFirst 
                 *  @param {Object} info.isLast 
                 *  @param {Object} info.index 
                 *  @param {Object} info.depth 
                 *  @param {Object} info.parentId 
                 *  @return {String[]} The names of any persistent fields that were modified.
                 */
                updateInfo: function(commit, info) {
                    var me = this,
                        phantom = me.phantom,
                        result, childInfo, children, childCount, i;
 
                    commit = {
                        silent: true,
                        commit: commit
                    };
 
                    // The only way child data can be influenced is if this node has changed level
                    // in this update.
                    if (info.depth != null && info.depth !== me.data.depth) {
                        childInfo = {
                            depth: info.depth + 1
                        };
 
                        children = me.childNodes;
                        childCount = children.length;
 
                        for (= 0; i < childCount; i++) {
                            children[i].updateInfo(commit, childInfo);
                        }
                    }
 
                    result = me.set(info, commit);
 
                    // Restore phantom flag which might get cleared by a commit.
                    me.phantom = phantom;
 
                    return result;
                },
 
                /**
                 * 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, otherwise returns false.
                 * @return {Boolean} 
                 */
                isExpandable: function() {
                    var me = this;
 
                    if (me.get('expandable')) {
                        return !(me.isLeaf() ||
                                (me.isLoaded() && !me.phantom && !me.hasChildNodes()));
                    }
 
                    return false;
                },
 
                triggerUIUpdate: function() {
                    // This isn't ideal, however none of the underlying fields have changed
                    // but we still need to update the UI
                    // callJoined calls both the Stores we are joined to, and any TreeStore
                    // of which we may be a descendant.
                    this.callJoined('afterEdit', []);
                },
 
                /**
                 * Inserts 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 {Ext.data.NodeInterface/Ext.data.NodeInterface[]/Object} node The node
                 * or Array of nodes to append
                 * @param {Boolean} [suppressEvents=false] True to suppress firing of 
                 * events.
                 * @param {Boolean} [commit=false] 
                 * @return {Ext.data.NodeInterface} The appended node if single append, or null
                 * if an array was passed
                 */
                appendChild: function(node, suppressEvents, commit) {
                    var me = this,
                        treeStore = me.getTreeStore(),
                        bulkUpdate = treeStore && treeStore.bulkUpdate,
                        childInfo = {
                            isLast: true,
                            parentId: me.getId(),
                            depth: (me.data.depth || 0) + 1
                        },
                        oldParent, previousSibling, modifiedFields, index, result,
                        i, ln;
 
                    // Coalesce all layouts caused by node append
                    Ext.suspendLayouts();
 
                    // if passed an array do them one by one
                    if (Ext.isArray(node)) {
                        ln = node.length;
                        result = new Array(ln);
                        // Suspend view updating and data syncing during update
                        me.callTreeStore('beginFill');
 
                        for (= 0; i < ln; i++) {
                            result[i] = me.appendChild(node[i], suppressEvents, commit);
                        }
 
                        // Resume view updating and data syncing after appending all new children.
                        // This will fire the add event to any views (if its the top level append)
                        me.callTreeStore('endFill', [result]);
                    }
                    else {
                        // Make sure it is a record
                        node = me.createNode(node);
 
                        /* eslint-disable-next-line max-len */
                        if (suppressEvents !== true && me.fireBubbledEvent('beforeappend', [me, node]) === false) {
                            Ext.resumeLayouts(true);
 
                            return false;
                        }
 
                        index = me.childNodes.length;
                        oldParent = node.parentNode;
 
                        // it's a move, make sure we move it cleanly
                        if (oldParent) {
                            /* eslint-disable-next-line max-len */
                            if (suppressEvents !== true && node.fireBubbledEvent('beforemove', [node, oldParent, me, index]) === false) {
                                Ext.resumeLayouts(true);
 
                                return false;
                            }
 
                            // Return false if a beforeremove listener vetoed the remove
                            /* eslint-disable-next-line max-len */
                            if (oldParent.removeChild(node, false, suppressEvents, oldParent.getTreeStore() === treeStore) === false) {
                                Ext.resumeLayouts(true);
 
                                return false;
                            }
                        }
 
                        // Coalesce sync operations across this operation
                        // Node field setting (loaded, expanded) and node addition both trigger
                        // a sync if autoSync is set.
                        if (treeStore) {
                            treeStore.beginUpdate();
                        }
 
                        index = me.childNodes.length;
 
                        if (index === 0) {
                            me.setFirstChild(node);
                        }
 
                        me.childNodes[index] = node;
                        node.parentNode = me;
                        node.nextSibling = null;
 
                        me.setLastChild(node);
 
                        previousSibling = me.childNodes[index - 1];
 
                        if (previousSibling) {
                            node.previousSibling = previousSibling;
                            previousSibling.nextSibling = node;
 
                            previousSibling.updateInfo(commit, {
                                isLast: false
                            });
 
                            // No need to trigger a ui update if we're doing a bulk update
                            if (!bulkUpdate) {
                                previousSibling.triggerUIUpdate();
                            }
                        }
                        else {
                            node.previousSibling = null;
                        }
 
                        // Update the new child's info passing in info we already know
                        childInfo.isFirst = index === 0;
                        childInfo.index = index;
 
                        // Integrate the new node into its new position.
                        // It's not in the store yet, so we might need to
                        // inform the store of significant field changes later.
                        modifiedFields = node.updateInfo(commit, childInfo);
 
                        // We stop being a leaf as soon as a node is appended
                        if (me.isLeaf()) {
                            me.set('leaf', false);
                        }
 
                        // As soon as we append a child to this node, we are loaded
                        if (!me.isLoaded()) {
                            if (bulkUpdate) {
                                me.data.loaded = true;
                            }
                            else {
                                me.set('loaded', true);
                            }
                        }
                        else if (me.childNodes.length === 1 && !bulkUpdate) {
                            me.triggerUIUpdate();
                        }
 
                        // Ensure connectors are correct by updating the UI on all intervening
                        // nodes (descendants) between last sibling and new node.
                        if (index && me.childNodes[index - 1].isExpanded() && !bulkUpdate) {
                            me.childNodes[index - 1].cascade(me.triggerUIUpdate);
                        }
 
                        // We register the subtree before we proceed so relayed events
                        // (like nodeappend) from our TreeStore (if we have one) will be
                        // able to use getNodeById. The node also needs to be added since 
                        // we're passing it in the events below. If we're not bulk updating, it
                        // means we're just appending a node (with possible children), so do it
                        // deeply here to ensure everything is captured.
                        if (treeStore) {
                            treeStore.registerNode(me, !bulkUpdate);
 
                            if (bulkUpdate) {
                                treeStore.registerNode(node);
                            }
                        }
 
                        // This node MUST fire its events first, so that if the TreeStore's
                        // onNodeAppend loads and appends local children, the events are still
                        // in order. This node appended this child first, before the descendant
                        // cascade.
                        if (suppressEvents !== true) {
                            me.fireBubbledEvent('append', [me, node, index]);
 
                            if (oldParent) {
                                node.fireBubbledEvent('move', [node, oldParent, me, index]);
                            }
                        }
 
                        // Inform the TreeStore so that the node can be inserted
                        // and registered.
                        me.callTreeStore('onNodeAppend', [node, index]);
 
                        // Now that the store contains the new node, we cam inform it of field
                        // changes.
                        if (modifiedFields) {
                            node.callJoined('afterEdit', [modifiedFields]);
                        }
 
                        result = node;
 
                        // Coalesce sync operations across this operation
                        // Node field setting (loaded, expanded) and node addition both trigger
                        // a sync if autoSync is set.
                        if (treeStore) {
                            treeStore.endUpdate();
                        }
                    }
 
                    // Flush layouts caused by updating of the UI
                    Ext.resumeLayouts(true);
 
                    return result;
                },
 
                /**
                * Returns the tree this node is in.
                * @return {Ext.tree.Panel} The tree panel which owns this node.
                */
                getOwnerTree: function() {
                    var store = this.getTreeStore();
 
                    return store && store.ownerTree;
                },
 
                /**
                 * Returns the {@link Ext.data.TreeStore} which owns this node.
                 * @return {Ext.data.TreeStore} The TreeStore which owns this node.
                 */
                getTreeStore: function() {
                    var root = this;
 
                    while (root && !root.treeStore) {
                        root = root.parentNode;
                    }
 
                    return root && root.treeStore;
                },
 
                /**
                 * Removes a child node from this node.
                 * @param {Ext.data.NodeInterface} node The node to remove
                 * @param {Boolean} [erase=false] True to erase the record using the
                 * configured proxy.
                 * @param {Boolean} [suppressEvents] (private)
                 * @param {Boolean} [isMove] (private)
                 * @return {Ext.data.NodeInterface} The removed node
                 */
                removeChild: function(node, erase, suppressEvents, isMove) {
                    var me = this,
                        index = me.indexOf(node),
                        i, childCount,
                        previousSibling,
                        treeStore = me.getTreeStore(),
                        bulkUpdate = treeStore && treeStore.bulkUpdate,
                        removeContext,
                        removeRange = [];
 
                    if (index === -1 || (suppressEvents !== true &&
                        me.fireBubbledEvent('beforeremove', [me, node, !!isMove]) === false)) {
                        return false;
                    }
 
                    // Coalesce all layouts caused by node removal
                    Ext.suspendLayouts();
 
                    // Coalesce sync operations across this operation
                    if (treeStore) {
                        treeStore.beginUpdate();
                    }
 
                    // remove it from childNodes collection
                    Ext.Array.erase(me.childNodes, index, 1);
 
                    // update child refs
                    if (me.firstChild === node) {
                        me.setFirstChild(node.nextSibling);
                    }
 
                    if (me.lastChild === node) {
                        me.setLastChild(node.previousSibling);
                    }
 
                    // Update previous sibling to point to its new next.
                    previousSibling = node.previousSibling;
 
                    if (previousSibling) {
                        node.previousSibling.nextSibling = node.nextSibling;
                    }
 
                    // Update the next sibling to point to its new previous
                    if (node.nextSibling) {
                        node.nextSibling.previousSibling = node.previousSibling;
 
                        // And if it's the new first child, let it know
                        if (index === 0) {
                            node.nextSibling.updateInfo(false, {
                                isFirst: true
                            });
                        }
 
                        // Update subsequent siblings' index values
                        for (= index, childCount = me.childNodes.length; i < childCount; i++) {
                            me.childNodes[i].updateInfo(false, {
                                index: i
                            });
                        }
                    }
 
                    // If the removed node had no next sibling, but had a previous,
                    // update the previous sibling so it knows it's the last
                    else if (previousSibling) {
                        previousSibling.updateInfo(false, {
                            isLast: true
                        });
 
                        // We're removing the last child.
                        // Ensure connectors are correct by updating the UI on all intervening
                        // nodes (descendants) between previous sibling and new node.
                        if (!bulkUpdate) {
                            if (previousSibling.isExpanded()) {
                                previousSibling.cascade(me.triggerUIUpdate);
                            }
                            // No intervening descendant nodes, just update the previous sibling
                            else {
                                previousSibling.triggerUIUpdate();
                            }
                        }
                    }
 
                    // If this node suddenly doesn't have child nodes anymore, update 
                    // myself
                    if (!me.childNodes.length && !bulkUpdate) {
                        me.triggerUIUpdate();
                    }
 
                    // Flush layouts caused by updating the UI
                    Ext.resumeLayouts(true);
 
                    if (suppressEvents !== true) {
                        // Context argument to events.
                        removeContext = {
                            parentNode: node.parentNode,
                            previousSibling: node.previousSibling,
                            nextSibling: node.nextSibling
                        };
 
                        // Inform the TreeStore so that descendant nodes can be removed.
                        me.callTreeStore('beforeNodeRemove', [[node], !!isMove, removeRange]);
 
                        node.previousSibling = node.nextSibling = node.parentNode = null;
 
                        me.fireBubbledEvent('remove', [me, node, !!isMove, removeContext]);
 
                        // Inform the TreeStore so that the node unregistered and unjoined.
                        me.callTreeStore('onNodeRemove', [[node], !!isMove, removeRange]);
                    }
 
                    // Update removed node's pointers *after* firing event so that listeners
                    // can tell where the removal took place
                    if (erase) {
                        node.erase(true);
                    }
                    else {
                        node.clear();
                    }
 
                    // Must clear the parentNode silently upon remove from the TreeStore.
                    // Any subsequent append to any node will trigger dirtiness
                    // (It may be added to a different node of the same ID, e.g. "root").
                    // lastParentId still needed for TreeStore's clearRemovedOnLoad functionality
                    // to be able to link nodes in the removed array to nodes under the reloading
                    // node's tree.
                    if (!isMove) {
                        node.set({
                            parentId: null,
                            lastParentId: me.getId()
                        }, silently);
                    }
 
                    // Coalesce sync operations across this operation
                    if (treeStore) {
                        treeStore.endUpdate();
                    }
 
                    return node;
                },
 
                /**
                 * Creates a copy (clone) of this Node.
                 * @param {String} [newId] A new id, defaults to this Node's id.
                 * @param {Ext.data.session.Session} [session] The session to which the
                 * new record belongs.
                 * @param {Boolean} [deep=false] True to recursively copy all child nodes
                 * into the new Node. False to copy without child Nodes.
                 * @return {Ext.data.NodeInterface} A copy of this Node.
                 */
                copy: function(newId, session, deep) {
                    var me = this,
                        result,
                        args = [newId],
                        len = me.childNodes ? me.childNodes.length : 0,
                        i;
 
                    // Historical API of NodeInterface#copy was (newId, deep)
                    // We must keep that working if a Session is passed.
                    // Second argument is Session in superclass copy method.
                    if (session && session.isSession) {
                        args.push(session);
                    }
                    else if (arguments.length < 3) {
                        deep = session;
                    }
 
                    result = me.callParent(args);
 
                    // Move child nodes across to the copy if required
                    if (deep) {
                        for (= 0; i < len; i++) {
                            result.appendChild(me.childNodes[i].copy(undefined, true));
                        }
                    }
 
                    return result;
                },
 
                /**
                 * Clears the node.
                 * @private
                 * @param {Boolean} [erase=false] True to erase the node using the configured
                 * proxy.
                 * @param {Boolean} [resetChildren=false] True to reset child nodes
                 */
                clear: function(erase, resetChildren) {
                    var me = this;
 
                    // clear any references from the node
                    me.parentNode = me.previousSibling = me.nextSibling = null;
 
                    if (erase) {
                        me.firstChild = me.lastChild = me.childNodes = null;
                    }
 
                    // This is used by TreeStore for clearing root node state on reload
                    if (resetChildren) {
                        me.firstChild = me.lastChild = null;
                        me.childNodes.length = 0;
 
                        if (me.data) {
                            me.data.children = null;
                        }
                    }
                },
 
                drop: function() {
                    var me = this,
                        childNodes = me.childNodes,
                        parentNode = me.parentNode,
                        treeStore = me.getTreeStore(),
                        node, i, len;
 
                    // Ensure Model operations are performed.
                    // Store removal is NOT handled.
                    // TreeStore's afterDrop does nothing.
                    me.callParent();
 
                    // If called in recursion from here, there'll be no parentNode
                    if (parentNode) {
                        // TreeStore.onNodeRemove also adds invisible descendant nodes to the remove
                        // tracking array.
                        parentNode.removeChild(me);
                    }
                    // If we are the root, there'll be no parent node. It's a special case. We must
                    // update the TreeStore's root with a null node.
                    else if (me.get('root')) {
                        treeStore.setRoot(null);
                    }
                    // Removing a node removes the node and all *VISIBLE* descendant nodes from the
                    // Store and adds them to the remove tracking array.
                    //
                    // After this point, no descendant nodes have a connection to the TreeStore.
 
                    // Coalesce sync operations across this operation
                    if (treeStore) {
                        treeStore.beginUpdate();
                    }
 
                    // Recurse down dropping all descendants.
                    // This will NOT remove them from the store's data collection
                    for (= 0, len = childNodes ? childNodes.length : 0; i < len; i++) {
                        node = childNodes[i];
 
                        // Detach descendant nodes so that they do not all attempt to perform
                        // removal from the parent.
                        node.clear();
 
                        // Drop descendant nodes.
                        node.drop();
                    }
 
                    // Coalesce sync operations across this operation
                    if (treeStore) {
                        treeStore.endUpdate();
                    }
                },
 
                /**
                 * Destroys the node.
                 * @param {Boolean} [options] (private)
                 */
                erase: function(options) {
                    var me = this,
                        childNodes = me.childNodes,
                        len = childNodes && childNodes.length,
                        i, node;
 
                    // This unhooks this node from the tree structure
                    // The UI is updated.
                    // Now to recursively erase.
                    me.remove();
 
                    // Clear removes linkage, so the erase's call into drop cannot recurse.
                    // this method has to recurse to do all its stuff.
                    me.clear(true);
                    me.callParent([options]);
 
                    for (= 0; i < len; i++) {
                        node = childNodes[i];
 
                        // The top level in the cascade is already removed.
                        // Prevent the recursive erase calls doing further node removal.
                        node.parentNode = null;
                        node.erase(options);
                    }
                },
 
                /**
                 * Inserts the first node before the second node in this nodes childNodes
                 * collection.
                 * @param {Ext.data.NodeInterface/Ext.data.NodeInterface[]/Object} node The node
                 * to insert
                 * @param {Ext.data.NodeInterface} refNode The node to insert before (if null the
                 * node is appended)
                 * @param {Boolean} [suppressEvents] (private)
                 * @return {Ext.data.NodeInterface} The inserted node
                 */
                insertBefore: function(node, refNode, suppressEvents) {
                    var me = this,
                        index = me.indexOf(refNode),
                        oldParent = node.parentNode,
                        refIndex = index,
                        childCount, previousSibling, i,
                        treeStore = me.getTreeStore(),
                        bulkUpdate = treeStore && treeStore.bulkUpdate,
                        modifiedFields, sibling, siblingModifiedFields;
 
                    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);
 
                    /* eslint-disable-next-line max-len */
                    if (suppressEvents !== true && me.fireBubbledEvent('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) {
                        /* eslint-disable-next-line max-len */
                        if (suppressEvents !== true && node.fireBubbledEvent('beforemove', [node, oldParent, me, index, refNode]) === false) {
                            return false;
                        }
 
                        // Return false if a beforeremove listener vetoed the remove
                        /* eslint-disable-next-line max-len */
                        if (oldParent.removeChild(node, false, suppressEvents, oldParent.getTreeStore() === treeStore) === false) {
                            return false;
                        }
                    }
 
                    // Coalesce sync operations across this operation
                    // Node field setting (loaded, expanded) and node addition both trigger a sync
                    // if autoSync is set.
                    // Nodes acquire a treeStore early now by virtue of getting a parentNode, so
                    // set operations on them will arrive to this Store's onCollectionUpdate
                    if (treeStore) {
                        treeStore.beginUpdate();
                    }
 
                    if (refIndex === 0) {
                        me.setFirstChild(node);
                    }
 
                    Ext.Array.splice(me.childNodes, refIndex, 0, node);
                    node.parentNode = me;
 
                    node.nextSibling = refNode;
                    refNode.previousSibling = node;
 
                    previousSibling = me.childNodes[refIndex - 1];
 
                    if (previousSibling) {
                        node.previousSibling = previousSibling;
                        previousSibling.nextSibling = node;
                    }
                    else {
                        node.previousSibling = null;
                    }
 
                    // Integrate the new node into its new position.
                    // It's not in the store yet, so we might need to
                    // inform the store of significant field changes later.
                    modifiedFields = node.updateInfo(false, {
                        parentId: me.getId(),
                        index: refIndex,
                        isFirst: refIndex === 0,
                        isLast: false,
                        depth: (me.data.depth || 0) + 1
                    });
 
                    // Update the index for all following siblings.
                    for (= refIndex + 1, childCount = me.childNodes.length; i < childCount; i++) {
                        sibling = me.childNodes[i];
 
                        siblingModifiedFields = sibling.updateInfo(false, {
                            index: i
                        });
 
                        if (siblingModifiedFields) {
                            sibling.callJoined('afterEdit', [siblingModifiedFields]);
                        }
                    }
 
                    if (!me.isLoaded()) {
                        if (bulkUpdate) {
                            me.data.loaded = true;
                        }
                        else {
                            me.set('loaded', true);
                        }
                    }
                    // If this node didn't have any child nodes before, update myself
                    else if (me.childNodes.length === 1 && !bulkUpdate) {
                        me.triggerUIUpdate();
                    }
 
                    // We register the subtree before we proceed so relayed events
                    // (like nodeappend) from our TreeStore (if we have one) will be
                    // able to use getNodeById.
                    if (treeStore) {
                        treeStore.registerNode(me, !bulkUpdate);
                    }
 
                    // This node MUST fire its events first, so that if the TreeStore's
                    // onNodeInsert loads and appends local children, the events are still in order;
                    // This node appended this child first, before the descendant cascade.
                    if (suppressEvents !== true) {
                        me.fireBubbledEvent('insert', [me, node, refNode]);
 
                        if (oldParent) {
                            node.fireBubbledEvent('move', [node, oldParent, me, refIndex, refNode]);
                        }
                    }
 
                    // Inform the TreeStore so that the node can be registered and added
                    me.callTreeStore('onNodeInsert', [node, refIndex]);
 
                    // Now that the store contains the new record, we cam inform it of
                    // field changes.
                    if (modifiedFields) {
                        node.callJoined('afterEdit', [modifiedFields]);
                    }
 
                    // Coalesce sync operations across this operation
                    // Node field setting (loaded, expanded) and node addition both trigger a sync
                    // if autoSync is set.
                    if (treeStore) {
                        treeStore.endUpdate();
                    }
 
                    return node;
                },
 
                /**
                 * Inserts a node into this node.
                 * @param {Number} index The zero-based index to insert the node at
                 * @param {Ext.data.NodeInterface/Object} node The node to insert
                 * @return {Ext.data.NodeInterface} The node you just inserted
                 */
                insertChild: function(index, node) {
                    var sibling = this.childNodes[index];
 
                    if (sibling) {
                        return this.insertBefore(node, sibling);
                    }
                    else {
                        return this.appendChild(node);
                    }
                },
 
                /**
                 * Used by {@link Ext.tree.Column#initTemplateRendererData} to determine whether
                 * a node is the last *visible*
                 * sibling.
                 *
                 * @private
                 */
                isLastVisible: function() {
                    var me = this,
                        result = me.data.isLast,
                        next = me.nextSibling;
 
                    // If it is not the true last and the store is filtered
                    // we need to see if any following siblings are visible.
                    // If any are, return false.
                    if (!result && me.getTreeStore().isFiltered()) {
                        while (next) {
                            if (next.data.visible) {
                                return false;
                            }
 
                            next = next.nextSibling;
                        }
 
                        return true;
                    }
 
                    return result;
                },
 
                /**
                 * Removes this node from its parent.
                 *
                 * **If** the node is not phantom (only added in the client side), then it may be
                 * marked for removal.
                 *
                 * If the owning {@link Ext.data.TreeStore tree store} is set to
                 * {@link Ext.data.ProxyStore#trackRemoved track removed} then the node will be
                 * added to the stack of nodes due to be removed the next time the store is
                 * synced with the server.
                 *
                 * If the owning {@link Ext.data.TreeStore tree store} is set to
                 * {@link Ext.data.ProxyStore#autoSync auto synchronize} then the synchronize
                 * request will be initiated immediately.
                 *
                 * @param {Boolean} [erase=false] True to erase the node using the configured
                 * proxy. This is only needed when the owning {@link Ext.data.TreeStore tree store}
                 * is not taking care of synchronization operations.
                 *
                 * @param {Boolean} [suppressEvents] (private)
                 * @return {Ext.data.NodeInterface} this
                 */
                remove: function(erase, suppressEvents) {
                    var me = this,
                        parentNode = me.parentNode;
 
                    if (parentNode) {
                        parentNode.removeChild(me, erase, suppressEvents);
                    }
                    else if (erase) {
                        // If we don't have a parent, just erase it
                        me.erase(true);
                    }
 
                    return me;
                },
 
                /**
                 * Removes all child nodes from this node.
                 * @param {Boolean} [erase=false] True to erase the node using the configured
                 * proxy.
                 * @param {Boolean} [suppressEvents] (private)
                 * @param {Boolean} [fromParent] (private)
                 * @return {Ext.data.NodeInterface} this
                 */
                removeAll: function(erase, suppressEvents, fromParent) {
                    // This method duplicates logic from removeChild for the sake of
                    // speed since we can make a number of assumptions because we're
                    // getting rid of everything
                    var me = this,
                        childNodes = me.childNodes,
                        len = childNodes.length,
                        node, treeStore, i,
                        removeRange = [];
 
                    // Avoid all this if nothing to remove
                    if (!len) {
                        return me;
                    }
 
                    // Inform the TreeStore so that descendant nodes can be removed.
                    if (!fromParent) {
                        treeStore = me.getTreeStore();
 
                        // Coalesce sync operations across this operation
                        if (treeStore) {
                            treeStore.beginUpdate();
 
                            // The remove of visible descendants is handled by the top level
                            // call to onNodeRemove, so suspend firing the remove event so
                            // that every descendant remove does not update the UI.
                            treeStore.suspendEvent('remove');
 
                            me.callTreeStore('beforeNodeRemove', [childNodes, false, removeRange]);
                        }
                    }
 
                    for (= 0; i < len; ++i) {
                        node = childNodes[i];
 
                        node.previousSibling = node.nextSibling = node.parentNode = null;
 
                        me.fireBubbledEvent('remove', [me, node, false]);
 
                        if (erase) {
                            node.erase(true);
                        }
                        // Otherwise.... apparently, removeAll is always recursive.
                        else {
                            node.removeAll(false, suppressEvents, true);
                        }
                    }
 
                    // Inform the TreeStore so that all descendants are unregistered and unjoined.
                    if (!fromParent && treeStore) {
                        treeStore.resumeEvent('remove');
                        me.callTreeStore('onNodeRemove', [childNodes, false, removeRange]);
 
                        // Coalesce sync operations across this operation
                        treeStore.endUpdate();
                    }
 
                    me.firstChild = me.lastChild = null;
 
                    childNodes.length = 0;
 
                    if (!fromParent) {
                        me.triggerUIUpdate();
                    }
 
                    return me;
                },
 
                /**
                 * Returns the child node at the specified index.
                 * @param {Number} index 
                 * @return {Ext.data.NodeInterface} 
                 */
                getChildAt: function(index) {
                    return this.childNodes[index];
                },
 
                /**
                 * Replaces one child node in this node with another.
                 * @param {Ext.data.NodeInterface} newChild The replacement node
                 * @param {Ext.data.NodeInterface} oldChild The node to replace
                 * @param {Boolean} [suppressEvents] (private)
                 * @return {Ext.data.NodeInterface} The replaced node
                 */
                replaceChild: function(newChild, oldChild, suppressEvents) {
                    var s = oldChild ? oldChild.nextSibling : null;
 
                    this.removeChild(oldChild, false, suppressEvents);
                    this.insertBefore(newChild, s, suppressEvents);
 
                    return oldChild;
                },
 
                /**
                 * Returns the index of a child node
                 * @param {Ext.data.NodeInterface} child 
                 * @return {Number} The index of the child node or -1 if it was not found.
                 */
                indexOf: function(child) {
                    return Ext.Array.indexOf(this.childNodes, child);
                },
 
                /**
                 * Returns the index of a child node that matches the id
                 * @param {String} id The id of the node to find
                 * @return {Number} The index of the node or -1 if it was not found
                 */
                indexOfId: function(id) {
                    var childNodes = this.childNodes,
                        len = childNodes.length,
                        i = 0;
 
                    for (; i < len; ++i) {
                        if (childNodes[i].getId() === id) {
                            return i;
                        }
                    }
 
                    return -1;
                },
 
                /**
                 * Gets the hierarchical path from the root of the current node.
                 * @param {String} [field] The field to construct the path from. Defaults to the
                 * model idProperty.
                 * @param {String} [separator='/'] A separator to use.
                 * @return {String} The node path
                 */
                getPath: function(field, separator) {
                    field = field || this.idProperty;
                    separator = separator || '/';
 
                    /* eslint-disable-next-line vars-on-top */
                    var path = [this.get(field)],
                        parent = this.parentNode;
 
                    while (parent) {
                        path.unshift(parent.get(field));
                        parent = parent.parentNode;
                    }
 
                    return separator + path.join(separator);
                },
 
                /**
                 * 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] The scope (this reference) in which the function
                 * is executed. Defaults to the current Node.
                 * @param {Array} [args] The args to call the function with. Defaults 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;
                    }
                },
 
                /**
                 * Cascades down the tree from this node, calling the specified functions with each
                 * node. The arguments to the function will be the args provided or the current
                 * node. If the `before` function returns false at any point, the cascade is
                 * stopped on that branch.
                 *
                 * Note that the 3 argument form passing `fn, scope, args` is still supported.
                 * The `fn` function is as before, called *before* cascading down into child nodes.
                 * If it returns `false`, the child nodes are not traversed.
                 *
                 * @param {Object/Function} spec An object containing `before` and `after`
                 * functions, scope and an argument list or simply the `before` function.
                 * @param {Function} [spec.before] A function to call on a node *before*
                 * cascading down into child nodes. If it returns `false`, the child nodes
                 * are not traversed.
                 * @param {Function} [spec.after] A function to call on a node *after*
                 * cascading down into child nodes.
                 * @param {Object} [spec.scope] The scope (this reference) in which the
                 * functions are executed. Defaults to the current Node.
                 * @param {Array} [spec.args] The args to call the function with. Defaults
                 * to passing the current Node.
                 * @param {Object} [scope] If `spec` is the `before` function instead of
                 * an object, this argument is the `this` pointer.
                 * @param {Array} [args] If `spec` is the `before` function instead of
                 * an object, this argument is the `args` to pass.
                 * @param {Function} [after] If `spec` is the `before` function instead of
                 * an object, this argument is the `after` function to call.
                 */
                cascade: function(spec, scope, args, after) {
                    var me = this,
                        before = spec,
                        childNodes, length, i;
 
                    if (arguments.length === 1 && !Ext.isFunction(spec)) {
                        after = spec.after;
                        scope = spec.scope;
                        args = spec.args;
                        before = spec.before;
                    }
 
                    if (!before || before.apply(scope || me, args || [me]) !== false) {
                        childNodes = me.childNodes;
 
                        for (= 0, length = childNodes.length; i < length; i++) {
                            childNodes[i].cascade.call(childNodes[i], before, scope, args, after);
                        }
 
                        if (after) {
                            after.apply(scope || me, args || [me]);
                        }
                    }
                },
 
                cascadeBy: function() {
                    return this.cascade.apply(this, arguments);
                },
 
                /**
                 * Iterates 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] The scope (_this_ reference) in which the 
                 * function is executed. Defaults to the Node on which eachChild is 
                 * called.
                 * @param {Array} [args] The args to call the function with. Defaults to 
                 * passing the current Node.
                 */
                eachChild: function(fn, scope, args) {
                    var childNodes = this.childNodes,
                        length = childNodes.length,
                        i;
 
                    for (= 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 {Object} value The value to search for
                 * @param {Boolean} [deep=false] True to search through nodes deeper than the
                 * immediate children
                 * @return {Ext.data.NodeInterface} The found child or null if none was found
                 */
                findChild: function(attribute, value, deep) {
                    return this.findChildBy(function() {
                        return this.get(attribute) == value; // eslint-disable-line eqeqeq
                    }, 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] The scope (this reference) in which the function is
                 * executed. Defaults to the Node being tested.
                 * @param {Boolean} [deep=false] True to search through nodes deeper than the
                 * immediate children
                 * @return {Ext.data.NodeInterface} The found child or null if none was found
                 */
                findChildBy: function(fn, scope, deep) {
                    var cs = this.childNodes,
                        i, len, n, res;
 
                    for (= 0, len = cs.length; 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 {Ext.data.NodeInterface} 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 {Ext.data.NodeInterface} node 
                 * @return {Boolean} 
                 */
                isAncestor: function(node) {
                    var p = this.parentNode;
 
                    while (p) {
                        if (=== node) {
                            return true;
                        }
 
                        p = p.parentNode;
                    }
 
                    return false;
                },
 
                /**
                 * Sorts this nodes children using the supplied sort function.
                 * @param {Function} [sortFn] A function which, when passed two Nodes, returns -1,
                 * 0 or 1 depending upon required sort order.
                 *
                 * It omitted, the node is sorted according to the existing sorters in the owning
                 * {@link Ext.data.TreeStore TreeStore}.
                 * @param {Boolean} [recursive=false] True to apply this sort recursively
                 * @param {Boolean} [suppressEvent=false] True to not fire a sort event.
                 */
                sort: function(sortFn, recursive, suppressEvent) {
                    var me = this,
                        childNodes = me.childNodes,
                        ln = childNodes.length,
                        info = {
                            isFirst: true
                        },
                        i, n;
 
                    if (ln > 0) {
                        if (!sortFn) {
                            sortFn = me.getTreeStore().getSortFn();
                        }
 
                        Ext.Array.sort(childNodes, sortFn);
                        me.setFirstChild(childNodes[0]);
                        me.setLastChild(childNodes[ln - 1]);
 
                        for (= 0; i < ln; i++) {
                            n