3 This file is part of Ext JS 4
5 Copyright (c) 2011 Sencha Inc
7 Contact: http://www.sencha.com/contact
9 GNU General Public License Usage
10 This file may be used under the terms of the GNU General Public License version 3.0 as published by the Free Software Foundation and appearing in the file LICENSE included in the packaging of this file. Please review the following information to ensure the GNU General Public License version 3.0 requirements will be met: http://www.gnu.org/copyleft/gpl.html.
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
16 * The TreeStore is a store implementation that is backed by by an {@link Ext.data.Tree}.
17 * It provides convenience methods for loading nodes, as well as the ability to use
18 * the hierarchical tree structure combined with a store. This class is generally used
19 * in conjunction with {@link Ext.tree.Panel}. This class also relays many events from
20 * the Tree for convenience.
24 * If no Model is specified, an implicit model will be created that implements {@link Ext.data.NodeInterface}.
25 * The standard Tree fields will also be copied onto the Model for maintaining their state. These fields are listed
26 * in the {@link Ext.data.NodeInterface} documentation.
28 * # Reading Nested Data
30 * For the tree to read nested data, the {@link Ext.data.reader.Reader} must be configured with a root property,
31 * so the reader can find nested data for each node. If a root is not specified, it will default to
34 Ext.define('Ext.data.TreeStore', {
35 extend: 'Ext.data.AbstractStore',
37 requires: ['Ext.data.Tree', 'Ext.data.NodeInterface', 'Ext.data.NodeStore'],
40 * @cfg {Ext.data.Model/Ext.data.NodeInterface/Object} root
41 * The root node for this store. For example:
47 * { text: "Child 1", leaf: true },
48 * { text: "Child 2", expanded: true, children: [
49 * { text: "GrandChild", leaf: true }
54 * Setting the `root` config option is the same as calling {@link #setRootNode}.
58 * @cfg {Boolean} clearOnLoad
59 * Remove previously existing child nodes before loading. Default to true.
64 * @cfg {String} nodeParam
65 * The name of the parameter sent to the server which contains the identifier of the node.
71 * @cfg {String} defaultRootId
72 * The default root id. Defaults to 'root'
74 defaultRootId: 'root',
77 * @cfg {String} defaultRootProperty
78 * The root property to specify on the reader if one is not explicitly defined.
80 defaultRootProperty: 'children',
83 * @cfg {Boolean} folderSort
84 * Set to true to automatically prepend a leaf sorter. Defaults to `undefined`.
88 constructor: function(config) {
93 config = Ext.apply({}, config);
96 * If we have no fields declare for the store, add some defaults.
97 * These will be ignored if a model is explicitly specified.
99 fields = config.fields || me.fields;
101 config.fields = [{name: 'text', type: 'string'}];
104 me.callParent([config]);
106 // We create our data tree.
107 me.tree = Ext.create('Ext.data.Tree');
109 me.relayEvents(me.tree, [
112 * @alias Ext.data.Tree#append
118 * @alias Ext.data.Tree#remove
124 * @alias Ext.data.Tree#move
130 * @alias Ext.data.Tree#insert
135 * @event beforeappend
136 * @alias Ext.data.Tree#beforeappend
141 * @event beforeremove
142 * @alias Ext.data.Tree#beforeremove
148 * @alias Ext.data.Tree#beforemove
153 * @event beforeinsert
154 * @alias Ext.data.Tree#beforeinsert
160 * @alias Ext.data.Tree#expand
166 * @alias Ext.data.Tree#collapse
171 * @event beforeexpand
172 * @alias Ext.data.Tree#beforeexpand
177 * @event beforecollapse
178 * @alias Ext.data.Tree#beforecollapse
184 * @alias Ext.data.Tree#rootchange
191 remove: me.onNodeRemove,
192 // this event must follow the relay to beforeitemexpand to allow users to
193 // cancel the expand:
194 beforeexpand: me.onBeforeNodeExpand,
195 beforecollapse: me.onBeforeNodeCollapse,
196 append: me.onNodeAdded,
197 insert: me.onNodeAdded
205 me.setRootNode(root);
211 * Fires when this TreeStore is sorted.
212 * @param {Ext.data.NodeInterface} node The node that is sorted.
217 //<deprecated since=0.99>
218 if (Ext.isDefined(me.nodeParameter)) {
219 if (Ext.isDefined(Ext.global.console)) {
220 Ext.global.console.warn('Ext.data.TreeStore: nodeParameter has been deprecated. Please use nodeParam instead.');
222 me.nodeParam = me.nodeParameter;
223 delete me.nodeParameter;
229 setProxy: function(proxy) {
233 if (proxy instanceof Ext.data.proxy.Proxy) {
234 // proxy instance, check if a root was set
235 needsRoot = Ext.isEmpty(proxy.getReader().root);
236 } else if (Ext.isString(proxy)) {
237 // string type, means a reader can't be set
240 // object, check if a reader and a root were specified.
241 reader = proxy.reader;
242 needsRoot = !(reader && !Ext.isEmpty(reader.root));
244 proxy = this.callParent(arguments);
246 reader = proxy.getReader();
247 reader.root = this.defaultRootProperty;
249 reader.buildExtractors(true);
254 onBeforeSort: function() {
255 if (this.folderSort) {
259 }, 'prepend', false);
264 * Called before a node is expanded.
266 * @param {Ext.data.NodeInterface} node The node being expanded.
267 * @param {Function} callback The function to run after the expand finishes
268 * @param {Object} scope The scope in which to run the callback function
270 onBeforeNodeExpand: function(node, callback, scope) {
271 if (node.isLoaded()) {
272 Ext.callback(callback, scope || node, [node.childNodes]);
274 else if (node.isLoading()) {
275 this.on('load', function() {
276 Ext.callback(callback, scope || node, [node.childNodes]);
277 }, this, {single: true});
282 callback: function() {
283 Ext.callback(callback, scope || node, [node.childNodes]);
290 getNewRecords: function() {
291 return Ext.Array.filter(this.tree.flatten(), this.filterNew);
295 getUpdatedRecords: function() {
296 return Ext.Array.filter(this.tree.flatten(), this.filterUpdated);
300 * Called before a node is collapsed.
302 * @param {Ext.data.NodeInterface} node The node being collapsed.
303 * @param {Function} callback The function to run after the collapse finishes
304 * @param {Object} scope The scope in which to run the callback function
306 onBeforeNodeCollapse: function(node, callback, scope) {
307 callback.call(scope || node, node.childNodes);
310 onNodeRemove: function(parent, node) {
311 var removed = this.removed;
313 if (!node.isReplace && Ext.Array.indexOf(removed, node) == -1) {
318 onNodeAdded: function(parent, node) {
319 var proxy = this.getProxy(),
320 reader = proxy.getReader(),
321 data = node.raw || node.data,
324 Ext.Array.remove(this.removed, node);
326 if (!node.isLeaf() && !node.isLoaded()) {
327 dataRoot = reader.getRoot(data);
329 this.fillNode(node, reader.extractData(dataRoot));
330 delete data[reader.root];
336 * Sets the root node for this store. See also the {@link #root} config option.
337 * @param {Ext.data.Model/Ext.data.NodeInterface/Object} root
338 * @return {Ext.data.NodeInterface} The new root
340 setRootNode: function(root) {
345 // create a default rootNode and create internal data struct.
347 id: me.defaultRootId,
351 root = Ext.ModelManager.create(root, me.model);
353 Ext.data.NodeInterface.decorate(root);
355 // Because we have decorated the model with new fields,
356 // we need to build new extactor functions on the reader.
357 me.getProxy().getReader().buildExtractors(true);
359 // When we add the root to the tree, it will automaticaly get the NodeInterface
360 me.tree.setRootNode(root);
362 // If the user has set expanded: true on the root, we want to call the expand function
363 if (!root.isLoaded() && (me.autoLoad === true || root.isExpanded())) {
373 * Returns the root node for this tree.
374 * @return {Ext.data.NodeInterface}
376 getRootNode: function() {
377 return this.tree.getRootNode();
381 * Returns the record node by id
382 * @return {Ext.data.NodeInterface}
384 getNodeById: function(id) {
385 return this.tree.getNodeById(id);
389 * Loads the Store using its configured {@link #proxy}.
390 * @param {Object} options (Optional) config object. This is passed into the {@link Ext.data.Operation Operation}
391 * object that is created and then sent to the proxy's {@link Ext.data.proxy.Proxy#read} function.
392 * The options can also contain a node, which indicates which node is to be loaded. If not specified, it will
393 * default to the root node.
395 load: function(options) {
396 options = options || {};
397 options.params = options.params || {};
400 node = options.node || me.tree.getRootNode(),
403 // If there is not a node it means the user hasnt defined a rootnode yet. In this case lets just
404 // create one for them.
406 node = me.setRootNode({
411 if (me.clearOnLoad) {
412 node.removeAll(true);
415 Ext.applyIf(options, {
418 options.params[me.nodeParam] = node ? node.getId() : 'root';
421 node.set('loading', true);
424 return me.callParent([options]);
429 * Fills a node with a series of child records.
431 * @param {Ext.data.NodeInterface} node The node to fill
432 * @param {Ext.data.Model[]} records The records to add
434 fillNode: function(node, records) {
436 ln = records ? records.length : 0,
437 i = 0, sortCollection;
439 if (ln && me.sortOnLoad && !me.remoteSort && me.sorters && me.sorters.items) {
440 sortCollection = Ext.create('Ext.util.MixedCollection');
441 sortCollection.addAll(records);
442 sortCollection.sort(me.sorters.items);
443 records = sortCollection.items;
446 node.set('loaded', true);
447 for (; i < ln; i++) {
448 node.appendChild(records[i], undefined, true);
455 onProxyLoad: function(operation) {
457 successful = operation.wasSuccessful(),
458 records = operation.getRecords(),
459 node = operation.node;
462 node.set('loading', false);
464 records = me.fillNode(node, records);
466 // The load event has an extra node parameter
467 // (differing from the load event described in AbstractStore)
470 * Fires whenever the store reads data from a remote data source.
471 * @param {Ext.data.TreeStore} this
472 * @param {Ext.data.NodeInterface} node The node that was loaded.
473 * @param {Ext.data.Model[]} records An array of records.
474 * @param {Boolean} successful True if the operation was successful.
477 me.fireEvent('read', me, operation.node, records, successful);
478 me.fireEvent('load', me, operation.node, records, successful);
479 //this is a callback that would have been passed to the 'read' function and is optional
480 Ext.callback(operation.callback, operation.scope || me, [records, operation, successful]);
484 * Creates any new records when a write is returned from the server.
486 * @param {Ext.data.Model[]} records The array of new records
487 * @param {Ext.data.Operation} operation The operation that just completed
488 * @param {Boolean} success True if the operation was successful
490 onCreateRecords: function(records, operation, success) {
493 length = records.length,
494 originalRecords = operation.records,
501 * Loop over each record returned from the server. Assume they are
502 * returned in order of how they were sent. If we find a matching
503 * record, replace it with the newly created one.
505 for (; i < length; ++i) {
507 original = originalRecords[i];
509 parentNode = original.parentNode;
511 // prevent being added to the removed cache
512 original.isReplace = true;
513 parentNode.replaceChild(record, original);
514 delete original.isReplace;
516 record.phantom = false;
523 * Updates any records when a write is returned from the server.
525 * @param {Ext.data.Model[]} records The array of updated records
526 * @param {Ext.data.Operation} operation The operation that just completed
527 * @param {Boolean} success True if the operation was successful
529 onUpdateRecords: function(records, operation, success){
533 length = records.length,
539 for (; i < length; ++i) {
541 original = me.tree.getNodeById(record.getId());
542 parentNode = original.parentNode;
544 // prevent being added to the removed cache
545 original.isReplace = true;
546 parentNode.replaceChild(record, original);
547 original.isReplace = false;
554 * Removes any records when a write is returned from the server.
556 * @param {Ext.data.Model[]} records The array of removed records
557 * @param {Ext.data.Operation} operation The operation that just completed
558 * @param {Boolean} success True if the operation was successful
560 onDestroyRecords: function(records, operation, success){
567 removeAll: function() {
568 this.getRootNode().destroy(true);
569 this.fireEvent('clear', this);
573 doSort: function(sorterFn) {
576 //the load function will pick up the new sorters and request the sorted data from the proxy
579 me.tree.sort(sorterFn, true);
580 me.fireEvent('datachanged', me);
582 me.fireEvent('sort', me);