/** * 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'. */ Ext.define('Ext.data.TreeStore', { extend: 'Ext.data.AbstractStore', alias: 'store.tree', requires: ['Ext.data.Tree', 'Ext.data.NodeInterface', 'Ext.data.NodeStore'], /** * @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 * Remove previously existing child nodes before loading. Default to true. */ clearOnLoad : true, /** * @cfg {String} nodeParam * The name of the parameter sent to the server which contains the identifier of the node. * Defaults to 'node'. */ nodeParam: 'node', /** * @cfg {String} defaultRootId * The default root id. Defaults to 'root' */ defaultRootId: 'root', /** * @cfg {String} defaultRootProperty * The root property to specify on the reader if one is not explicitly defined. */ defaultRootProperty: 'children', /** * @cfg {Boolean} folderSort * Set to true to automatically prepend a leaf sorter. Defaults to `undefined`. */ folderSort: false, constructor: function(config) { var me = this, root, fields; 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'}]; } me.callParent([config]); // We create our data tree. me.tree = Ext.create('Ext.data.Tree'); me.relayEvents(me.tree, [ /** * @event append * @alias Ext.data.Tree#append */ "append", /** * @event remove * @alias Ext.data.Tree#remove */ "remove", /** * @event move * @alias Ext.data.Tree#move */ "move", /** * @event insert * @alias Ext.data.Tree#insert */ "insert", /** * @event beforeappend * @alias Ext.data.Tree#beforeappend */ "beforeappend", /** * @event beforeremove * @alias Ext.data.Tree#beforeremove */ "beforeremove", /** * @event beforemove * @alias Ext.data.Tree#beforemove */ "beforemove", /** * @event beforeinsert * @alias Ext.data.Tree#beforeinsert */ "beforeinsert", /** * @event expand * @alias Ext.data.Tree#expand */ "expand", /** * @event collapse * @alias Ext.data.Tree#collapse */ "collapse", /** * @event beforeexpand * @alias Ext.data.Tree#beforeexpand */ "beforeexpand", /** * @event beforecollapse * @alias Ext.data.Tree#beforecollapse */ "beforecollapse", /** * @event rootchange * @alias Ext.data.Tree#rootchange */ "rootchange" ]); 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, beforecollapse: me.onBeforeNodeCollapse, append: me.onNodeAdded, insert: me.onNodeAdded }); me.onBeforeSort(); root = me.root; if (root) { delete me.root; me.setRootNode(root); } me.addEvents( /** * @event sort * Fires when this TreeStore is sorted. * @param {Ext.data.NodeInterface} node The node that is sorted. */ 'sort' ); //<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); if (needsRoot) { reader = proxy.getReader(); reader.root = this.defaultRootProperty; // force rebuild reader.buildExtractors(true); } }, // inherit docs onBeforeSort: function() { if (this.folderSort) { this.sort({ property: 'leaf', direction: 'ASC' }, 'prepend', false); } }, /** * Called before a node is expanded. * @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 */ onBeforeNodeExpand: function(node, callback, scope) { if (node.isLoaded()) { Ext.callback(callback, scope || node, [node.childNodes]); } else if (node.isLoading()) { this.on('load', function() { Ext.callback(callback, scope || node, [node.childNodes]); }, this, {single: true}); } else { this.read({ node: node, callback: function() { Ext.callback(callback, scope || node, [node.childNodes]); } }); } }, //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); }, /** * Called before a node is collapsed. * @private * @param {Ext.data.NodeInterface} node The node being collapsed. * @param {Function} callback The function to run after the collapse finishes * @param {Object} scope The scope in which to run the callback function */ onBeforeNodeCollapse: function(node, callback, scope) { callback.call(scope || node, node.childNodes); }, onNodeRemove: function(parent, node) { var removed = this.removed; if (!node.isReplace && Ext.Array.indexOf(removed, node) == -1) { removed.push(node); } }, onNodeAdded: function(parent, node) { var proxy = this.getProxy(), reader = proxy.getReader(), data = node.raw || node.data, dataRoot, children; Ext.Array.remove(this.removed, node); if (!node.isLeaf() && !node.isLoaded()) { dataRoot = reader.getRoot(data); if (dataRoot) { this.fillNode(node, reader.extractData(dataRoot)); delete data[reader.root]; } } }, /** * 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) { var me = this; root = root || {}; if (!root.isNode) { // create a default rootNode and create internal data struct. Ext.applyIf(root, { id: me.defaultRootId, text: 'Root', allowDrag: false }); root = Ext.ModelManager.create(root, me.model); } Ext.data.NodeInterface.decorate(root); // 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 if (!root.isLoaded() && (me.autoLoad === true || root.isExpanded())) { me.load({ node: root }); } 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); }, /** * 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(), root; // 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 }); } if (me.clearOnLoad) { node.removeAll(true); } Ext.applyIf(options, { node: node }); options.params[me.nodeParam] = node ? node.getId() : 'root'; if (node) { node.set('loading', true); } return me.callParent([options]); }, /** * Fills a node with a series of child records. * @private * @param {Ext.data.NodeInterface} node The node to fill * @param {Ext.data.Model[]} records The records to add */ fillNode: function(node, records) { var me = this, ln = records ? records.length : 0, i = 0, sortCollection; if (ln && me.sortOnLoad && !me.remoteSort && me.sorters && me.sorters.items) { sortCollection = Ext.create('Ext.util.MixedCollection'); sortCollection.addAll(records); sortCollection.sort(me.sorters.items); records = sortCollection.items; } node.set('loaded', true); for (; i < ln; i++) { node.appendChild(records[i], undefined, true); } return records; }, // 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) { 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]); }, /** * Creates any new records when a write is returned from the server. * @private * @param {Ext.data.Model[]} records The array of new records * @param {Ext.data.Operation} operation The operation that just completed * @param {Boolean} success True if the operation was successful */ onCreateRecords: function(records, operation, success) { if (success) { var i = 0, length = records.length, originalRecords = operation.records, parentNode, record, original, index; /* * Loop over each record returned from the server. Assume they are * returned in order of how they were sent. If we find a matching * record, replace it with the newly created one. */ for (; i < length; ++i) { record = records[i]; original = originalRecords[i]; if (original) { parentNode = original.parentNode; if (parentNode) { // prevent being added to the removed cache original.isReplace = true; parentNode.replaceChild(record, original); delete original.isReplace; } record.phantom = false; } } } }, /** * Updates any records when a write is returned from the server. * @private * @param {Ext.data.Model[]} records The array of updated records * @param {Ext.data.Operation} operation The operation that just completed * @param {Boolean} success True if the operation was successful */ onUpdateRecords: function(records, operation, success){ if (success) { var me = this, i = 0, length = records.length, data = me.data, original, parentNode, record; for (; i < length; ++i) { record = records[i]; original = me.tree.getNodeById(record.getId()); parentNode = original.parentNode; if (parentNode) { // prevent being added to the removed cache original.isReplace = true; parentNode.replaceChild(record, original); original.isReplace = false; } } } }, /** * Removes any records when a write is returned from the server. * @private * @param {Ext.data.Model[]} records The array of removed records * @param {Ext.data.Operation} operation The operation that just completed * @param {Boolean} success True if the operation was successful */ onDestroyRecords: function(records, operation, success){ if (success) { this.removed = []; } }, // inherit docs removeAll: function() { this.getRootNode().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('sort', me); } });