/** * The TreeStore is a store implementation that is backed by by an {@link Ext.data.Tree}. * It provides convenience methods for loading nodes, as well as the ability to use * the hierarchical tree structure combined with a store. This class is generally used * in conjunction with {@link Ext.tree.Panel}. This class also relays many events from * the Tree for convenience. * * # Using Models * * If no Model is specified, an implicit model will be created that implements {@link Ext.data.NodeInterface}. * 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 that setting {@link #defaultRootProperty} accomplishes the same thing. */ Ext.define('Ext.data.TreeStore', { extend: 'Ext.data.AbstractStore', alias: 'store.tree', requires: [ 'Ext.util.Sorter', 'Ext.data.Tree', 'Ext.data.NodeInterface' ], /** * @cfg {Ext.data.Model/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}. */ /** * @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 {String} [defaultRootProperty="children"] * The root property to specify on the reader if one is not explicitly defined. */ defaultRootProperty: 'children', // Keep a copy of the default so we know if it's been changed in a subclass/config rootProperty: 'children', fillCount: 0, /** * @cfg {Boolean} [folderSort=false] * Set to true to automatically prepend a leaf sorter. */ folderSort: false, constructor: function(config) { var me = this, root, fields, defaultRoot; config = Ext.apply({}, config); /** * If we have no fields declare for the store, add some defaults. * These will be ignored if a model is explicitly specified. */ fields = config.fields || me.fields; if (!fields) { config.fields = [ {name: 'text', type: 'string'} ]; defaultRoot = config.defaultRootProperty || me.defaultRootProperty; if (defaultRoot !== me.defaultRootProperty) { config.fields.push({ name: defaultRoot, type: 'auto', defaultValue: null, persist: false }); } } me.callParent([config]); // We create our data tree. me.tree = new Ext.data.Tree(); // data tree has an upward link me.tree.treeStore = me; // The following events are fired on this TreeStore by the bubbling from NodeInterface.fireEvent /** * @event append * @inheritdoc Ext.data.Tree#append */ /** * @event remove * @inheritdoc Ext.data.Tree#remove */ /** * @event move * @inheritdoc Ext.data.Tree#move */ /** * @event insert * @inheritdoc Ext.data.Tree#insert */ /** * @event beforeappend * @inheritdoc Ext.data.Tree#beforeappend */ /** * @event beforeremove * @inheritdoc Ext.data.Tree#beforeremove */ /** * @event beforemove * @inheritdoc Ext.data.Tree#beforemove */ /** * @event beforeinsert * @inheritdoc Ext.data.Tree#beforeinsert */ /** * @event expand * @inheritdoc Ext.data.Tree#expand */ /** * @event collapse * @inheritdoc Ext.data.Tree#collapse */ /** * @event beforeexpand * @inheritdoc Ext.data.Tree#beforeexpand */ /** * @event beforecollapse * @inheritdoc Ext.data.Tree#beforecollapse */ /** * @event sort * @inheritdoc Ext.data.Tree#sort */ me.tree.on({ scope: me, remove: me.onNodeRemove, // this event must follow the relay to beforeitemexpand to allow users to // cancel the expand: beforeexpand: me.onBeforeNodeExpand, append: me.onNodeAdded, insert: me.onNodeAdded, sort: me.onNodeSort }); me.onBeforeSort(); root = me.root; if (root) { delete me.root; me.setRootNode(root); } //<deprecated since=0.99> 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; } //</deprecated> }, // inherit docs setProxy: function(proxy) { var reader, needsRoot; if (proxy instanceof Ext.data.proxy.Proxy) { // proxy instance, check if a root was set needsRoot = Ext.isEmpty(proxy.getReader().root); } else if (Ext.isString(proxy)) { // string type, means a reader can't be set needsRoot = true; } else { // object, check if a reader and a root were specified. reader = proxy.reader; needsRoot = !(reader && !Ext.isEmpty(reader.root)); } proxy = this.callParent(arguments); // The proxy sets a parameter to carry the entity ID based upon the Operation's id // That partameter name defaults to "id". // TreeStore however uses a nodeParam configuration to specify the entity id proxy.idParam = this.nodeParam; if (needsRoot) { reader = proxy.getReader(); reader.root = this.defaultRootProperty; // force rebuild reader.buildExtractors(true); } return proxy; }, // inherit docs onBeforeSort: function() { if (this.folderSort) { this.sort({ property: 'leaf', direction: 'ASC' }, 'prepend', false); } }, /** * Fired by the root node. * * Called before a node is expanded. * * 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, reader, dataRoot, data, callbackArgs; // Children are loaded go ahead with expand if (node.isLoaded()) { callbackArgs = [node.childNodes]; if (args) { callbackArgs.push.apply(callbackArgs, args); } Ext.callback(callback, scope || node, callbackArgs); } // There are unloaded child nodes in the raw data because of the lazy configuration, load them then call back. else if (dataRoot = (data = (node.raw || node[node.persistenceProperty])[(reader = me.getProxy().getReader()).root])) { me.fillNode(node, reader.extractData(dataRoot)); delete data[reader.root]; 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}); } // Node needs loading else { me.read({ node: node, callback: function() { // Clear the callback, since if we're introducing a custom one, // it may be re-used on reload delete me.lastOptions.callback; callbackArgs = [node.childNodes]; if (args) { callbackArgs.push.apply(callbackArgs, args); } Ext.callback(callback, scope || node, callbackArgs); } }); } }, //inherit docs getNewRecords: function() { return Ext.Array.filter(this.tree.flatten(), this.filterNew); }, //inherit docs getUpdatedRecords: function() { return Ext.Array.filter(this.tree.flatten(), this.filterUpdated); }, onNodeRemove: function(parent, node, isMove) { var me = this; node.unjoin(me); // Phantom nodes should never be included in the removed collection. // Also, if we're moving a node a remove will be fired, however we'll // be inserting it again, so don't push it into the removed collection if (!node.phantom && !isMove) { Ext.Array.include(me.removed, node); } if (me.autoSync && !me.autoSyncSuspended && !isMove) { me.sync(); } }, onNodeAdded: function(parent, node) { var me = this, proxy = me.getProxy(), reader = proxy.getReader(), data = node.raw || node[node.persistenceProperty], dataRoot; Ext.Array.remove(me.removed, node); node.join(me); // If node has raw data, load the child nodes from it. if (!node.isLeaf() && !me.lazyFill) { dataRoot = reader.getRoot(data); if (dataRoot) { me.fillNode(node, reader.extractData(dataRoot)); delete data[reader.root]; } } if (me.autoSync && !me.autoSyncSuspended && (node.phantom || node.dirty)) { me.sync(); } }, onNodeSort: function() { if (this.autoSync && !this.autoSyncSuspended) { this.sync(); } }, /** * Sets the root node for this store. See also the {@link #root} config option. * @param {Ext.data.Model/Ext.data.NodeInterface/Object} root * @return {Ext.data.NodeInterface} The new root */ setRootNode: function(root, /* private */ preventLoad) { var me = this, model = me.model, idProperty = model.prototype.idProperty root = root || {}; if (!root.isModel) { root = Ext.apply({}, root); // create a default rootNode and create internal data struct. Ext.applyIf(root, { id: me.defaultRootId, text: me.defaultRootText, allowDrag: false }); if (root[idProperty] === undefined) { root[idProperty] = me.defaultRootId; } Ext.data.NodeInterface.decorate(model); root = Ext.ModelManager.create(root, model); } else if (root.isModel && !root.isNode) { Ext.data.NodeInterface.decorate(model); } // Because we have decorated the model with new fields, // we need to build new extactor functions on the reader. me.getProxy().getReader().buildExtractors(true); // When we add the root to the tree, it will automaticaly get the NodeInterface me.tree.setRootNode(root); // If the user has set expanded: true on the root, we want to call the expand function to kick off // an expand process, so clear the expanded status and call expand. // Upon receipt, the expansion process is the most efficient way of processing the // returned nodes and putting them into the NodeStore in one block. // Appending a node to an expanded node is expensive - the NodeStore and UI are updated. if (preventLoad !== true && !root.isLoaded() && (me.autoLoad === true || root.isExpanded())) { root.data.expanded = false; root.expand(); } return root; }, /** * Returns the root node for this tree. * @return {Ext.data.NodeInterface} */ getRootNode: function() { return this.tree.getRootNode(); }, /** * Returns the record node by id * @return {Ext.data.NodeInterface} */ getNodeById: function(id) { return this.tree.getNodeById(id); }, // inherit docs getById: function(id) { return this.getNodeById(id); }, /** * Loads the Store using its configured {@link #proxy}. * @param {Object} options (Optional) config object. This is passed into the {@link Ext.data.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. */ load: function(options) { options = options || {}; options.params = options.params || {}; var me = this, node = options.node || me.tree.getRootNode(); // If there is not a node it means the user hasnt defined a rootnode yet. In this case lets just // create one for them. if (!node) { node = me.setRootNode({ expanded: true }, true); } // 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(); if (me.clearOnLoad) { if(me.clearRemovedOnLoad) { // 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); } // temporarily remove the onNodeRemove event listener so that when removeAll is called, the removed nodes do not get added to the removed array me.tree.un('remove', me.onNodeRemove, me); // remove all the nodes node.removeAll(false); // reattach the onNodeRemove listener me.tree.on('remove', me.onNodeRemove, me); } Ext.applyIf(options, { node: node }); me.callParent([options]); if (me.loading && node) { node.set('loading', true); } return me; }, /** * 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.removed, id = node.getId(), removedLength = removed.length, i = removedLength, recordsToClear = {}, newRemoved = [], removedHash = {}, removedNode, targetNode, targetId; if(node === me.getRootNode()) { // if the passed node is the root node, just reset the removed array me.removed = []; 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 targetId = targetNode.get('parentId'); 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.removed = newRemoved; }, /** * Fills a node with a series of child records. * @private * @param {Ext.data.NodeInterface} node The node to fill * @param {Ext.data.Model[]} newNodes The records to add */ fillNode: function(node, newNodes) { var me = this, ln = newNodes ? newNodes.length : 0, sorters = me.sorters, i, sortCollection, needsIndexSort = false, performLocalSort = ln && me.sortOnLoad && !me.remoteSort && sorters && sorters.items && sorters.items.length, node1, node2, rootFill; // 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 < ln; i++) { node1 = newNodes[i]; node2 = newNodes[i - 1]; needsIndexSort = node1[node1.persistenceProperty].index != node2[node2.persistenceProperty].index; if (needsIndexSort) { break; } } // If there is a set of local sorters defined. if (performLocalSort) { // If sorting by index is needed, sort by index first if (needsIndexSort) { me.sorters.insert(0, me.indexSorter); } sortCollection = new Ext.util.MixedCollection(); sortCollection.addAll(newNodes); sortCollection.sort(me.sorters.items); newNodes = sortCollection.items; // Remove the index sorter me.sorters.remove(me.indexSorter); } else if (needsIndexSort) { Ext.Array.sort(newNodes, me.sortByIndex); } node.set('loaded', true); // Fill node gets called recursively (indirectly) as we're populating the // nodes via a load or when appending a new child. As such, when we hit // the top most node, we fire an event to let the view know we'll be doing // a bulk operation so it can take appropriate action rootFill = me.fillCount === 0; if (rootFill) { // internal event me.fireEvent('beforefill', me, node, newNodes); } ++me.fillCount; if (newNodes.length) { node.appendChild(newNodes, undefined, true); } if (rootFill) { // internal event me.fireEvent('fillcomplete', me, node, newNodes); } --me.fillCount; return newNodes; }, /** * 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[node1.persistenceProperty].index - node2[node2.persistenceProperty].index; }, onIdChanged: function(model, oldId, newId, oldInternalId){ this.tree.onNodeIdChanged(model, oldId, newId, oldInternalId); this.callParent(arguments); }, // inherit docs onProxyLoad: function(operation) { var me = this, successful = operation.wasSuccessful(), records = operation.getRecords(), node = operation.node; me.loading = false; node.set('loading', false); if (successful) { if (!me.clearOnLoad) { records = me.cleanRecords(node, records); } 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.NodeInterface} node The node that was loaded. * @param {Ext.data.Model[]} records An array of records. * @param {Boolean} successful True if the operation was successful. */ // deprecate read? me.fireEvent('read', me, operation.node, records, successful); me.fireEvent('load', me, operation.node, records, successful); //this is a callback that would have been passed to the 'read' function and is optional Ext.callback(operation.callback, operation.scope || me, [records, operation, successful]); }, 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; }, // inherit docs removeAll: function() { var root = this.getRootNode(); if (root) { root.destroy(true); } this.fireEvent('clear', this); }, // inherit docs doSort: function(sorterFn) { var me = this; if (me.remoteSort) { //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()); } }, function() { var proto = this.prototype; proto.indexSorter = new Ext.util.Sorter({ sorterFn: proto.sortByIndex }); });