/** * 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 {String} iconCls * @inheritdoc Ext.panel.Header#iconCls * @localdoc Use {@link #icon} to set the icon src path directly. */ /** * @cfg {String} icon * @inheritdoc Ext.panel.Header#icon */ /** * @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. */ statics: { /** * This method allows you to decorate a Model's class to implement the NodeInterface. * This adds a set of methods, new events, new properties and new fields on every Record. * @param {Ext.Class/Ext.data.Model} model The Model class or an instance of the Model class you want to * decorate the prototype of. * @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()); 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 : '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 } ]); }, 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. reader = !storeReader || (nodeReader && nodeReader.initialConfig.typeProperty) ? nodeReader : storeReader; 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 */ updateInfo: function(commit, info) { var me = this, dataObject = me.data, oldDepth = dataObject.depth, childInfo = {}, children = me.childNodes, childCount = children.length, phantom = me.phantom, fields = me.fields, modified = me.modified || (me.modified = {}), propName, newValue, field, currentValue, key, newParentId = info.parentId, settingIndexInNewParent, persistentField, i; //<debug> if (!info) { Ext.raise('NodeInterface expects update info to be passed'); } //</debug> // Set the passed field values into the data object. // We do NOT need the expense of Model.set. We just need to ensure // that the dirty flag is set. for (propName in info) { field = fields[me.fieldOrdinals[propName]]; newValue = info[propName]; persistentField = field && field.persist; currentValue = dataObject[propName]; // If we are setting the index value, and the developer has changed it to be persistent, and the // new parent node is different to the starting one, it must be dirty. // The index may be the same value, but it's in a different parent. // This is so that a Writer can write the correct persistent fields which must include // the index to insert at if the parentId has changed. settingIndexInNewParent = persistentField && (propName === 'index') && (currentValue !== -1) && (newParentId && newParentId !== modified.parentId); // If new value is the same (unless we are setting the index in a new parent node), then skip the change. if (!settingIndexInNewParent && me.isEqual(currentValue, newValue)) { continue; } dataObject[propName] = newValue; // Only flag dirty when persistent fields are modified if (persistentField) { // Already modified, just check if we've reverted it back to start value (unless we are setting the index in a new parent node) if (!settingIndexInNewParent && modified.hasOwnProperty(propName)) { // If we have reverted to start value, possibly clear dirty flag if (me.isEqual(modified[propName], newValue)) { // The original value in me.modified equals the new value, so // the field is no longer modified: delete modified[propName]; // We might have removed the last modified field, so check to // see if there are any modified fields remaining and correct // me.dirty: me.dirty = false; for (key in modified) { if (modified.hasOwnProperty(key)){ me.dirty = true; break; } } } } // Not already modified, set dirty flag else { me.dirty = true; modified[propName] = currentValue; } } } if (commit) { me.commit(); me.phantom = phantom; } // The only way child data can be influenced is if this node has changed level in this update. if (me.data.depth !== oldDepth) { childInfo = { depth: me.data.depth + 1 }; for (i = 0; i < childCount; i++) { children[i].updateInfo(commit, childInfo); } } }, /** * Returns true if this node is the last child of its parent * @return {Boolean} */ isLast: function() { return this.get('isLast'); }, /** * Returns true if this node is the first child of its parent * @return {Boolean} */ isFirst: function() { return this.get('isFirst'); }, /** * Returns true if this node has one or more child nodes, else false. * @return {Boolean} */ hasChildNodes: function() { return !this.isLeaf() && this.childNodes.length > 0; }, /** * Returns true if this node has one or more child nodes, or if the <tt>expandable</tt> * node attribute is explicitly specified as true, 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, i, ln, index, oldParent, previousSibling, childInfo = { isLast: true, parentId: me.getId(), depth: (me.data.depth||0) + 1 }, result, treeStore = me.getTreeStore(), bulkUpdate = treeStore && treeStore.bulkUpdate; // 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); if (suppressEvents !== true && me.fireEventArgs('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) { if (suppressEvents !== true && node.fireEventArgs('beforemove', [node, oldParent, me, index]) === false) { Ext.resumeLayouts(true); return false; } // Return false if a beforeremove listener vetoed the remove 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; 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].cascadeBy(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.fireEventArgs('append', [me, node, index]); if (oldParent) { node.fireEventArgs('move', [node, oldParent, me, index]); } } // Inform the TreeStore so that the node can be inserted // and registered. me.callTreeStore('onNodeAppend', [node, index]); 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(); if (store) { return 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. * @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; if (index === -1 || (suppressEvents !== true && me.fireEventArgs('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 (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.cascadeBy(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]); node.previousSibling = node.nextSibling = node.parentNode = null; me.fireEventArgs('remove', [me, node, !!isMove, removeContext]); // Inform the TreeStore so that the node unregistered and unjoined. me.callTreeStore('onNodeRemove', [[node], !!isMove]); } // 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. // to be able to 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} [id] A new id, defaults to this Node's id. * @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, deep) { var me = this, result = me.callParent([newId]), len = me.childNodes ? me.childNodes.length : 0, i; // Move child nodes across to the copy if required if (deep) { for (i = 0; i < len; i++) { result.appendChild(me.childNodes[i].copy(undefined, true)); } } return result; }, /** * Clears the node. * @private * @param {Boolean} [erase=false] True to erase the node using the configured * proxy. */ clear: function(erase) { 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; } }, drop: function() { var me = this, childNodes = me.childNodes, parentNode = me.parentNode, len = childNodes ? childNodes.length : 0, i, node, treeStore; // 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 = me.getTreeStore(); // 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 = me.getTreeStore(); 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 (i = 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. */ 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) * @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; if (!refNode) { // like standard Dom, refNode can be null for append return me.appendChild(node); } // nothing to do if (node === refNode) { return false; } // Make sure it is a record with the NodeInterface node = me.createNode(node); if (suppressEvents !== true && me.fireEventArgs('beforeinsert', [me, node, refNode]) === false) { return false; } // when moving internally, indexes will change after remove if (oldParent === me && me.indexOf(node) < index) { refIndex--; } // it's a move, make sure we move it cleanly if (oldParent) { if (suppressEvents !== true && node.fireEventArgs('beforemove', [node, oldParent, me, index, refNode]) === false) { return false; } // Return false if a beforeremove listener vetoed the remove 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. 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++) { me.childNodes[i].updateInfo(false, { index: i }); } 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.fireEventArgs('insert', [me, node, refNode]); if (oldParent) { node.fireEventArgs('move', [node, oldParent, me, refIndex, refNode]); } } // Inform the TreeStore so that the node can be registered and added me.callTreeStore('onNodeInsert', [node, refIndex]); // 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); } }, /** * @private * Used by {@link Ext.tree.Column#initTemplateRendererData} to determine whether a node is the last *visible* * sibling. * */ 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. * * @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. * @return {Ext.data.NodeInterface} this * @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; // Avoid all this if nothing to remove if (!len) { return; } // 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]); } } for (i = 0; i < len; ++i) { node = childNodes[i]; node.previousSibling = node.nextSibling = node.parentNode = null; me.fireEventArgs('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]); // 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 * @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} node * @return {Number} The index of the node or -1 if it was not found */ indexOf: function(child) { return Ext.Array.indexOf(this.childNodes, child); }, /** * Returns 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 || '/'; 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; } }, //<deprecated since=0.99> cascade: function() { if (Ext.isDefined(Ext.global.console)) { Ext.global.console.warn('Ext.data.Node: cascade has been deprecated. Please use cascadeBy instead.'); } return this.cascadeBy.apply(this, arguments); }, //</deprecated> /** * Cascades down the tree from this node, calling the specified 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} spec An object containing before and after functions, scope and an argument list. * @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. */ cascadeBy: function(before, scope, args, after) { var me = this; if (arguments.length === 1 && !Ext.isFunction(before)) { after = before.after; scope = before.scope; args = before.args; before = before.before; } if (!before || before.apply(scope || me, args || [me]) !== false) { var childNodes = me.childNodes, length = childNodes.length, i; for (i = 0; i < length; i++) { childNodes[i].cascadeBy.call(childNodes[i], before, scope, args, after); } if (after) { after.apply(scope || me, args || [me]); } } }, /** * 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; }, 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, len = cs.length, i = 0, n, res; for (; i < len; i++) { n = cs[i]; if (fn.call(scope || n, n) === true) { return n; } else if (deep) { res = n.findChildBy(fn, scope, deep); if (res !== null) { return res; } } } return null; }, /** * Returns true if this node is an ancestor (at any point) of the passed node. * @param {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, i, n, info = { isFirst: true }; 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.fireEventArgs('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.cascadeBy(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.fireEventArgs('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'); resumeAddEvent = true; } } } // Inform the TreeStore that we intend to expand, and that it should call onChildNodesAvailable // when the child nodes are available 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'); 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 beforeexpand listener fired by {@link #method-expand} when the child nodes have been loaded and appended. */ onChildNodesAvailable: function(records, recursive, callback, scope) { var me = this, treeStore = me.getTreeStore(), bulkUpdate = treeStore && treeStore.bulkUpdate, ancestor, i, collapsedAncestors; // 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) { 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) { 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.fireEventArgs('expand', [ancestor, ancestor.childNodes]); } } else { // TreeStore's onNodeExpand inserts the child nodes below the parent me.callTreeStore('onNodeExpand', [records, false]); } me.fireEventArgs('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 scope to run the callback in */ expandChildren: function(recursive, callback, scope, /* private */ 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.fireEventArgs('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) { 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) { 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.fireEventArgs('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.fireEventArgs('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.fireEventArgs('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, i, allNodes = me.childNodes, ln = allNodes.length, collapseNodes = [], node; // 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.fireEventArgs(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. fireEventArgs: function(eventName, args) { // Use the model prototype directly. If we have a BaseModel and then a SubModel, // if we access the superclass fireEventArgs it will just refer to the same method // and we end up in an infinite loop. var fireEventArgs = Ext.mixin.Observable.prototype.fireEventArgs, result, eventSource, topNode; // The event bubbles (all native NodeInterface events do)... if (bubbledEvents[eventName]) { for (eventSource = this; result !== false && eventSource; eventSource = (topNode = eventSource).parentNode) { if (eventSource.hasListeners && eventSource.hasListeners[eventName]) { result = 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 - call superclass fireEventArgs method else { return 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); } }, // Override private methods from Model superclass privates: { 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); } } }; } }});