/** * 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 (i = 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 (i = 0; i < ln; i++) { result[i] = me.appendChild(node[i], suppressEvents, commit); } // Resume view updating and data syncing after appending all new children. // This will fire the add event to any views (if its the top level append) me.callTreeStore('endFill', [result]); } else { // Make sure it is a record node = me.createNode(node); /* eslint-disable-next-line max-len */ if (suppressEvents !== true && me.fireBubbledEvent('beforeappend', [me, node]) === false) { Ext.resumeLayouts(true); return false; } index = me.childNodes.length; oldParent = node.parentNode; // it's a move, make sure we move it cleanly if (oldParent) { /* eslint-disable-next-line max-len */ if (suppressEvents !== true && node.fireBubbledEvent('beforemove', [node, oldParent, me, index]) === false) { Ext.resumeLayouts(true); return false; } // Return false if a beforeremove listener vetoed the remove /* eslint-disable-next-line max-len */ if (oldParent.removeChild(node, false, suppressEvents, oldParent.getTreeStore() === treeStore) === false) { Ext.resumeLayouts(true); return false; } } // Coalesce sync operations across this operation // Node field setting (loaded, expanded) and node addition both trigger // a sync if autoSync is set. if (treeStore) { treeStore.beginUpdate(); } index = me.childNodes.length; if (index === 0) { me.setFirstChild(node); } me.childNodes[index] = node; node.parentNode = me; node.nextSibling = null; me.setLastChild(node); previousSibling = me.childNodes[index - 1]; if (previousSibling) { node.previousSibling = previousSibling; previousSibling.nextSibling = node; previousSibling.updateInfo(commit, { isLast: false }); // No need to trigger a ui update if we're doing a bulk update if (!bulkUpdate) { previousSibling.triggerUIUpdate(); } } else { node.previousSibling = null; } // Update the new child's info passing in info we already know childInfo.isFirst = index === 0; childInfo.index = index; // Integrate the new node into its new position. // It's not in the store yet, so we might need to // inform the store of significant field changes later. modifiedFields = node.updateInfo(commit, childInfo); // We stop being a leaf as soon as a node is appended if (me.isLeaf()) { me.set('leaf', false); } // As soon as we append a child to this node, we are loaded if (!me.isLoaded()) { if (bulkUpdate) { me.data.loaded = true; } else { me.set('loaded', true); } } else if (me.childNodes.length === 1 && !bulkUpdate) { me.triggerUIUpdate(); } // Ensure connectors are correct by updating the UI on all intervening // nodes (descendants) between last sibling and new node. if (index && me.childNodes[index - 1].isExpanded() && !bulkUpdate) { me.childNodes[index - 1].cascade(me.triggerUIUpdate); } // We register the subtree before we proceed so relayed events // (like nodeappend) from our TreeStore (if we have one) will be // able to use getNodeById. The node also needs to be added since // we're passing it in the events below. If we're not bulk updating, it // means we're just appending a node (with possible children), so do it // deeply here to ensure everything is captured. if (treeStore) { treeStore.registerNode(me, !bulkUpdate); if (bulkUpdate) { treeStore.registerNode(node); } } // This node MUST fire its events first, so that if the TreeStore's // onNodeAppend loads and appends local children, the events are still // in order. This node appended this child first, before the descendant // cascade. if (suppressEvents !== true) { me.fireBubbledEvent('append', [me, node, index]); if (oldParent) { node.fireBubbledEvent('move', [node, oldParent, me, index]); } } // Inform the TreeStore so that the node can be inserted // and registered. me.callTreeStore('onNodeAppend', [node, index]); // Now that the store contains the new node, we cam inform it of field // changes. if (modifiedFields) { node.callJoined('afterEdit', [modifiedFields]); } result = node; // Coalesce sync operations across this operation // Node field setting (loaded, expanded) and node addition both trigger // a sync if autoSync is set. if (treeStore) { treeStore.endUpdate(); } } // Flush layouts caused by updating of the UI Ext.resumeLayouts(true); return result; }, /** * Returns the tree this node is in. * @return {Ext.tree.Panel} The tree panel which owns this node. */ getOwnerTree: function() { var store = this.getTreeStore(); return store && store.ownerTree; }, /** * Returns the {@link Ext.data.TreeStore} which owns this node. * @return {Ext.data.TreeStore} The TreeStore which owns this node. */ getTreeStore: function() { var root = this; while (root && !root.treeStore) { root = root.parentNode; } return root && root.treeStore; }, /** * Removes a child node from this node. * @param {Ext.data.NodeInterface} node The node to remove * @param {Boolean} [erase=false] True to erase the record using the * configured proxy. * @param {Boolean} [suppressEvents] (private) * @param {Boolean} [isMove] (private) * @return {Ext.data.NodeInterface} The removed node */ removeChild: function(node, erase, suppressEvents, isMove) { var me = this, index = me.indexOf(node), i, childCount, previousSibling, treeStore = me.getTreeStore(), bulkUpdate = treeStore && treeStore.bulkUpdate, removeContext, removeRange = []; if (index === -1 || (suppressEvents !== true && me.fireBubbledEvent('beforeremove', [me, node, !!isMove]) === false)) { return false; } // Coalesce all layouts caused by node removal Ext.suspendLayouts(); // Coalesce sync operations across this operation if (treeStore) { treeStore.beginUpdate(); } // remove it from childNodes collection Ext.Array.erase(me.childNodes, index, 1); // update child refs if (me.firstChild === node) { me.setFirstChild(node.nextSibling); } if (me.lastChild === node) { me.setLastChild(node.previousSibling); } // Update previous sibling to point to its new next. previousSibling = node.previousSibling; if (previousSibling) { node.previousSibling.nextSibling = node.nextSibling; } // Update the next sibling to point to its new previous if (node.nextSibling) { node.nextSibling.previousSibling = node.previousSibling; // And if it's the new first child, let it know if (index === 0) { node.nextSibling.updateInfo(false, { isFirst: true }); } // Update subsequent siblings' index values for (i = 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 (i = 0; i < len; i++) { result.appendChild(me.childNodes[i].copy(undefined, true)); } } return result; }, /** * Clears the node. * @private * @param {Boolean} [erase=false] True to erase the node using the configured * proxy. * @param {Boolean} [resetChildren=false] True to reset child nodes */ clear: function(erase, resetChildren) { var me = this; // clear any references from the node me.parentNode = me.previousSibling = me.nextSibling = null; if (erase) { me.firstChild = me.lastChild = me.childNodes = null; } // This is used by TreeStore for clearing root node state on reload if (resetChildren) { me.firstChild = me.lastChild = null; me.childNodes.length = 0; if (me.data) { me.data.children = null; } } }, drop: function() { var me = this, childNodes = me.childNodes, parentNode = me.parentNode, treeStore = me.getTreeStore(), node, i, len; // Ensure Model operations are performed. // Store removal is NOT handled. // TreeStore's afterDrop does nothing. me.callParent(); // If called in recursion from here, there'll be no parentNode if (parentNode) { // TreeStore.onNodeRemove also adds invisible descendant nodes to the remove // tracking array. parentNode.removeChild(me); } // If we are the root, there'll be no parent node. It's a special case. We must // update the TreeStore's root with a null node. else if (me.get('root')) { treeStore.setRoot(null); } // Removing a node removes the node and all *VISIBLE* descendant nodes from the // Store and adds them to the remove tracking array. // // After this point, no descendant nodes have a connection to the TreeStore. // Coalesce sync operations across this operation if (treeStore) { treeStore.beginUpdate(); } // Recurse down dropping all descendants. // This will NOT remove them from the store's data collection for (i = 0, len = childNodes ? childNodes.length : 0; i < len; i++) { node = childNodes[i]; // Detach descendant nodes so that they do not all attempt to perform // removal from the parent. node.clear(); // Drop descendant nodes. node.drop(); } // Coalesce sync operations across this operation if (treeStore) { treeStore.endUpdate(); } }, /** * Destroys the node. * @param {Boolean} [options] (private) */ erase: function(options) { var me = this, childNodes = me.childNodes, len = childNodes && childNodes.length, i, node; // This unhooks this node from the tree structure // The UI is updated. // Now to recursively erase. me.remove(); // Clear removes linkage, so the erase's call into drop cannot recurse. // this method has to recurse to do all its stuff. me.clear(true); me.callParent([options]); for (i = 0; i < len; i++) { node = childNodes[i]; // The top level in the cascade is already removed. // Prevent the recursive erase calls doing further node removal. node.parentNode = null; node.erase(options); } }, /** * Inserts the first node before the second node in this nodes childNodes * collection. * @param {Ext.data.NodeInterface/Ext.data.NodeInterface[]/Object} node The node * to insert * @param {Ext.data.NodeInterface} refNode The node to insert before (if null the * node is appended) * @param {Boolean} [suppressEvents] (private) * @return {Ext.data.NodeInterface} The inserted node */ insertBefore: function(node, refNode, suppressEvents) { var me = this, index = me.indexOf(refNode), oldParent = node.parentNode, refIndex = index, childCount, previousSibling, i, treeStore = me.getTreeStore(), bulkUpdate = treeStore && treeStore.bulkUpdate, modifiedFields, sibling, siblingModifiedFields; if (!refNode) { // like standard Dom, refNode can be null for append return me.appendChild(node); } // nothing to do if (node === refNode) { return false; } // Make sure it is a record with the NodeInterface node = me.createNode(node); /* eslint-disable-next-line max-len */ if (suppressEvents !== true && me.fireBubbledEvent('beforeinsert', [me, node, refNode]) === false) { return false; } // when moving internally, indexes will change after remove if (oldParent === me && me.indexOf(node) < index) { refIndex--; } // it's a move, make sure we move it cleanly if (oldParent) { /* eslint-disable-next-line max-len */ if (suppressEvents !== true && node.fireBubbledEvent('beforemove', [node, oldParent, me, index, refNode]) === false) { return false; } // Return false if a beforeremove listener vetoed the remove /* eslint-disable-next-line max-len */ if (oldParent.removeChild(node, false, suppressEvents, oldParent.getTreeStore() === treeStore) === false) { return false; } } // Coalesce sync operations across this operation // Node field setting (loaded, expanded) and node addition both trigger a sync // if autoSync is set. // Nodes acquire a treeStore early now by virtue of getting a parentNode, so // set operations on them will arrive to this Store's onCollectionUpdate if (treeStore) { treeStore.beginUpdate(); } if (refIndex === 0) { me.setFirstChild(node); } Ext.Array.splice(me.childNodes, refIndex, 0, node); node.parentNode = me; node.nextSibling = refNode; refNode.previousSibling = node; previousSibling = me.childNodes[refIndex - 1]; if (previousSibling) { node.previousSibling = previousSibling; previousSibling.nextSibling = node; } else { node.previousSibling = null; } // Integrate the new node into its new position. // It's not in the store yet, so we might need to // inform the store of significant field changes later. modifiedFields = node.updateInfo(false, { parentId: me.getId(), index: refIndex, isFirst: refIndex === 0, isLast: false, depth: (me.data.depth || 0) + 1 }); // Update the index for all following siblings. for (i = 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 (i = 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 (i = 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 (i = 0; i < length; i++) { if (fn.apply(scope || this, args || [childNodes[i]]) === false) { break; } } }, /** * Finds the first child that has the attribute with the specified value. * @param {String} attribute The attribute name * @param {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 (i = 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 (p === 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 (i = 0; i < ln; i++) { n = childNodes[i]; n.previousSibling = childNodes[i - 1]; n.nextSibling = childNodes[i + 1]; // Update the index and first/last status of children info.isLast = (i === 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 (i = 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 (i = 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 (i = 0; i < ln; ++i) { node = allNodes[i]; if (!node.isLeaf()) { expandNodes[expandNodes.length] = node; } } ln = expandNodes.length; for (i = 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 (i = 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 (i = 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 (i = 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 (i = 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 (i = 0; i < ln; ++i) { node = collapseNodes[i]; if (i === 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 (i = 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 (i = 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; } } }; } }});