extjs-gpl
Version:
GPL licensed version of Sencha Ext JS
1,284 lines (1,142 loc) • 87.7 kB
JavaScript
/**
* 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 programatically 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
* 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="children"]
*/
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=true]
* Remove previously existing child nodes before loading.
*/
clearOnLoad : true,
/**
* @cfg {Boolean} [clearRemovedOnLoad=true]
* 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="node"]
* The name of the parameter sent to the server which contains the identifier of the node.
*/
nodeParam: 'node',
/**
* @cfg {String} [defaultRootId="root"]
* The default root id.
*/
defaultRootId: 'root',
/**
* @cfg {String} [defaultRootText="Root"]
* The default root text (if not specified)
*/
defaultRootText: 'Root',
/**
* @cfg {Boolean} [folderSort=false]
* 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=topdown]
* 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.
*/
filterer: 'topdown',
/**
* @cfg {Boolean} [lazyFill=false]
* 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 {Object[]/String[]} 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
},
implicitModel: 'Ext.data.TreeModel',
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]);
},
// 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) {
if (model) {
var 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 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;
// 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())) {
// Extract records from the raw data. Allow the node being expanded to dictate its child type
if (children) {
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.
else {
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
record.set('expanded', false);
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)
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 ];
}
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,
i,
startNode;
// 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 interally 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 calllParent
// 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 unregster, 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)
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,
oldOwner,
initial = !oldRoot,
toRemove,
removeRange = [];
// 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');
// 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, removeR