/** 
 * 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: 'xf015@FontAwesome'     // the "home" icon
     * 
     *     // using Pictos
     *     glyph: 'H@Pictos'              // 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.
                        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
                    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
                    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
                    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
                    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 = childNodes[i];
                            n.previousSibling = childNodes[- 1];
                            n.nextSibling = childNodes[+ 1];
                            
                            // Update the index and first/last status of children
                            info.isLast = (=== ln - 1);
                            info.index = i;
                            n.updateInfo(false, info);
                            info.isFirst = false;
 
                            if (recursive && !n.isLeaf()) {
                                n.sort(sortFn, true, true);
                            }
                        }
 
                        // The suppressEvent flag is basically used to indicate a recursive sort
                        if (suppressEvent !== true) {
                            me.fireBubbledEvent('sort', [me, childNodes]);
 
                            // Inform the TreeStore that this node is sorted
                            me.callTreeStore('onNodeSort', [childNodes]);
                        }
                    }
                },
 
                /**
                 * Returns `true` if this node is expanded.
                 * @return {Boolean} 
                 */
                isExpanded: function() {
                    return this.get('expanded');
                },
 
                /**
                 * Returns true if this node is loaded
                 * @return {Boolean} 
                 */
                isLoaded: function() {
                    return this.get('loaded');
                },
                
                /**
                 * Returns true if this node is a branch node, and the entire branch is fully
                 * loaded.
                 *
                 * Using this method, it is possible to ascertain whether an
                 * `expandAll()` call (_classic toolkit TreePanel method_) will have 
                 * access to all descendant nodes without incurring a store load.
                 * @return {Boolean} 
                 */
                isBranchLoaded: function() {
                    var isBranchLoaded = !this.isLeaf() && this.isLoaded();
 
                    if (isBranchLoaded) {
                        this.cascade(function(node) {
                            if (!node.isLeaf()) {
                                isBranchLoaded = isBranchLoaded || node.isBranchLoaded();
                            }
                            
                            return isBranchLoaded;
                        });
                    }
                    
                    return isBranchLoaded;
                },
 
                /**
                 * Returns true if this node is loading
                 * @return {Boolean} 
                 */
                isLoading: function() {
                    return this.get('loading');
                },
 
                /**
                 * Returns true if this node is the root node
                 * @return {Boolean} 
                 */
                isRoot: function() {
                    return !this.parentNode;
                },
 
                /**
                 * Returns true if this node is visible. Note that visibility refers to
                 * the structure of the tree, the {@link Ext.tree.Panel#rootVisible}
                 * configuration is not taken into account here. If this method is called
                 * on the root node, it will always be visible.
                 * @return {Boolean} 
                 */
                isVisible: function() {
                    var parent = this.parentNode;
                    
                    while (parent) {
                        if (!parent.isExpanded()) {
                            return false;
                        }
                        
                        parent = parent.parentNode;
                    }
                    
                    return true;
                },
 
                /**
                 * Expand this node.
                 * @param {Boolean} [recursive=false] True to recursively expand all the children
                 * @param {Function} [callback] The function to execute once the expand completes
                 * @param {Object} [scope] The scope to run the callback in
                 */
                expand: function(recursive, callback, scope) {
                    var me = this,
                        treeStore,
                        resumeAddEvent;
 
                    // all paths must call the callback (eventually) or things like
                    // selectPath fail
 
                    // First we start by checking if this node is a parent
                    if (!me.isLeaf()) {
                        // If it's loading, wait until it loads before proceeding
                        if (me.isLoading()) {
                            me.on('expand', function() {
                                me.expand(recursive, callback, scope);
                            }, me, { single: true });
                        }
                        else {
                            // Now we check if this record is already expanding or expanded
                            if (!me.isExpanded()) {
 
                                if (me.fireBubbledEvent('beforeexpand', [me]) !== false) {
 
                                    // Here we are testing if all the descendant nodes required
                                    // by a recursive expansion are available without an
                                    // asynchronous store load.
                                    //
                                    // That is either all branch nodes are loaded, or the store
                                    // loads synchronously.
                                    //
                                    // If that is the case, then we do not want the TreeStore to
                                    // fire add events and update the UI (and layout) for every
                                    // batch of child nodes inserted.
                                    // Instead, we suspend the add event, and at the end, fire
                                    // a data refresh so that the UI gets only one update. It will
                                    // be a view refresh, but will still be more efficient.
                                    if (recursive) {
                                        // Only the topmost node in a recursive expand should
                                        // suspend the add event and fire the refresh event, so if
                                        // our parent is synchronously, recursively expanding,
                                        // we just flag that we are doing likewise.
                                        if (me.parentNode &&
                                            me.parentNode.isSynchronousRecursiveExpand) {
                                            me.isSynchronousRecursiveExpand = true;
                                        }
                                        else {
                                            treeStore = me.getTreeStore();
                                            
                                            if (treeStore.getProxy().isSynchronous ||
                                                me.isBranchLoaded()) {
                                                me.isSynchronousRecursiveExpand = true;
                                                treeStore.suspendEvent('add', 'datachanged');
                                                resumeAddEvent = true;
                                            }
                                        }
                                    }
 
                                    // Inform the TreeStore that we intend to expand, and that
                                    // it should call onChildNodesAvailable when the child nodes
                                    // are available
                                    /* eslint-disable-next-line max-len */
                                    me.callTreeStore('onBeforeNodeExpand', [me.onChildNodesAvailable, me, [recursive, callback, scope]]);
 
                                    // If we suspended the add event so that all additions of
                                    // descendant nodes did not update the UI, then resume the
                                    // event here, and refresh the data
                                    if (resumeAddEvent) {
                                        treeStore.resumeEvent('add', 'datachanged');
 
                                        // Fire the generic datachanged event in addition to the
                                        // refresh event
                                        treeStore.fireEvent('datachanged', treeStore);
                                        treeStore.fireEvent('refresh', treeStore);
                                    }
                                    
                                    me.isSynchronousRecursiveExpand = false;
                                }
 
                            }
                            else if (recursive) {
                                // If it is is already expanded but we want to recursively expand
                                // then call expandChildren
                                me.expandChildren(true, callback, scope);
                            }
                            else {
                                Ext.callback(callback, scope || me, [me.childNodes]);
                            }
                        }
                    }
                    else {
                        // If it's not then we fire the callback right away
                        Ext.callback(callback, scope || me); // leaf = no childNodes
                    }
                },
 
                /**
                 * @private
                 * Called as a callback from the {@link Ext.data.TreeStore#onBeforeNodeExpand} when
                 * the child nodes needed by {@link #method-expand} have been loaded and appended.
                 */
                onChildNodesAvailable: function(records, recursive, callback, scope) {
                    var me = this,
                        treeStore = me.getTreeStore(),
                        bulkUpdate = treeStore && treeStore.bulkUpdate,
                        ancestor, collapsedAncestors, i;
 
                    // Bracket expansion with layout suspension.
                    // In optimum case, when recursive, child node data are loaded and expansion is
                    // synchronous within the suspension.
                    Ext.suspendLayouts();
 
                    // Collect collapsed ancestors.
                    // We are going to expand the topmost one while ensuring that
                    // any intervening collapsed nodes have their expanded state as true.
                    for (ancestor = me.parentNode; ancestor; ancestor = ancestor.parentNode) {
                        if (!ancestor.isExpanded()) {
                            (collapsedAncestors || (collapsedAncestors = [])).unshift(ancestor);
                        }
                    }
 
                    // Not structural. The TreeView's onUpdate listener just updates the [+] icon
                    // to [-] in response.
                    if (bulkUpdate || !treeStore.isVisible(me)) {
                        me.data.expanded = true;
                    }
                    else {
                        me.set('expanded', true);
                    }
 
                    // Set the intervening collapsed nodes to expanded state, then expand the
                    // topmost.
                    // The whole descendant tree will be inserted into the collection below the
                    // topmost ancestor.
                    if (collapsedAncestors) {
                        // Ensure intervening collapsed nodes have their status set to expanded
                        // Not structural. The TreeView's onUpdate listener just updates the
                        // [+] icon to [-] in response.
                        for (= 1; i < collapsedAncestors.length; i++) {
                            ancestor = collapsedAncestors[i];
                            
                            if (bulkUpdate || !treeStore.isVisible(ancestor)) {
                                ancestor.data.expanded = true;
                            }
                            else {
                                ancestor.set('expanded', true);
                            }
                        }
 
                        // Expand the topmost collapsed one.
                        // The correctly set expanded states all the way down will ensure that
                        // All nodes needed are inserted into the Store.
                        collapsedAncestors[0].expand();
 
                        // Fire the expand event on all those intervening collapsed nodes
                        for (= 1; i < collapsedAncestors.length; i++) {
                            ancestor = collapsedAncestors[i];
                            ancestor.fireBubbledEvent('expand', [ancestor, ancestor.childNodes]);
                        }
                    }
                    else {
                        // TreeStore's onNodeExpand inserts the child nodes below the parent
                        me.callTreeStore('onNodeExpand', [records, false]);
                    }
 
                    me.fireBubbledEvent('expand', [me, records]);
 
                    // Call the expandChildren method if recursive was set to true
                    if (recursive) {
                        me.expandChildren(true, callback, scope);
                    }
                    else {
                        Ext.callback(callback, scope || me, [me.childNodes]);
                    }
 
                    Ext.resumeLayouts(true);
                },
 
                /**
                 * Expand all the children of this node.
                 * @param {Boolean} [recursive=false] True to recursively expand all the children
                 * @param {Function} [callback] The function to execute once all the children are
                 * expanded
                 * @param {Object} [scope] The `this` pointer for the callback.
                 * @param {Boolean} [singleExpand] (private)
                 */
                expandChildren: function(recursive, callback, scope, singleExpand) {
                    var me = this,
                        origCallback, i, allNodes, expandNodes, ln, node, treeStore;
 
                    // Ext 4.2.0 broke the API for this method by adding a singleExpand argument
                    // at index 1. As of 4.2.3 The method signature has been reverted back
                    // to its original pre-4.2.0 state, however, we must check to see if
                    // the 4.2.0 version is being used for compatibility reasons.
                    if (Ext.isBoolean(callback)) {
                        origCallback = callback;
                        callback = scope;
                        scope = singleExpand;
                        singleExpand = origCallback;
                    }
 
                    if (singleExpand === undefined) {
                        treeStore = me.getTreeStore();
                        singleExpand = treeStore && treeStore.singleExpand;
                    }
                    
                    allNodes = me.childNodes;
                    expandNodes = [];
                    ln = singleExpand ? Math.min(allNodes.length, 1) : allNodes.length;
 
                    for (= 0; i < ln; ++i) {
                        node = allNodes[i];
                        
                        if (!node.isLeaf()) {
                            expandNodes[expandNodes.length] = node;
                        }
                    }
                    
                    ln = expandNodes.length;
 
                    for (= 0; i < ln; ++i) {
                        expandNodes[i].expand(recursive);
                    }
 
                    if (callback) {
                        Ext.callback(callback, scope || me, [me.childNodes]);
                    }
                },
 
                /**
                 * Collapse this node.
                 * @param {Boolean} [recursive=false] True to recursively collapse all the children
                 * @param {Function} [callback] The function to execute once the collapse completes
                 * @param {Object} [scope] The scope to run the callback in
                 */
                collapse: function(recursive, callback, scope) {
                    var me = this,
                        expanded = me.isExpanded(),
                        treeStore = me.getTreeStore(),
                        bulkUpdate = treeStore && treeStore.bulkUpdate,
                        len = me.childNodes.length,
                        i, collapseChildren;
 
                    // If this is a parent and
                    //      already collapsed but the recursive flag is passed to target child nodes
                    //   or
                    //      the collapse is not vetoed by a listener
                    if (!me.isLeaf() && ((!expanded && recursive) ||
                        me.fireBubbledEvent('beforecollapse', [me]) !== false)) {
                        // Bracket collapsing with layout suspension.
                        // Collapsing is synchronous within the suspension.
                        Ext.suspendLayouts();
 
                        // Inform listeners of a collapse event if we are still expanded.
                        if (me.isExpanded()) {
                            // Set up the callback to set non-leaf descendants to collapsed
                            // if necessary. If recursive, we just need to set all non-leaf
                            // descendants to collapsed state.
                            // We *DO NOT* call collapse on them. That would attempt to remove
                            // their descendants from the UI, and that is done: THIS node
                            // is collapsed - ALL descendants are removed from the UI.
                            // Descendant non-leaves just silently change state.
                            if (recursive) {
                                collapseChildren = function() {
                                    for (= 0; i < len; i++) {
                                        me.childNodes[i].setCollapsed(true);
                                    }
                                };
                                
                                if (callback) {
                                    /* eslint-disable-next-line max-len */
                                    callback = Ext.Function.createSequence(collapseChildren, Ext.Function.bind(callback, scope, [me.childNodes]));
                                }
                                else {
                                    callback = collapseChildren;
                                }
                            }
                            else if (callback) {
                                callback = Ext.Function.bind(callback, scope, [me.childNodes]);
                            }
 
                            // Not structural. The TreeView's onUpdate listener just updates the
                            // [+] icon to [-] in response.
                            if (bulkUpdate || !treeStore.contains(me)) {
                                me.data.expanded = false;
                            }
                            else {
                                me.set('expanded', false);
                            }
 
                            // Call the TreeStore's onNodeCollapse which removes all descendant
                            // nodes to achieve UI collapse and passes callback on in its
                            // beforecollapse event which is poked into the animWrap for
                            // final calling in the animation callback.
                            me.callTreeStore('onNodeCollapse', [me.childNodes, callback, scope]);
 
                            me.fireBubbledEvent('collapse', [me, me.childNodes]);
 
                            // So that it's not called at the end
                            callback = null;
                        }
 
                        // If recursive, we just need to set all non-leaf descendants to collapsed
                        // state. We *DO NOT* call collapse on them. That would attempt to remove
                        // their descendants from the UI, and that is done: THIS node is collapsed -
                        // ALL descendants are removed from the UI.
                        // Descendant non-leaves just silently change state.
                        else if (recursive) {
                            for (= 0; i < len; i++) {
                                me.childNodes[i].setCollapsed(true);
                            }
                        }
 
                        Ext.resumeLayouts(true);
                    }
 
                    // Call the passed callback
                    Ext.callback(callback, scope || me, [me.childNodes]);
                },
 
                /**
                 * @private
                 *
                 * Sets the node into the collapsed state without affecting the UI.
                 *
                 * This is called when a node is collapsed with the recursive flag. All the
                 * descendant nodes will have been removed from the store, but descendant non-leaf
                 * nodes still need to be set to the collapsed state without affecting the UI.
                 */
                setCollapsed: function(recursive) {
                    var me = this,
                        len = me.childNodes.length,
                        i;
 
                    // Only if we are not a leaf node and the collapse was not vetoed by a listener.
                    if (!me.isLeaf() && me.fireBubbledEvent('beforecollapse', [me]) !== false) {
 
                        // Update the state directly.
                        me.data.expanded = false;
 
                        // Listened for by NodeStore.onNodeCollapse, but will do nothing except
                        // pass on the documented events because the records have already been
                        // removed from the store when the ancestor node was collapsed.
                        me.fireBubbledEvent('collapse', [me, me.childNodes]);
 
                        if (recursive) {
                            for (= 0; i < len; i++) {
                                me.childNodes[i].setCollapsed(true);
                            }
                        }
                    }
                },
 
                /**
                 * Collapse all the children of this node.
                 * @param {Function} [recursive=false] True to recursively collapse all the children
                 * @param {Function} [callback] The function to execute once all the children are
                 * collapsed
                 * @param {Object} [scope] The scope to run the callback in
                 */
                collapseChildren: function(recursive, callback, scope) {
                    var me = this,
                        allNodes = me.childNodes,
                        ln = allNodes.length,
                        collapseNodes = [],
                        node, i;
 
                    // Only bother with loaded, expanded, non-leaf nodes
                    for (= 0; i < ln; ++i) {
                        node = allNodes[i];
                        
                        if (!node.isLeaf() && node.isLoaded() && node.isExpanded()) {
                            collapseNodes.push(node);
                        }
                    }
                    
                    ln = collapseNodes.length;
 
                    if (ln) {
                        // Collapse the collapsible children.
                        // Pass our callback to the last one.
                        for (= 0; i < ln; ++i) {
                            node = collapseNodes[i];
                            
                            if (=== ln - 1) {
                                node.collapse(recursive, callback, scope);
                            }
                            else {
                                node.collapse(recursive);
                            }
                        }
                    }
                    else {
                        // Nothing to collapse, so fire the callback
                        Ext.callback(callback, scope);
                    }
                },
 
                /**
                 * Fires the specified event with the passed parameters (minus the event name,
                 * plus the `options` object passed to
                 * {@link Ext.mixin.Observable#addListener addListener}).
                 *
                 * An event may be set to bubble up an Observable parent hierarchy (See
                 * {@link Ext.Component#getBubbleTarget}) by
                 * calling {@link Ext.mixin.Observable#enableBubble enableBubble}.
                 *
                 * @param {String} eventName The name of the event to fire.
                 * @param {Object...} args Variable number of parameters are passed to handlers.
                 * @return {Boolean} returns `false` if any of the handlers return `false`
                 * otherwise it returns `true`.
                 */
                fireEvent: function(eventName) {
                    return this.fireBubbledEvent(eventName, Ext.Array.slice(arguments, 1));
                },
 
                // Node events always bubble, but events which bubble are always created,
                // so bubble in a loop and only fire when there are listeners at each level.
                // bubbled events always fire because they cannot tell if there is a listener
                // at each level.
                fireBubbledEvent: function(eventName, args) {
                    var result, eventSource, topNode;
 
                    // The event bubbles (all native NodeInterface events do)...
                    if (bubbledEvents[eventName]) {
                        /* eslint-disable-next-line max-len */
                        for (eventSource = this; result !== false && eventSource; eventSource = (topNode = eventSource).parentNode) {
                            result = eventSource.fireEventArgs.call(eventSource, eventName, args);
                        }
 
                        // We hit the topmost node in the loop above.
                        // Fire the event on its TreeStore if any (might be a disembodied
                        // tree fragment with no TreeStore)
                        if (result !== false) {
                            eventSource = topNode.getTreeStore();
                            
                            if (eventSource && eventSource.hasListeners &&
                                eventSource.hasListeners[eventName = 'node' + eventName]) {
                                result = eventSource.fireEventArgs(eventName, args);
                            }
                        }
                        
                        return result;
                    }
                    // Event does not bubble.
                    else {
                        return this.fireEventArgs.apply(this, arguments);
                    }
                },
 
                /**
                 * Creates an object representation of this node including its children.
                 */
                serialize: function(writerParam) {
                    var writer = writerParam || new Ext.data.writer.Json({
                            writeAllFields: true
                        }),
                        result = writer.getRecordData(this),
                        childNodes = this.childNodes,
                        len = childNodes.length,
                        children, i;
 
                    if (len > 0) {
                        result.children = children = [];
                        
                        for (= 0; i < len; i++) {
                            children.push(childNodes[i].serialize(writer));
                        }
                    }
                    
                    return result;
                },
 
                // Used to inform the TreeStore that we belong to about some event which requires
                // its participation.
                callTreeStore: function(funcName, args) {
                    var me = this,
                        target = me.getTreeStore(),
                        fn = target && target[funcName];
 
                    if (target && fn) {
                        args = args || [];
                        
                        if (args[0] !== me) {
                            args.unshift(me);
                        }
                        
                        fn.apply(target, args);
                    }
                },
 
                addCls: function(cls) {
                    this.replaceCls(null, cls);
                },
 
                removeCls: function(cls) {
                    this.replaceCls(cls);
                },
 
                replaceCls: function(oldCls, newCls) {
                    var pieces = this._parseCls(this.data.cls),
                        parts = this._parseCls(oldCls);
 
                    if (parts.length) {
                        pieces = Ext.Array.difference(pieces, parts);
                    }
 
                    parts = this._parseCls(newCls);
                    
                    if (parts.length) {
                        pieces = Ext.Array.unique(pieces.concat(parts));
                    }
 
                    this.set('cls', pieces.join(' '));
                },
 
                toggleCls: function(cls, state) {
                    var pieces, parts, len, i, p;
                    
                    if (state === undefined) {
                        pieces = this._parseCls(this.data.cls);
                        parts = this._parseCls(cls);
 
                        for (= 0, len = parts.length; i < len; ++i) {
                            p = parts[i];
 
                            if (Ext.Array.contains(pieces, p)) {
                                Ext.Array.remove(pieces, p);
                            }
                            else {
                                pieces.push(p);
                            }
                        }
 
                        this.set('cls', pieces.join(' '));
                    }
                    else if (state) {
                        this.addCls(cls);
                    }
                    else {
                        this.removeCls(cls);
                    }
                },
 
                // Override private methods from Model superclass
                privates: {
                    _noCls: [],
                    spacesRe: /\s+/,
                    
                    join: function(store) {
 
                        // Only the root node is linked to the TreeStore
                        if (store.isTreeStore) {
                            if (this.isRoot()) {
                                this.treeStore = this.store = store;
                            }
                        }
 
                        // Other stores are always joined.
                        // So a tree node could also be used by a flat store linked to a DataView
                        else {
                            this.callParent([store]);
                        }
                    },
 
                    // Used by Model base class methods to inform all interested Stores that the
                    // record has been mutated.
                    callJoined: function(funcName, args) {
                        this.callParent([funcName, args]);
                        this.callTreeStore(funcName, args);
                    },
 
                    _parseCls: function(cls) {
                        if (!cls) {
                            return this._noCls;
                        }
                        
                        if (typeof cls === 'string') {
                            return cls.split(this.spacesRe);
                        }
                        
                        return cls;
                    }
                }
            };
        }
    }
});