UNPKG

extjs-gpl

Version:

GPL licensed version of Sencha Ext JS

1,284 lines (1,142 loc) 87.7 kB
/** * 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