/**
* The TreeStore is a store implementation that owns the {@link #cfg-root root node} of
* a tree, and provides methods to load either local or remote data as child nodes of the root
* and any descendant non-leaf node.
*
* The TreeStore must be used as the store of a {@link Ext.tree.Panel tree panel}.
*
* This class also relays many node events from the underlying node structure.
*
* # Using Models
*
* If no Model is specified, an implicit model will be created that extends
* {@link Ext.data.TreeModel}. The standard Tree fields will also be copied onto the Model
* for maintaining their state. These fields are listed in the {@link Ext.data.NodeInterface}
* documentation.
*
* # Reading Nested Data
*
* For the tree to read nested data, the {@link Ext.data.reader.Reader} must be configured
* with a root property, so the reader can find nested data for each node (if a root is not
* specified, it will default to 'children'). This will tell the tree to look for any nested tree
* nodes by the same keyword, i.e., 'children'. If a root is specified in the config make sure
* that any nested nodes with children have the same name.
*
* **Note:** Setting {@link #defaultRootProperty} accomplishes the same thing.
*
* #rootProperty as a Function
* You can pass a function as the data reader's rootProperty when the tree's dataset has
* mixed root properties. Child nodes can then be programmatically determined at read time.
*
* For example, the child nodes may be passed via the 'children' property
* name, though you may have a top-level root property of 'items'.
*
* See {@link Ext.data.reader.Reader#rootProperty rootProperty} for more information.
*
* #Filtering#
* Filtering of nodes in a TreeStore is hierarchically top down by default. This means that
* if a non-leaf node does not pass the filter, then it, and all its descendants are filtered *out*
* of the store.
*
* To reverse this, so that any node which passes the filter causes all its ancestors to be visible,
* configure the `TreeStore` with '{@link #cfg-filterer filterer: 'bottomup'}`
*
* You may also programmatically filter individual tree nodes by setting their `'visible'` field.
*
* Setting this to `false` filters the node out so that it will not appear in the UI. Setting it
* to `true` filters the node in.
*
* Note that if performing several filter operations, it is best to {@link #method-suspendEvents}
* on the store first, and when all nodes have been modified, {@link #method-resumeEvents} and fire
* the {@link #event-refresh} event on the store.
*/
Ext.define('Ext.data.TreeStore', {
extend: 'Ext.data.Store',
alias: 'store.tree',
requires: [
'Ext.util.Sorter',
'Ext.data.TreeModel',
'Ext.data.NodeInterface'
],
/**
* @property {Boolean} isTreeStore
* `true` in this class to identify an object as an instantiated TreeStore, or subclass thereof.
*/
isTreeStore: true,
config: {
/**
* @cfg {Ext.data.TreeModel/Ext.data.NodeInterface/Object} root
* @accessor
* The root node for this store. For example:
*
* root: {
* expanded: true,
* text: "My Root",
* children: [
* { text: "Child 1", leaf: true },
* { text: "Child 2", expanded: true, children: [
* { text: "GrandChild", leaf: true }
* ] }
* ]
* }
*
* Setting the `root` config option is the same as calling {@link #setRootNode}.
*
* It's important to note that setting expanded to true on the root node will
* cause the tree store to attempt to load. This will occur regardless the value
* of {@link Ext.data.ProxyStore#autoLoad autoLoad}. If you you do not want the
* store to load on instantiation, ensure expanded is false and load the store
* when you're ready.
*
*/
root: null,
/**
* @cfg {Boolean} rootVisible
* `false` to not include the root node in this Stores collection.
* @accessor
*/
rootVisible: false,
/**
* @cfg {String} defaultRootProperty
*/
defaultRootProperty: 'children',
/**
* @cfg {String} parentIdProperty
* This config allows node data to be returned from the server in linear format
* without having to structure it into `children` arrays.
*
* This property specifies which property name in the raw node data yields the id
* of the parent node.
*
* For example the following data would be read into a geographic tree by
* configuring the TreeStore with `parentIdProperty: 'parentId'`. The node data
* contains an upward link to a parent node.
*
* data: [{
* name: 'North America',
* id: 'NA'
* }, {
* name: 'Unites States',
* id: 'USA',
* parentId: 'NA'
* }, {
* name: 'Redwood City',
* leaf: true,
* parentId: 'USA'
* }, {
* name: 'Frederick, MD',
* leaf: true,
* parentId: 'USA'
* }]
*
*/
parentIdProperty: null,
/**
* @cfg {Boolean} clearOnLoad
* Remove previously existing child nodes before loading.
*/
clearOnLoad: true,
/**
* @cfg {Boolean} clearRemovedOnLoad
* If `true`, when a node is reloaded, any records in the {@link #removed} record
* collection that were previously descendants of the node being reloaded will be
* cleared from the {@link #removed} collection. Only applicable if
* {@link #clearOnLoad} is `true`.
*/
clearRemovedOnLoad: true,
/**
* @cfg {String} nodeParam
* The name of the parameter sent to the server which contains the identifier of
* the node.
*/
nodeParam: 'node',
/**
* @cfg {String} defaultRootId
* The default root id.
*/
defaultRootId: 'root',
/**
* @cfg {String} defaultRootText
* The default root text (if not specified)
*/
defaultRootText: 'Root',
/**
* @cfg {Boolean} folderSort
* Set to true to automatically prepend a leaf sorter.
*/
folderSort: false,
/**
* @cfg {Number} pageSize
* @hide
*/
pageSize: null // Not valid for TreeStore. Paging parameters must not be passed.
},
/**
* @cfg {String} filterer
* The order in which to prioritize how filters are applied to nodes.
*
* The default, `'topdown'` means that if a parent node does *not* pass the filter,
* then the branch ends there, and no descendant nodes are filtered in, even if they
* would pass the filter.
*
* By specifying `'bottomup'`, if a leaf node passes the filter, then all its
* ancestor nodes are filtered in to allow it to be visible.
* @since 6.0.2
*/
filterer: 'topdown',
/**
* @cfg {Boolean} lazyFill
* Set to true to prevent child nodes from being loaded until the the node is
* expanded or loaded explicitly.
*/
lazyFill: false,
fillCount: 0,
bulkUpdate: 0,
nodesToUnregister: 0,
/**
* @cfg fields
* @inheritdoc Ext.data.Model#cfg-fields
*
* @localdoc **Note:** If you wish to create a Tree*Grid*, and configure your tree
* with a {@link Ext.panel.Table#cfg-columns columns} configuration, it is possible
* to define the set of fields you wish to use in the Store instead of configuring
* the store with a {@link #cfg-model}.
*
* By default, the Store uses an {@link Ext.data.TreeModel}. If you configure
* fields, it uses a subclass of {@link Ext.data.TreeModel} defined with the set of
* fields that you specify (in addition to the fields which it uses for storing
* internal state).
*/
_silentOptions: {
silent: true
},
/**
* @property implicitModel
* @inheritdoc
*/
implicitModel: 'Ext.data.TreeModel',
/**
* @cfg {String} groupField
* @hide
*/
groupField: null,
/**
* @cfg {String} groupDir
* @hide
*/
groupDir: null,
/**
* @cfg {Object/Ext.util.Grouper} grouper
* @hide
*/
grouper: null,
constructor: function(config) {
var me = this;
me.byIdMap = {};
me.callParent([config]);
// The following events are fired on this TreeStore by the bubbling
// from NodeInterface.fireEvent
/**
* @event nodeappend
* @inheritdoc Ext.data.NodeInterface#append
*/
/**
* @event noderemove
* @inheritdoc Ext.data.NodeInterface#remove
*/
/**
* @event nodemove
* @inheritdoc Ext.data.NodeInterface#move
*/
/**
* @event nodeinsert
* @inheritdoc Ext.data.NodeInterface#insert
*/
/**
* @event nodebeforeappend
* @inheritdoc Ext.data.NodeInterface#beforeappend
*/
/**
* @event nodebeforeremove
* @inheritdoc Ext.data.NodeInterface#beforeremove
*/
/**
* @event nodebeforemove
* @inheritdoc Ext.data.NodeInterface#beforemove
*/
/**
* @event nodebeforeinsert
* @inheritdoc Ext.data.NodeInterface#beforeinsert
*/
/**
* @event nodeexpand
* @inheritdoc Ext.data.NodeInterface#expand
*/
/**
* @event nodecollapse
* @inheritdoc Ext.data.NodeInterface#collapse
*/
/**
* @event nodebeforeexpand
* @inheritdoc Ext.data.NodeInterface#beforeexpand
*/
/**
* @event nodebeforecollapse
* @inheritdoc Ext.data.NodeInterface#beforecollapse
*/
/**
* @event nodesort
* @inheritdoc Ext.data.NodeInterface#sort
*/
//<debug>
if (Ext.isDefined(me.nodeParameter)) {
if (Ext.isDefined(Ext.global.console)) {
Ext.global.console.warn('Ext.data.TreeStore: nodeParameter has been deprecated. ' +
'Please use nodeParam instead.');
}
me.nodeParam = me.nodeParameter;
delete me.nodeParameter;
}
//</debug>
},
/**
* @event rootchange
* Fires any time the tree's root node changes.
* @param {Ext.data.TreeModel/Ext.data.NodeInterface} newRoot The new root
* @param {Ext.data.TreeModel/Ext.data.NodeInterface} oldRoot The old root
*/
applyFields: function(fields, oldFields) {
var me = this;
if (fields) {
if (me.defaultRootProperty !== me.self.prototype.config.defaultRootProperty) {
// Use concat. Must not mutate incoming configs
fields = fields.concat({
name: me.defaultRootProperty,
type: 'auto',
defaultValue: null,
persist: false
});
}
}
me.callParent([fields, oldFields]);
},
applyGroupField: function(field) {
return null;
},
applyGroupDir: function(dir) {
return null;
},
setGrouper: function(grouper) {
//<debug>
if (grouper) {
Ext.raise('You can\'t group a TreeStore');
}
//</debug>
return null;
},
/**
* @hide
*/
group: Ext.emptyFn,
// TreeStore has to do right things upon SorterCollection update
onSorterEndUpdate: function() {
var me = this,
sorterCollection = me.getSorters(),
sorters = sorterCollection.getRange(),
rootNode = me.getRoot(),
folderSort = me.getFolderSort();
me.fireEvent('beforesort', me, sorters);
// Only load or sort if there are sorters
if (rootNode && (folderSort || sorters.length)) {
if (me.getRemoteSort()) {
if (sorters.length) {
me.load({
callback: function() {
me.fireEvent('sort', me, sorters);
}
});
}
}
else {
rootNode.sort(this.getSortFn(), true);
// Don't fire the event if we have no sorters
me.fireEvent('datachanged', me);
me.fireEvent('refresh', me);
me.fireEvent('sort', me, sorters);
}
}
// Sort event must fire when sorters collection is updated to empty.
else {
me.fireEvent('sort', me, sorters);
}
},
updateFolderSort: function(folderSort) {
this.needsFolderSort = folderSort;
this.onSorterEndUpdate();
},
getSortFn: function() {
return this._sortFn || (this._sortFn = this.createSortFn());
},
createSortFn: function() {
var me = this,
sortersSortFn = this.sorters.getSortFn();
return function(node1, node2) {
var node1FolderOrder, node2FolderOrder,
result = 0;
if (me.needsFolderSort) {
// Primary comparator puts Folders before leaves.
node1FolderOrder = node1.data.leaf ? 1 : 0;
node2FolderOrder = node2.data.leaf ? 1 : 0;
result = node1FolderOrder - node2FolderOrder;
}
if (me.needsIndexSort && result === 0) {
result = node1.data.index - node2.data.index;
}
return result || sortersSortFn(node1, node2);
};
},
getTotalCount: function() {
return this.getCount();
},
afterEdit: function(node, modifiedFieldNames) {
var me = this,
parentNode = node.parentNode,
rootVisible = me.getRootVisible(),
isHiddenRoot = !parentNode && !rootVisible,
prevVisibleNodeIndex,
isVisible = node.get('visible'),
toAdd, removeStart;
// If the node visibility flag is not matched by the Store state, correct it.
// The hidden root node is a special case. That never appears in the flat store
// so skip processing for that.
if (!isHiddenRoot && isVisible !== me.contains(node)) {
// If we are restoring the node to visibility, then insert
// at the correct point if this TreeStore considers the node visible
// (visible flag set, and all ancestors expanded and visible)
if (isVisible) {
if (!parentNode || me.isVisible(node)) {
toAdd = [node];
// Collect visible descendants. Same operation as expanding
if (node.isExpanded()) {
me.handleNodeExpand(node, node.childNodes, toAdd);
}
prevVisibleNodeIndex = node.previousSibling
? me.indexOfPreviousVisibleNode(node.previousSibling)
: (parentNode ? me.indexOf(parentNode) : -1);
me.insert(prevVisibleNodeIndex + 1, toAdd);
}
}
// If we are hiding the node, remove it and all its descendants.
else {
removeStart = me.indexOf(node);
me.removeAt(removeStart, me.indexOfNextVisibleNode(node) - removeStart);
}
}
// Modification of other fields must lead to node filtering if we are
// local filtering. Update the flat store though onFilterEndUpdate.
// Data modification takes place during initial setup of root node
// so ignore that.
else if (me.getRoot() && me.needsLocalFilter()) {
me.onFilterEndUpdate(me.getFilters());
}
me.callParent([node, modifiedFieldNames]);
},
afterReject: function(record) {
var me = this;
// Must pass the 5th param (modifiedFieldNames) as null, otherwise the
// event firing machinery appends the listeners "options" object to the arg list
// which may get used as the modified fields array by a handler.
// This array is used for selective grid cell updating by Grid View.
// Null will be treated as though all cells need updating.
if (me.contains(record)) {
me.onUpdate(record, Ext.data.Model.REJECT, null);
me.fireEvent('update', me, record, Ext.data.Model.REJECT, null);
}
},
afterCommit: function(record, modifiedFieldNames) {
var me = this;
if (!modifiedFieldNames) {
modifiedFieldNames = null;
}
if (me.contains(record)) {
me.onUpdate(record, Ext.data.Model.COMMIT, modifiedFieldNames);
me.fireEvent('update', me, record, Ext.data.Model.COMMIT, modifiedFieldNames);
}
},
updateRootVisible: function(rootVisible) {
var rootNode = this.getRoot(),
data;
if (rootNode) {
data = this.getData();
if (rootVisible) {
data.insert(0, rootNode);
}
else {
data.remove(rootNode);
}
}
},
updateTrackRemoved: function(trackRemoved) {
this.callParent(arguments);
this.removedNodes = this.removed;
this.removed = null;
},
onDestroyRecords: function(records, operation, success) {
if (success) {
this.removedNodes.length = 0;
}
},
updateProxy: function(proxy) {
var reader;
// The proxy sets a parameter to carry the entity ID based upon the Operation's id
// That parameter name defaults to "id".
// TreeStore however uses a nodeParam configuration to specify the entity id
if (proxy) {
if (proxy.setIdParam) {
proxy.setIdParam(this.getNodeParam());
}
// Readers in a TreeStore's proxy have to use a special rootProperty which defaults
// to "children"
reader = proxy.getReader();
if (Ext.isEmpty(reader.getRootProperty())) {
reader.setRootProperty(this.getDefaultRootProperty());
}
}
},
setProxy: function(proxy) {
this.changingProxy = true;
this.callParent([proxy]);
this.changingProxy = false;
},
updateModel: function(model) {
var isNode;
if (model) {
isNode = model.prototype.isNode;
// Ensure that the model has the required interface to function as a tree node.
Ext.data.NodeInterface.decorate(model);
// If we just had to decorate a raw Model to upgrade it to be a NodeInterface
// then we need to build new extractor functions on the reader.
if (!isNode && !this.changingProxy) {
this.getProxy().getReader().buildExtractors(true);
}
}
},
onCollectionFilter: Ext.emptyFn,
// We add listeners to the FilterCollection and do the filtering in a hierarchical
// way. We are not interested in notifications as an observer on the data collection.
onFilterEndUpdate: function(filters) {
var me = this,
length = filters.length,
root = me.getRoot(),
childNodes, childNode,
filteredNodes, i;
if (!me.getRemoteFilter()) {
if (length) {
me.doFilter(root);
}
else {
root.cascade({
after: function(node) {
// Set visible field silently: do not fire update events to views.
// Views will receive refresh event from onNodeFilter.
node.set('visible', true, me._silentOptions);
}
});
}
if (length) {
filteredNodes = [];
childNodes = root.childNodes;
for (i = 0, length = childNodes.length; i < length; i++) {
childNode = childNodes[i];
if (childNode.get('visible')) {
filteredNodes.push(childNode);
}
}
}
else {
filteredNodes = root.childNodes;
}
me.onNodeFilter(root, filteredNodes);
root.fireEvent('filterchange', root, filteredNodes);
// Inhibit AbstractStore's implementation from firing the refresh event.
// We fire it in the onNodeFilter.
me.suppressNextFilter = true;
me.callParent([filters]);
me.suppressNextFilter = false;
}
else {
me.callParent([filters]);
}
},
/**
* @private
*
* Called from filter/clearFilter. Refreshes the view based upon
* the new filter setting.
*/
onNodeFilter: function(root, childNodes) {
var me = this,
data = me.getData(),
toAdd = [];
// Honour rootVisible.
if (me.getRootVisible() && root.get('visible')) {
toAdd.push(root);
}
me.handleNodeExpand(root, childNodes, toAdd);
// Do not relay the splicing's add&remove events.
// We inform interested parties about filtering through a refresh event.
me.suspendEvents();
data.splice(0, data.getCount(), toAdd);
me.resumeEvents();
if (!me.suppressNextFilter) {
me.fireEvent('datachanged', me);
me.fireEvent('refresh', me);
}
},
/**
* Called from a node's expand method to ensure that child nodes are available.
*
* This ensures that the child nodes are available before calling the passed callback.
* @private
* @param {Ext.data.NodeInterface} node The node being expanded.
* @param {Function} callback The function to run after the expand finishes
* @param {Object} scope The scope in which to run the callback function
* @param {Array} args The extra args to pass to the callback after the new child nodes
*/
onBeforeNodeExpand: function(node, callback, scope, args) {
var me = this,
storeReader, nodeProxy, nodeReader, reader, children, callbackArgs;
// childNodes are loaded: go ahead with expand
// This will also expand phantom nodes with childNodes.
if (node.isLoaded()) {
callbackArgs = [node.childNodes];
if (args) {
callbackArgs.push.apply(callbackArgs, args);
}
Ext.callback(callback, scope || node, callbackArgs);
}
// The node is loading
else if (node.isLoading()) {
me.on('load', function() {
callbackArgs = [node.childNodes];
if (args) {
callbackArgs.push.apply(callbackArgs, args);
}
Ext.callback(callback, scope || node, callbackArgs);
}, me, {
single: true,
priority: 1001
});
}
// There are unloaded child nodes in the raw data because of the lazy configuration,
// load them, then call back.
else {
// With heterogeneous nodes, different levels may require differently configured
// readers to extract children. For example a "Disk" node type may configure its proxy
// reader with root: 'folders', while a "Folder" node type might configure its proxy
// reader with root: 'files'. Or the root property could be a configured-in accessor.
storeReader = me.getProxy().getReader();
nodeProxy = node.getProxy();
nodeReader = nodeProxy ? nodeProxy.getReader() : null;
// If the node's reader was configured with a special root (property name which defines
// the children array) use that.
reader = nodeReader && nodeReader.initialConfig.rootProperty ? nodeReader : storeReader;
// 1. If the raw data read in for the node contains a root (children array), then
// read it.
// 2. If a phantom w/o any children, it should still be processed if expanded so check
// for that here as well. See EXTJS-13509.
children = reader.getRoot(node.raw || node.data);
// Load locally if there are local children, or it's a phantom (client side only) node.
// Ensure that programmatically added new root nodes which could be phantom are able to
// kick off remote requests.
if (children || (node.phantom && !node.isRoot())) {
// If node is expanded when children are not set, pass an empty array.
// See EXTJS-26086.
// Extract records from the raw data. Allow the node being expanded to dictate
// its child type
me.fillNode(node, reader.extractData(children || [], {
model: node.childType,
recordCreator: me.recordCreator
}));
callbackArgs = [node.childNodes];
if (args) {
callbackArgs.push.apply(callbackArgs, args);
}
Ext.callback(callback, scope || node, callbackArgs);
}
// Node needs loading
else {
me.read({
node: node,
// We use onChildNodesAvailable here because we want trigger to
// the loading event after we've loaded children
onChildNodesAvailable: function() {
// Clear the callback, since if we're introducing a custom one,
// it may be re-used on reload
delete me.lastOptions.onChildNodesAvailable;
callbackArgs = [node.childNodes];
if (args) {
callbackArgs.push.apply(callbackArgs, args);
}
Ext.callback(callback, scope || node, callbackArgs);
}
});
// Requests for node expansion must be immediate
me.flushLoad();
}
}
},
// Called from a node's onChildNodesAvailable method to
// insert the newly available child nodes below the parent.
onNodeExpand: function(parent, records) {
var me = this,
insertIndex = me.indexOf(parent) + 1,
toAdd = [];
me.handleNodeExpand(parent, records, toAdd);
// If a hidden root is being expanded for the first time, it's not an insert operation
if (!me.refreshCounter && parent.isRoot() && !parent.get('visible')) {
me.loadRecords(toAdd);
}
// The add event from this insertion is handled by TreeView.onAdd.
// That implementation calls parent and then ensures the previous sibling's joining lines
// are correct. Apart from the above code path, TreeStores, do not get a load event. A root
// node may or may not be inserted, and from then on, chld nodes are added and removed.
// Views interrogate loadCount so, TreeStores with any data must appear to be loaded.
else {
++me.loadCount;
me.insert(insertIndex, toAdd);
}
},
// Collects child nodes to remove into the passed toRemove array.
// When available, all descendant nodes are pushed into that array using recursion.
handleNodeExpand: function(parent, records, toAdd) {
var me = this,
ln = records ? records.length : 0,
i, record;
// If parent is not visible, nothing to do (unless parent is the root)
if (parent !== this.getRoot() && !me.isVisible(parent)) {
return;
}
if (ln) {
// The view items corresponding to these are rendered.
// Loop through and expand any of the non-leaf nodes which are expanded
for (i = 0; i < ln; i++) {
record = records[i];
// If the TreePanel has not set its visible flag to false, add to new node array
if (record.get('visible')) {
// Add to array being collected by recursion when child nodes are loaded.
// Must be done here in loop so that child nodes are inserted into the stream
// in place in recursive calls.
toAdd.push(record);
if (record.isExpanded()) {
if (record.isLoaded()) {
// Take a shortcut - appends to toAdd array
me.handleNodeExpand(record, record.childNodes, toAdd);
}
else {
// Might be asynchronous if child nodes are not immediately available
// MUST set expanded silently, otherwise we will receive notification
// and attempt to update the UI.
record.set('expanded', false, { silent: true });
record.expand();
}
}
}
}
}
},
/**
* @private
* Called from a node's collapse method
*/
onNodeCollapse: function(parent, records, callback, scope) {
var me = this,
collapseIndex = me.indexOf(parent) + 1,
lastNodeIndexPlus;
// Only remove what is visible and therefore in the collection side of this store
if (me.needsLocalFilter()) {
records = Ext.Array.filter(records, me.filterVisible);
}
// Only attempt to remove the records if they are there.
// Collapsing an ancestor node *immediately removes from the view, ALL its descendant nodes
// at all levels*.
// But if the collapse was recursive, all descendant root nodes will still fire their
// events. But we must ignore those events here - we have nothing to do.
if (records.length && me.isVisible(parent)) {
// Calculate the index *one beyond* the last node we are going to remove.
lastNodeIndexPlus = me.indexOfNextVisibleNode(parent);
// Remove the whole collapsed node set.
me.removeAt(collapseIndex, lastNodeIndexPlus - collapseIndex);
}
Ext.callback(callback, scope);
},
/**
* @private
* Gets the index of next visible node at either the same sibling level or a higher level.
*
* This is to facilitate bulk removal of visible descendant nodes. eg in the following case
* TreeStore.indexOfNextVisibleNode(bletch) must return indexOf(belch) - the next sibling.
*
* But TreeStore.indexOfNextVisibleNode(blivit) and TreeStore.indexOfNextVisibleNode(screeble)
* and TreeStore.indexOfNextVisibleNode(poot) must also return return indexOf(belch)
*
* foo
* ├ bar
* ├ bletch
* │ ├ zarg
* │ └ blivit
* │ ├ ik
* │ └ screeble
* │ ├ raz
* │ └ poot
* ├ belch
* apresfoo
*
* This is so that removal of nodes at full depth can be optimized into one
* removeAt(start, length) call.
*/
indexOfNextVisibleNode: function(node) {
var result;
while (node.parentNode) {
// Find the next visible sibling (filtering may have knocked out intervening nodes)
/* eslint-disable-next-line max-len */
for (result = node.nextSibling; result && !result.get('visible'); result = result.nextSibling) {
// This block is intentionally left blank
}
// If found, we're done.
if (result) {
return this.indexOf(result);
}
// If there is no next sibling, we try to find the parent node's next visible sibling.
node = node.parentNode;
}
// No subsequent visible siblings
return this.getCount();
},
/**
* @private
* Gets the index of previous visible node at either the same sibling level or a higher level
* *inclusive* of passed node.
*
* This is to facilitate insertion of nodes in a filtered tree. eg in the following case
* TreeStore.indexOfPreviousVisibleNode(bletch) must return indexOf(bar) - the previous sibling.
*
* But TreeStore.indexOfPreviousVisibleNode(belch) must return indexOf(raz) because
* poot is filtered out of visibility.
*
* foo
* ├ bar
* ├ bletch
* │ ├ zarg
* │ └ blivit
* │ ├ ik
* │ │ ├screeble
* │ │ └ raz
* │ └ poot<filtered out>
* ├ belch
* apresfoo
*
*/
indexOfPreviousVisibleNode: function(node) {
var result;
// Find the previous visible sibling (filtering may have knocked out intervening nodes)
for (result = node; result && !result.get('visible'); result = result.previousSibling) {
// This block is intentionally left blank
}
// If found, and there are child nodes, do the same operation on the last child
if (result) {
if (result.isExpanded() && result.lastChild) {
return this.indexOfPreviousVisibleNode(result.lastChild);
}
}
// If there is no previous visible sibling, we use the parent node.
// We only even ATTEMPT to insert into the flat store children of visible nodes.
else {
result = node.parentNode;
}
return this.indexOf(result);
},
/**
* @private
* Filter function for new records.
*/
filterNew: function(item) {
// Root nodes are always generated on the client side, and therefore phantom.
// But they should never be included in the new records list.
return !item.get('root') && this.callParent([item]);
},
/**
* @private
* Filter function for rejected records.
*/
filterRejects: function(item) {
// Root nodes are always generated on the client side, and therefore phantom.
// But they should never be included in the rejects list.
return !item.get('root') && this.callParent([item]);
},
getNewRecords: function() {
return Ext.Array.filter(Ext.Object.getValues(this.byIdMap), this.filterNew, this);
},
getRejectRecords: function() {
return Ext.Array.filter(Ext.Object.getValues(this.byIdMap), this.filterRejects, this);
},
getUpdatedRecords: function() {
return Ext.Array.filter(Ext.Object.getValues(this.byIdMap), this.filterUpdated);
},
// Called from a node's removeChild & removeAll methods *before* the node(s) is/are unhooked
// from siblings and parent. We calculate the range of visible nodes affected by the removal.
// For example in the tree below, if the "bletch" node was being removed, we would have to
// remove bletch, zarg, blivit, ik, screeble, razz and poot.
//
// foo
// ├ bar
// ├ bletch
// │ ├ zarg
// │ └ blivit
// │ ├ ik
// │ └ screeble
// │ ├ raz
// │ └ poot
// ├ belch
// apresfoo
//
// If there are expanded nodes, descendants will be in this store and need removing too.
// These values are used in onNodeRemove below, after the node has been unhooked from its
// siblings and parent. The [start, length] range parameter list for the flat store removeAt
// call is calculated and returned before the calling NodeInterface method removes child nodes.
beforeNodeRemove: function(parentNode, childNodes, isMove, removeRange) {
if (!Ext.isArray(childNodes)) {
childNodes = [ childNodes ];
}
/* eslint-disable-next-line vars-on-top */
var me = this,
len = childNodes.length,
// Must use class-specific removedNodes property.
// Regular Stores add to the "removed" property on CollectionRemove.
// TreeStores are having records removed all the time; node collapse removes.
// TreeStores add to the "removedNodes" property onNodeRemove
removed = me.removedNodes,
startNode, i;
// Skip to the first visible node.
for (i = 0; !startNode && i < len; i++) {
if (childNodes[i].get('visible')) {
startNode = childNodes[i];
}
}
// Calculate the range of contiguous *VISIBLE* nodes that the childNodes array represents.
// This is used by the calling code AFTER it has detached the tree structure.
if (startNode) {
removeRange[0] = me.indexOf(childNodes[0]);
removeRange[1] = me.indexOfNextVisibleNode(childNodes[childNodes.length - 1]) -
removeRange[0];
}
else {
removeRange[0] = -1;
removeRange[1] = 0;
}
// The code above calculated the range of nodes that are below expanded parents and not
// filtered out. That will be used to removed the block from the flat store, thereby
// updating any dependent UIs.
//
// We now have to walk the descendant tree for nodes which were not in the Store due to not
// being visible. This means either below a collapsed parent, or filtered out (visible
// property false)
//
// For example, in the tree below, imagine "bletch" is being removed, "zarg" is filtered out
// of visibility and the "blivit" node is collasped.
//
// foo
// ├ bar
// ├ bletch
// │ ├ zarg <- this is filtered out and therefore not visible
// │ └ blivit <- this is collapsed. ik, screeble, raz and poot are NOT in the Collection
// │ ├ ik
// │ └ screeble
// │ ├ raz
// │ └ poot
// ├ belch
// apresfoo
//
// the above code would only collect "bletch" and "blivit".
// We now have to collect bletch, zarg, blivit, uk, screeble, raz and poot.
for (i = 0; i < len; i++) {
childNodes[i].cascade(function(node) {
// We have to unregister all descendant nodes.
me.unregisterNode(node, true);
// We also have to ensure that all descendant nodes that were NOT removed above
// (ones that were not in the store collection due to invisibility are added to the
// remove tracking array...
// IF we are tracking, and is the remove is not for moving elsewhere in the tree.
if (removed && !isMove) {
// Don't push internally moving, or phantom (client side only), or erasing
// (informing server through its own proxy) records onto removed or which
// have been through a drop operation which will already have registered as to
// remove.
if (!node.phantom && !node.erasing && !me.loading) {
// Store the index the record was removed from so that rejectChanges can
// re-insert at the correct place. The record's index property won't do,
// as that is the index in the overall dataset when Store is buffered.
node.removedFrom = me.indexOf(node);
removed.push(node);
// Removal of a non-phantom record which is NOT erasing (informing
// the server through its own proxy) requires that the store be synced
// at some point.
me.needsSync = true;
}
}
});
}
},
// The drop operation of a Model calls afterDrop on attached stores which removes that model
// from the store's collection, and the store reacts to that.
// The drop operation on a tree NodeInterface object must not affect the Store. It must
// callParent to ensure associations are dropped too, but presence in a TreeStore is handled
// between the NodeInterface object and the TreeStore persona of the store, NOT its Store
// persona.
afterDrop: Ext.emptyFn,
// Called from a node's removeChild & removeAll methods *after* the node is unhooked from
// siblings and parent. Remove the visible descendant nodes that we calculated in
// beforeRemoveNode above.
onNodeRemove: function(parentNode, childNodes, isMove, removeRange) {
var me = this;
// Prevent the me.removeAt call which removes *VISIBLE* nodes when this store has
// a UI attached from syncing. We sync at the end.
me.suspendAutoSync();
// Remove all visible descendants from store.
// Only visible nodes are present in the store.
// Superclass's onCollectionRemove will handle unjoining.
// That will not add to removed list. TreeStores keep a different list and we add to it
// below. Set removeIsMove flag correctly for onCollectionRemove to do the right thing.
if (removeRange[0] !== -1) {
me.removeIsMove = isMove;
me.removeAt.apply(me, removeRange);
me.removeIsMove = false;
}
me.resumeAutoSync();
},
/**
* @private
*
* Called from a node's appendChild method.
*/
onNodeAppend: function(parent, node, index) {
this.onNodeInsert(parent, node, index);
},
/**
* @private
*
* Called from a node's insertBefore method.
*/
onNodeInsert: function(parent, node, index) {
var me = this,
data = node.raw || node.data,
// Must use class-specific removedNodes property.
// Regular Stores add to the "removed" property on CollectionRemove.
// TreeStores are having records removed all the time; node collapse removes.
// TreeStores add to the "removedNodes" property onNodeRemove
removed = me.removedNodes,
storeReader, nodeProxy, nodeReader, reader, dataRoot, storeInsertionPoint;
if (parent && me.needsLocalFilter()) {
me.doFilter(parent);
}
me.beginUpdate();
// Only react to a node append if it is to a node which is expanded.
if (me.isVisible(node)) {
// Calculate the insertion point into the flat store.
// If the new node is the first, then it goes after the parent node.
if (index === 0 || !node.previousSibling) {
storeInsertionPoint = me.indexOf(parent);
}
// Otherwise it has to go after the previous visible node which has
// to be calculated. See indexOfPreviousVisibleNode for explanation.
else {
storeInsertionPoint = me.indexOfPreviousVisibleNode(node.previousSibling);
}
// The reaction to collection add joins the node to this Store
me.insert(storeInsertionPoint + 1, node);
if (!node.isLeaf() && node.isExpanded()) {
if (node.isLoaded()) {
// Take a shortcut
me.onNodeExpand(node, node.childNodes);
}
else if (!me.fillCount) {
// If the node has been marked as expanded, it means the children
// should be provided as part of the raw data. If we're filling the nodes,
// the children may not have been loaded yet, so only do this if we're
// not in the middle of populating the nodes.
node.set('expanded', false);
node.expand();
}
}
}
// In case the node was removed and added to the removed nodes list.
Ext.Array.remove(removed, node);
// New nodes mean we need a sync if those nodes are phantom or dirty (have client-side only
// information)
me.needsSync = me.needsSync || node.phantom || node.dirty;
if (!node.isLeaf() && !node.isLoaded() && !me.lazyFill) {
// With heterogeneous nodes, different levels may require differently configured readers
// to extract children. For example a "Disk" node type may configure it's proxy reader
// with root: 'folders', while a "Folder" node type might configure its proxy reader
// with root: 'files'. Or the root property could be a configured-in accessor.
storeReader = me.getProxy().getReader();
nodeProxy = node.getProxy();
nodeReader = nodeProxy ? nodeProxy.getReader() : null;
// If the node's reader was configured with a special root (property name which defines
// the children array) use that.
reader = nodeReader && nodeReader.initialConfig.rootProperty ? nodeReader : storeReader;
dataRoot = reader.getRoot(data);
if (dataRoot) {
me.fillNode(node, reader.extractData(dataRoot, {
model: node.childType,
recordCreator: me.recordCreator
}));
}
}
me.endUpdate();
},
/**
* Registers a node so that it can be looked up by ID.
* @private
* @param {Ext.data.NodeInterface} node The node to register
* @param {Boolean} [includeChildren] True to unregister any child nodes
*/
registerNode: function(node, includeChildren) {
var me = this,
was = me.byIdMap[node.id],
children, length, i;
// Key the node hash by the node's IDs
me.byIdMap[node.id] = node;
// If the node requires to be informed upon register, and is not already
// registered, keep it informed.
if (node.onRegisterTreeNode && node !== was) {
node.onRegisterTreeNode(me);
}
// Keep a count of nodes which require to be informed upon unregister.
// If we are destroyed, or change root nodes, a cascade will be
// necessary if this is non-zero.
if (node.onUnregisterTreeNode) {
me.nodesToUnregister++;
}
if (includeChildren === true) {
children = node.childNodes;
length = children.length;
for (i = 0; i < length; i++) {
me.registerNode(children[i], true);
}
}
},
/**
* Unregisters a node.
* @private
* @param {Ext.data.NodeInterface} node The node to unregister
* @param {Boolean} [includeChildren] True to unregister any child nodes
*/
unregisterNode: function(node, includeChildren) {
var me = this,
was = me.byIdMap[node.id],
children, length, i;
delete me.byIdMap[node.id];
if (includeChildren === true) {
children = node.childNodes;
length = children.length;
for (i = 0; i < length; i++) {
me.unregisterNode(children[i], true);
}
}
// If the node requires to be informed upon unregister, and it was
// registered, keep it informed.
if (node.onUnregisterTreeNode && node === was) {
node.onUnregisterTreeNode(me);
me.nodesToUnregister--;
}
},
onNodeSort: function(node, childNodes) {
var me = this;
// The onNodeCollapse and onNodeExpand should not sync.
// Should be one coalesced sync if autoSync.
me.suspendAutoSync();
// Collapse, then expand the node to refresh the displayed node set if the node
// is expanded, or it's the root, but the root is not visible (so cannot be expanded
// by the UI)
/* eslint-disable-next-line max-len */
if ((me.indexOf(node) !== -1 && node.isExpanded()) || (node === me.getRoot() && !me.getRootVisible())) {
Ext.suspendLayouts();
me.onNodeCollapse(node, childNodes);
me.onNodeExpand(node, childNodes);
Ext.resumeLayouts(true);
}
// Lift suspension. This will execute a sync if the suspension count has gone to zero
// and this store is configured to autoSync
me.resumeAutoSync(me.autoSync);
},
applyRoot: function(newRoot) {
var me = this,
Model = me.getModel(),
idProperty = Model.prototype.idProperty,
defaultRootId = me.getDefaultRootId();
// Convert to a node. Even if they are passing a normal Model, the Model will not yet
// have been decorated with the constructor which initializes properties, so we always
// have to construct a new node if the passed root is not a Node.
if (newRoot && !newRoot.isNode) {
// create a default rootNode and create internal data struct.
newRoot = Ext.apply({
text: me.getDefaultRootText(),
root: true,
isFirst: true,
isLast: true,
depth: 0,
index: 0,
parentId: null,
allowDrag: false
}, newRoot);
// Ensure the root has the default root id if it has no id.
if (defaultRootId && newRoot[idProperty] === undefined) {
newRoot[idProperty] = defaultRootId;
}
// Specify that the data object is raw, and converters will need to be called
newRoot = new Model(newRoot);
}
return newRoot;
},
updateRoot: function(newRoot, oldRoot) {
var me = this,
removeRange = [],
initial = me.isConfiguring,
oldOwner, toRemove;
// Ensure that the removedNodes array is correct, and that the base class's removed array
// is null
me.getTrackRemoved();
// We do not want an add event to fire. This is a refresh operation.
// A refresh will be fired after the new root is set.
me.suspendEvent('add', 'remove');
if (initial) {
me.suspendEvent('refresh', 'datachanged');
}
// Ensure that the old root is unjoined, visible children are removed from Collection,
// and descendants added to removed list if tracking removed.
if (oldRoot && oldRoot.isModel) {
// root will be in flat store only if rootVisible is false
if (me.getRootVisible()) {
toRemove = [oldRoot];
}
else {
toRemove = oldRoot.childNodes;
}
me.beforeNodeRemove(null, toRemove, false, removeRange);
oldRoot.set('root', false);
me.onNodeRemove(null, toRemove, false, removeRange);
oldRoot.fireEvent('remove', null, oldRoot, false);
oldRoot.fireEvent('rootchange', null);
oldRoot.clearListeners();
oldRoot.store = oldRoot.treeStore = null;
// If rootVisible is false, the root will not have been unregistered by
// the beforeNodeRemove call which calls a recursive unregister on all
// *visible* nodes being removed from the flat store.
me.unregisterNode(oldRoot);
}
me.getData().clear();
// Nulling the root node is essentially clearing the store.
// TreeStore.removeAll updates the root node to null.
if (newRoot) {
// Fire beforeappend, to allow veto of new root setting
if (newRoot.fireEventArgs('beforeappend', [null, newRoot]) === false) {
newRoot = null;
}
else {
// The passed node was a childNode somewhere else; remove it from there.
oldOwner = newRoot.parentNode;
if (oldOwner) {
// The removeChild operation can be vetoed by beforeremove event handler,
// and returns false if so.
// Important: That last boolean test is informing the remove whether or not it's
// just a move operation within the same TreeStore
/* eslint-disable-next-line max-len */
if (!oldOwner.removeChild(newRoot, false, false, oldOwner.getTreeStore() === me)) {
return;
}
}
// If the passed root was previously the rootNode of another TreeStore,
// it must be removed from that store
/* eslint-disable-next-line max-len */
else if ((oldOwner = newRoot.getTreeStore()) && oldOwner !== me && newRoot === oldOwner.getRoot()) {
oldOwner.setRoot(null);
}
// The root node is the only node bound to the TreeStore by a reference.
// All descendant nodes acquire a reference to their TreeStore by interrogating
// the parentNode axis. The rootNode never joins this store.
newRoot.store = newRoot.treeStore = me;
newRoot.set('root', true);
// root node should never be phantom or dirty, so commit it
newRoot.updateInfo(true, {
isFirst: true,
isLast: true,
depth: 0,
index: 0,
parentId: null
});
// We register the subtree before we proceed so relayed events (like
// nodeappend) will be able to use getNodeById.
me.registerNode(newRoot, true);
// The new root fires the append and rootchange events
newRoot.fireEvent('append', null, newRoot, false);
newRoot.fireEvent('rootchange', newRoot);
// Ensure the root node is filtered, registered and joined.
me.onNodeAppend(null, newRoot, 0);
// Because of the application of an ID, this client-created root
// will not be phantom. Ensure it is correctly flagged as a phantom.
// AFTER being registered and joined, otherwise onNodeInsert will set
// the needsSync flag.
newRoot.phantom = true;
}
}
if (!initial) {
me.fireEvent('rootchange', newRoot, oldRoot);
}
// If root configure to start expanded, or we are autoLoad, we want the root's nodes
// in the Store.
if (newRoot && (me.getAutoLoad() || newRoot.isExpanded())) {
// If it was configured with inline children, it will be loaded, so skip ahead
// to the onNodeExpand callback.
if (newRoot.isLoaded()) {
me.onNodeExpand(newRoot, newRoot.childNodes);
if (!initial) {
me.fireEvent('datachanged', me);
me.fireEvent('refresh', me);
}
}
// Root is not loaded; go through the expand mechanism to force a load
else {
newRoot.data.expanded = false;
newRoot.expand(false);
// If it's already loaded, but the store is not synchronous, then it was loaded
// from a local children array, so we need to refresh the data.
// A store based load goes through onProxyLoad which will fire a refresh.
if (newRoot.isLoaded && !me.getProxy().isSynchronous && !initial) {
me.fireEvent('datachanged', me);
me.fireEvent('refresh', me);
}
}
}
else if (!initial) {
me.fireEvent('datachanged', me);
me.fireEvent('refresh', me);
}
// Inform views that the entire structure has changed.
me.resumeEvent('add', 'remove');
if (initial) {
me.resumeEvent('refresh', 'datachanged');
}
},
doDestroy: function() {
var me = this,
root = me.getRoot();
// If we contain some nodes which require to be informed upon unregister
// then we must cascade the whole tree and inform any that require it.
// The cascade method calls the passed function on the topmost node.
if (root && me.nodesToUnregister) {
root.cascade(function(node) {
if (node.onUnregisterTreeNode) {
node.onUnregisterTreeNode(me);
}
});
}
me.callParent();
},
/**
* @method getById
* @inheritdoc Ext.data.LocalStore
* @localdoc **NOTE:** TreeStore's getById method will only search nodes that
* are expanded (all ancestor nodes are {@link Ext.data.NodeInterface#expanded
* expanded}: true -- {@link Ext.data.NodeInterface#isExpanded isExpanded})
*
* See also {@link #getNodeById}
*/
/**
* Calls the specified function for each {@link Ext.data.NodeInterface node} in the store.
*
* When store is filtered, only loops over the filtered records unless the `bypassFilters`
* parameter is `true`.
*
* @param {Function} fn The function to call. The {@link Ext.data.Model Record} is passed
* as the first parameter. Returning `false` aborts and exits the iteration.
* @param {Object} [scope] The scope (`this` reference) in which the function is executed.
* Defaults to the current {@link Ext.data.NodeInterface node} in the iteration.
* @param {Object/Boolean} [includeOptions] An object which contains options which
* modify how the store is traversed. Alternatively, this parameter can be just the
* `filtered` option.
* @param {Boolean} [includeOptions.filtered] Pass `true` to include filtered out
* nodes in the iteration.
* @param {Boolean} [includeOptions.collapsed] Pass `true` to include nodes which are
* descendants of collapsed nodes.
*/
each: function(fn, scope, includeOptions) {
var i = 0,
filtered = includeOptions,
includeCollapsed;
if (includeOptions && typeof includeOptions === 'object') {
includeCollapsed = includeOptions.collapsed;
filtered = includeOptions.filtered;
}
if (includeCollapsed) {
this.getRoot().cascade(function(node) {
if (filtered === true || node.get('visible')) {
return fn.call(scope || node, node, i++);
}
});
}
else {
return this.callParent([fn, scope, filtered]);
}
},
/**
* Collects unique values for a particular dataIndex from this store.
*
* @param {String} dataIndex The property to collect
* @param {Object/Boolean} [options] An object which contains options which modify how
* the store is traversed. Or just the `allowNull` option.
* @param {Boolean} [options.allowNull] Pass true to allow null, undefined or empty
* string values.
* @param {Boolean} [options.filtered] Pass `true` to collect from all records, even
* ones which are filtered.
* @param {Boolean} [options.collapsed] Pass `true` to include nodes which are
* descendants of collapsed nodes.
*
* @param {Boolean} [filtered] If previous parameter (`options`) is just the
* `allowNull` value, this parameter is the `filtered` option.
*
* @return {Object[]} An array of the unique values
*/
collect: function(dataIndex, options, filtered) {
var includeCollapsed,
map = {},
result = [],
allowNull = options,
strValue, value;
if (options && typeof options === 'object') {
includeCollapsed = options.collapsed;
filtered = options.filtered;
allowNull = options.allowNull;
}
if (includeCollapsed || filtered) {
this.getRoot().cascade(function(node) {
if (filtered === true || node.get('visible')) {
value = node.get(dataIndex);
strValue = String(value);
if ((allowNull || !Ext.isEmpty(value)) && !map[strValue]) {
map[strValue] = 1;
result.push(value);
}
}
// Do not traverse into children if this is collapsed
// and they are not asking to include collapsed
if (!includeCollapsed && !node.isExpanded()) {
return false;
}
});
}
else {
result = this.callParent([dataIndex, allowNull, filtered]);
}
return result;
},
/**
* Returns the record node by id regardless of visibility due to collapsed states;
* all nodes present in the tree structure are available.
* @param {String} id The id of the node to get.
* @return {Ext.data.NodeInterface}
*/
getNodeById: function(id) {
return this.byIdMap[id] || null;
},
/**
* Finds the first matching node in the tree by a specific field value regardless of visibility
* due to collapsed states; all nodes present in the tree structure are searched.
*
* @param {String} fieldName The name of the Record field to test.
* @param {String/RegExp} value Either a string that the field value
* should begin with, or a RegExp to test against the field.
* @param {Boolean} [startsWith=true] Pass `false` to allow a match to start
* anywhere in the string. By default the `value` will match only at the start
* of the string.
* @param {Boolean} [endsWith=true] Pass `false` to allow the match to end before
* the end of the string. By default the `value` will match only at the end of the
* string.
* @param {Boolean} [ignoreCase=true] Pass `false` to make the `RegExp` case
* sensitive (removes the 'i' flag).
* @return {Ext.data.NodeInterface} The matched node or null
*/
findNode: function(fieldName, value, startsWith, endsWith, ignoreCase) {
var result = null,
regex;
if (Ext.isEmpty(value, false)) {
return result;
}
// If they are looking up by the idProperty, do it the fast way.
if (fieldName === this.model.idProperty && arguments.length < 3) {
return this.byIdMap[value];
}
regex = Ext.String.createRegex(value, startsWith, endsWith, ignoreCase);
Ext.Object.eachValue(this.byIdMap, function(node) {
if (node && regex.test(node.get(fieldName))) {
result = node;
return false;
}
});
return result;
},
/**
* Marks this store as needing a load. When the current executing event handler exits,
* this store will send a request to load using its configured {@link #proxy}.
*
* **Be aware that it is not usually valid for a developer to call this method on a TreeStore.**
*
* TreeStore loads are triggered by a load request from an existing
* {@link Ext.data.NodeInterface tree node}, when the node is expanding, and it has no
* locally defined children in its data.
*
* *Note:* Even for synchronous Proxy types such as {@link Ext.data.proxy.Memory memory proxy},
* the result will *NOT* be available in the following line of code. You must use a callback
* in the load options, or a {@link #event-load load listener}.
*
* @param {Object} [options] This is passed into the
* {@link Ext.data.operation.Operation Operation} object that is created and then sent to the
* proxy's {@link Ext.data.proxy.Proxy#read} function. The options can also contain a node,
* which indicates which node is to be loaded. If not specified, it will default to the
* root node.
* @param {Ext.data.NodeInterface} [options.node] The tree node to load. Defaults to the store's
* {@link #cfg-root root node}
* @param {Function} [options.callback] A function which is called when the response arrives.
* @param {Ext.data.Model[]} options.callback.records Array of records.
* @param {Ext.data.operation.Operation} options.callback.operation The Operation itself.
* @param {Boolean} options.callback.success `true` when operation completed successfully.
* @method load
*/
load: function(options) {
var node = options && options.node;
// If there is not a node it means the user hasn't defined a root node yet. In this case
// let's just create one for them. The expanded: true will cause a load operation,
// so return.
if (!node && !(node = this.getRoot())) {
node = this.setRoot({
expanded: true,
autoRoot: true
});
return;
}
// If the node kicked off its own load, then don't schedule another load.
if (node.isLoading()) {
return;
}
return this.callParent([options]);
},
/**
* Reloads the root node of this store.
*/
reload: function(options) {
var o = Ext.apply({}, options, this.lastOptions);
// A reload implies root load
o.node = this.getRoot();
return this.load(o);
},
/**
* Called when the event handler which called the {@link #method-load} method exits.
*/
flushLoad: function() {
var me = this,
options = me.pendingLoadOptions,
clearOnLoad = me.getClearOnLoad(),
node, callback, scope, isRootLoad, operation, doClear;
// If it gets called programmatically before the timer fired, the listener
// will need cancelling.
me.clearLoadTask();
if (!options) {
return;
}
node = options.node || me.getRoot();
// If we are loading the root, then it is a whole store reload.
// This is handled efficiently in onProxyLoad by firing the refresh event
// which will completely refresh any dependent views as would be expected
// from a load() call.
isRootLoad = node && node.isRoot();
callback = options.callback;
scope = options.scope;
options.params = options.params || {};
// If this is not a root reload.
// If the node we are loading was expanded, we have to expand it after the load
if (node.data.expanded && !isRootLoad) {
node.data.loaded = false;
// Must set expanded to false otherwise the onProxyLoad->fillNode->appendChild
// calls will update the view. We ned to update the view in the callback below.
if (clearOnLoad) {
node.data.expanded = false;
}
options.callback = function(loadedNodes, operation, success) {
// If newly loaded nodes are to be added to the existing child node set, then
// we have to collapse first so that they get removed from the NodeStore,
// and the subsequent expand will reveal the newly augmented child node set.
if (!clearOnLoad) {
node.collapse();
}
node.expand();
// Call the original callback (if any)
Ext.callback(callback, scope, [loadedNodes, operation, success]);
};
}
// Assign the ID of the Operation so that a ServerProxy can set its idParam parameter,
// or a REST proxy can create the correct URL
options.id = node.getId();
// Only add filtering and sorting options if those options are remote
me.setLoadOptions(options);
if (me.getRemoteSort() && options.sorters) {
me.fireEvent('beforesort', me, options.sorters);
}
options = Ext.apply({
node: options.node || node,
internalScope: me,
internalCallback: me.onProxyLoad
}, options);
me.lastOptions = Ext.apply({}, options);
// Must not be copied into lastOptions, otherwise it overrides next call.
options.isRootLoad = isRootLoad;
operation = me.createOperation('read', options);
if (me.fireEvent('beforeload', me, operation) !== false) {
// Set the loading flag early
// Used by onNodeRemove to NOT add the removed nodes to the removed collection
me.loading = true;
// If this is a full root load, clear the root node and the flat data.
if (isRootLoad) {
if (me.getClearRemovedOnLoad()) {
me.removedNodes.length = 0;
}
if (clearOnLoad) {
// Unregister all descendants, but immediately re-register the root
// It is NOT being tipped out by this load operation
me.unregisterNode(node, true);
node.clear(false, true);
me.registerNode(node);
doClear = true;
}
}
// Load a non-root
else {
if (me.loading) {
// set loaded: false so that the eventual response will trigger the UI update
node.data.loaded = false;
}
if (me.getTrackRemoved() && me.getClearRemovedOnLoad()) {
// clear from the removed array any nodes that were descendants of the node
// being reloaded so that they do not get saved on next sync.
me.clearRemoved(node);
}
if (clearOnLoad) {
node.removeAll(false);
}
}
// Silently set loading state if the node doesn't already exist so the UI displays
// the load icon but doesn't attempt other UI updates
if (me.loading && node) {
/* eslint-disable-next-line max-len */
node.set('loading', true, { silent: !(me.contains(node) || node === me.getRoot()) });
}
if (doClear) {
me.clearData(true);
// Read the root we just cleared, since we're reloading it
if (me.getRootVisible()) {
me.suspendEvents();
me.add(node);
me.resumeEvents();
}
}
operation.execute();
}
return me;
},
onProxyLoad: function(operation) {
var me = this,
options = operation.initialConfig,
successful = operation.wasSuccessful(),
records = operation.getRecords(),
node = options.node,
isRootLoad = options.isRootLoad,
scope = operation.getScope() || me,
args = [records, operation, successful];
if (me.destroyed) {
return;
}
me.loading = false;
node.set('loading', false);
if (successful) {
++me.loadCount;
if (!me.getClearOnLoad()) {
records = me.cleanRecords(node, records);
}
// Nodes are in linear form, linked to the parent using a parentId property
if (me.getParentIdProperty()) {
records = me.treeify(node, records);
}
if (isRootLoad) {
me.suspendEvent('add', 'update');
}
records = me.fillNode(node, records);
}
// The load event has an extra node parameter
// (differing from the load event described in AbstractStore)
/**
* @event load
* Fires whenever the store reads data from a remote data source.
* @param {Ext.data.TreeStore} this
* @param {Ext.data.TreeModel[]} records An array of records.
* @param {Boolean} successful True if the operation was successful.
* @param {Ext.data.operation.Operation} operation The operation that triggered this load.
* @param {Ext.data.NodeInterface} node The node that was loaded.
*/
Ext.callback(options.onChildNodesAvailable, scope, args);
if (isRootLoad) {
me.resumeEvent('add', 'update');
me.callObservers('BeforePopulate');
me.fireEvent('datachanged', me);
me.fireEvent('refresh', me);
me.callObservers('AfterPopulate');
}
me.fireEvent('load', me, records, successful, operation, node);
},
/**
* Removes all records that used to be descendants of the passed node from the removed array
* @private
* @param {Ext.data.NodeInterface} node
*/
clearRemoved: function(node) {
var me = this,
removed = me.removedNodes,
id = node.getId(),
removedLength = removed.length,
i = removedLength,
recordsToClear = {},
newRemoved = [],
removedHash = {},
removedNode, targetNode, targetId;
if (node === me.getRoot()) {
// if the passed node is the root node, just reset the removed array
me.removedNodes.length = 0;
return;
}
// add removed records to a hash so they can be easily retrieved by id later
for (; i--;) {
removedNode = removed[i];
removedHash[removedNode.getId()] = removedNode;
}
for (i = removedLength; i--;) {
removedNode = removed[i];
targetNode = removedNode;
while (targetNode && targetNode.getId() !== id) {
// walk up the parent hierarchy until we find the passed node or until
// we get to the root node lastParentId is set in nodes which have been removed.
targetId = targetNode.get('parentId') || targetNode.get('lastParentId');
targetNode = targetNode.parentNode || me.getNodeById(targetId) ||
removedHash[targetId];
}
if (targetNode) {
// removed node was previously a descendant of the passed node - add it
// to the records to clear from "removed" later
recordsToClear[removedNode.getId()] = removedNode;
}
}
// create a new removed array containing only the records that are not in recordsToClear
for (i = 0; i < removedLength; i++) {
removedNode = removed[i];
if (!recordsToClear[removedNode.getId()]) {
newRemoved.push(removedNode);
}
}
me.removedNodes = newRemoved;
},
/**
* Fills a node with a series of child records.
* @private
* @param {Ext.data.NodeInterface} node The node to fill
* @param {Ext.data.TreeModel[]} newNodes The records to add
*/
fillNode: function(node, newNodes) {
var me = this,
newNodeCount = newNodes ? newNodes.length : 0;
// If we're filling, increment the counter so nodes can react without doing
// expensive operations
if (++me.bulkUpdate === 1) {
me.suspendEvent('datachanged');
}
if (newNodeCount) {
me.setupNodes(newNodes);
node.appendChild(newNodes, undefined, true);
}
// only set loaded if there are no newNodes;
// appendChild already handles updating the loaded status,
// and will do it *after* the child nodes have been added
else {
if (me.bulkUpdate === 1) {
node.set('loaded', true);
}
else {
node.data.loaded = true;
}
}
if (!--me.bulkUpdate) {
me.resumeEvent('datachanged');
}
// No need to call registerNode here, because each child will register itself as it joins
return newNodes;
},
setupNodes: function(newNodes) {
var me = this,
sorters = me.getSorters(),
needsIndexSort = false,
newNodeCount = newNodes.length,
/* eslint-disable-next-line max-len */
performLocalSort = me.sortOnLoad && newNodeCount > 1 && !me.getRemoteSort() && me.getFolderSort() || sorters.length,
performLocalFilter = me.needsLocalFilter(),
node1, node2, i;
// Apply any local filter to the nodes as we fill
if (performLocalFilter) {
me.doFilter(newNodes[0]);
}
// See if there are any differing index values in the new nodes. If not, then we do not
// have to sortByIndex
for (i = 1; i < newNodeCount; i++) {
node1 = newNodes[i];
node2 = newNodes[i - 1];
// Apply any filter to the nodes as we fill
if (performLocalFilter) {
me.doFilter(node1);
}
needsIndexSort = node1.data.index !== node2.data.index;
}
// If there is a set of local sorters defined.
if (performLocalSort) {
// If sorting by index is needed, sort by index first
me.needsIndexSort = true;
Ext.Array.sort(newNodes, me.getSortFn());
me.needsIndexSort = false;
}
else if (needsIndexSort) {
Ext.Array.sort(newNodes, me.sortByIndex);
}
},
// Called by a node which is appending children to itself
beginFill: function() {
var me = this;
if (!me.fillCount++) { // jshint ignore:line
me.beginUpdate();
me.suspendEvent('add', 'update');
me.suspendAutoSync();
me.fillArray = [];
}
},
// resume view updating and data syncing after a node fill
endFill: function(parent, nodes) {
var me = this,
fillArray = me.fillArray,
i, len, index;
// Keep every block of records added during the fill
fillArray.push(nodes);
if (! --me.fillCount) {
me.resumeAutoSync();
me.resumeEvent('add', 'update');
// Add all blocks of records from nested beginFill calls.
// appendChild can load local child data and recursively call appendChild.
// This coalesces all add operations into a layout suspension
for (i = 0, len = fillArray.length; i < len; i++) {
index = me.indexOf(fillArray[i][0]);
// Only inform views if the blocks appended actually made it into the linear store
// (are visible)
if (index !== -1) {
me.fireEvent('add', me, fillArray[i], index);
}
}
me.fillArray = null;
me.endUpdate();
}
},
/**
* Sorter function for sorting records in index order
* @private
* @param {Ext.data.NodeInterface} node1
* @param {Ext.data.NodeInterface} node2
* @return {Number}
*/
sortByIndex: function(node1, node2) {
return node1.data.index - node2.data.index;
},
onIdChanged: function(node, oldId, newId) {
var childNodes = node.childNodes,
len = childNodes && childNodes.length,
i;
this.callParent(arguments);
delete this.byIdMap[oldId];
this.byIdMap[newId] = node;
// Ensure all child nodes know their parent's new ID
for (i = 0; i < len; i++) {
childNodes[i].set('parentId', newId);
}
},
/**
* @private
* Converts a flat array of nodes into a tree structure.
* Returns an array which is the childNodes array of the rootNode.
*/
treeify: function(parentNode, records) {
var me = this,
loadParentNodeId = parentNode.getId(),
parentIdProperty = me.getParentIdProperty(),
len = records.length,
result = [],
nodeMap = {},
i, node, parentId, parent, id, children;
// Collect all nodes keyed by ID, so that regardless of order, they can all be
// linked to a parent.
for (i = 0; i < len; i++) {
node = records[i];
node.data.depth = 1;
nodeMap[node.id] = node;
}
// Link child nodes up to their parents
for (i = 0; i < len; i++) {
node = records[i];
parentId = node.data[parentIdProperty];
if (!(parentId || parentId === 0) || parentId === loadParentNodeId) {
result.push(node);
}
else {
//<debug>
if (!nodeMap[parentId]) {
Ext.raise('Ext.data.TreeStore, Invalid parentId "' + parentId + '"');
}
//</debug>
parent = nodeMap[parentId];
parent.$children = parent.$children || [];
parent.$children.push(node);
node.data.depth = parent.data.depth + 1;
}
}
for (id in nodeMap) {
node = nodeMap[id];
children = node.$children;
if (children) {
delete node.$children;
me.setupNodes(children);
node.appendChild(children);
}
me.registerNode(node);
}
me.setupNodes(result);
return result;
},
cleanRecords: function(node, records) {
var nodeHash = {},
childNodes = node.childNodes,
i = 0,
len = childNodes.length,
out = [],
rec;
// build a hash of all the childNodes under the current node for performance
for (; i < len; ++i) {
nodeHash[childNodes[i].getId()] = true;
}
for (i = 0, len = records.length; i < len; ++i) {
rec = records[i];
if (!nodeHash[rec.getId()]) {
out.push(rec);
}
}
return out;
},
removeAll: function() {
var me = this,
root = me.getRoot(),
ln = me.getData().length;
me.suspendEvents();
me.setRoot(null);
me.resumeEvents();
me.callParent();
if (ln) {
me.fireEvent('clear', me);
me.fireEvent('rootchange', root, null);
}
},
doSort: function(sorterFn) {
var me = this;
if (me.getRemoteSort()) {
// the load function will pick up the new sorters and request the sorted data
// from the proxy
me.load();
}
else {
me.tree.sort(sorterFn, true);
me.fireEvent('datachanged', me);
me.fireEvent('refresh', me);
}
me.fireEvent('sort', me, me.sorters.getRange());
},
filterVisible: function(node) {
return node.get('visible');
},
/**
* @method isVisible
* @inheritdoc Ext.data.NodeStore#method-isVisible
*/
isVisible: function(node) {
var parentNode = node.parentNode,
visible = node.data.visible,
root = this.getRoot();
while (visible && parentNode) {
visible = parentNode.data.expanded && parentNode.data.visible;
parentNode = parentNode.parentNode;
}
// The passed node is visible if we ended up at the root node, and it is visible.
// UNLESS it's the root node, and we are configured with rootVisible:false
return visible && !(node === root && !this.getRootVisible());
},
commitChanges: function() {
var removed = this.removedNodes;
if (removed) {
removed.length = 0;
}
this.callParent();
},
/**
* Returns the root node for this tree.
* @return {Ext.data.NodeInterface}
* @deprecated 5.0 Use {@link #getRoot} instead
*/
getRootNode: function() {
return this.getRoot();
},
/**
* Sets the root node for this store. See also the {@link #root} config option.
* @param {Ext.data.TreeModel/Ext.data.NodeInterface/Object} root
* @return {Ext.data.NodeInterface} The new root
* @deprecated 5.0 Use {@link #setRoot} instead
*/
setRootNode: function(root) {
this.setRoot(root);
return this.getRoot();
},
privates: {
fireChangeEvent: function(record) {
return !!this.byIdMap[record.id];
},
/**
* @private
* Returns the array of nodes which have been removed since the last time this store
* was synced.
*
* This is used internally, when purging removed records after a successful sync.
* This is overridden by TreeStore because TreeStore accumulates deleted records on removal
* of child nodes from their parent, *not* on removal of records from its collection.
* The collection has records added on expand, and removed on collapse.
*/
getRawRemovedRecords: function() {
return this.removedNodes;
},
createOperation: function(type, options) {
// Use the proxy assigned to the node by preference so that in a heterogeneous tree,
// different node types can use different proxies and data providers.
var me = this,
node = options.node,
proxy;
// Use the node's proxy by preference if we acquired our proxy
// from the configured Model (See ProxyStore#applyProxy)
if (me.useModelProxy && node && node !== me.getRootNode()) {
proxy = node.getProxy();
}
// If the node has a proxy which is different from this Store's proxy
// then we must use that to create and manage its I/O operations.
if (proxy && proxy !== me.getProxy()) {
return proxy.createOperation(type, options);
}
else {
return me.callParent([type, options]);
}
},
/**
* @private
* A function passed into {@link Ext.data.reader.Reader#extractData} which is used
* as a record creation function.
* @param {Object} data Raw data to transform into a record
* @param {Function} Model Model constructor to create a record from the passed data.
* @return {Ext.data.Model} The resulting new Model instance.
*/
recordCreator: function(data, Model) {
return new Model(data);
},
doFilter: function(node) {
this.filterNodes(node, this.getFilters().getFilterFn(), true);
},
/**
* @private
* Filters the passed node according to the passed function.
*
* If this TreeStore is configured with {@link #cfg-filterer filterer: 'bottomup'}, leaf
* nodes are tested according to the function. Additionally, parent nodes are filtered in
* if any descendant leaf nodes have passed the filter test.
*
* Otherwise a parent node which fails the test will terminate the branch and
* descendant nodes which pass the filter test will be filtered out.
*/
filterNodes: function(node, filterFn, parentVisible) {
var me = this,
bottomUpFiltering = me.filterer === 'bottomup',
// MUST call filterFn first to avoid shortcutting if parentVisible is false.
// filterFn may have side effects, so must be called on all nodes.
match = filterFn(node) && parentVisible || (node.isRoot() && !me.getRootVisible()),
childNodes = node.childNodes,
len = childNodes && childNodes.length,
i, matchingChildren;
if (len) {
for (i = 0; i < len; ++i) {
// MUST call method first to avoid shortcutting boolean expression
// if matchingChildren is true
/* eslint-disable-next-line max-len */
matchingChildren = me.filterNodes(childNodes[i], filterFn, match || bottomUpFiltering) || matchingChildren;
}
if (bottomUpFiltering) {
match = matchingChildren || match;
}
}
node.set("visible", match, me._silentOptions);
return match;
},
needsLocalFilter: function() {
return !this.getRemoteFilter() && this.getFilters().length;
},
onRemoteFilterSet: function(filters, remoteFilter) {
// Filtering is done at the Store level for TreeStores.
// It has to be done on a hierarchical basis.
// The onFilterEndUpdate signal has to be passed into the root node which filters
// its children and cascades the filter instruction downwards.
var data = this.getData();
data.setFilters(null);
if (filters) {
filters.on('endupdate', this.onFilterEndUpdate, this);
}
},
onRemoteSortSet: function(sorters, remoteSort) {
// Sorting is done at the Store level for TreeStores.
// It has to be done on a hierarchical basis.
// The onSorterEndUpdate signal has to be passed root node which sorts its children
// and cascades the sort instruction downwards.
var data = this.getData();
data.setSorters(null);
if (sorters) {
sorters.on('endupdate', this.onSorterEndUpdate, this);
}
}
},
deprecated: {
5: {
properties: {
tree: null
}
}
}
});