/**
* 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;
}
}
};
}
}
});