X-Git-Url: http://git.ithinksw.org/extjs.git/blobdiff_plain/0494b8d9b9bb03ab6c22b34dae81261e3cd7e3e6..7a654f8d43fdb43d78b63d90528bed6e86b608cc:/src/data/TreeStore.js diff --git a/src/data/TreeStore.js b/src/data/TreeStore.js new file mode 100644 index 00000000..ae105cf2 --- /dev/null +++ b/src/data/TreeStore.js @@ -0,0 +1,582 @@ +/** + * @class Ext.data.TreeStore + * @extends Ext.data.AbstractStore + * + * 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. + * + * ## Reading Nested Data + * For the tree to read nested data, the {@link Ext.data.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 {Boolean} clearOnLoad (optional) Default to true. Remove previously existing + * child nodes before loading. + */ + 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.tree.on({ + scope: me, + remove: me.onNodeRemove, + 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.relayEvents(me.tree, [ + /** + * @event append + * Fires when a new child node is appended to a node in this store's tree. + * @param {Tree} tree The owner tree + * @param {Node} parent The parent node + * @param {Node} node The newly appended node + * @param {Number} index The index of the newly appended node + */ + "append", + + /** + * @event remove + * Fires when a child node is removed from a node in this store's tree. + * @param {Tree} tree The owner tree + * @param {Node} parent The parent node + * @param {Node} node The child node removed + */ + "remove", + + /** + * @event move + * Fires when a node is moved to a new location in the store's tree + * @param {Tree} tree The owner tree + * @param {Node} node The node moved + * @param {Node} oldParent The old parent of this node + * @param {Node} newParent The new parent of this node + * @param {Number} index The index it was moved to + */ + "move", + + /** + * @event insert + * Fires when a new child node is inserted in a node in this store's tree. + * @param {Tree} tree The owner tree + * @param {Node} parent The parent node + * @param {Node} node The child node inserted + * @param {Node} refNode The child node the node was inserted before + */ + "insert", + + /** + * @event beforeappend + * Fires before a new child is appended to a node in this store's tree, return false to cancel the append. + * @param {Tree} tree The owner tree + * @param {Node} parent The parent node + * @param {Node} node The child node to be appended + */ + "beforeappend", + + /** + * @event beforeremove + * Fires before a child is removed from a node in this store's tree, return false to cancel the remove. + * @param {Tree} tree The owner tree + * @param {Node} parent The parent node + * @param {Node} node The child node to be removed + */ + "beforeremove", + + /** + * @event beforemove + * Fires before a node is moved to a new location in the store's tree. Return false to cancel the move. + * @param {Tree} tree The owner tree + * @param {Node} node The node being moved + * @param {Node} oldParent The parent of the node + * @param {Node} newParent The new parent the node is moving to + * @param {Number} index The index it is being moved to + */ + "beforemove", + + /** + * @event beforeinsert + * Fires before a new child is inserted in a node in this store's tree, return false to cancel the insert. + * @param {Tree} tree The owner tree + * @param {Node} parent The parent node + * @param {Node} node The child node to be inserted + * @param {Node} refNode The child node the node is being inserted before + */ + "beforeinsert", + + /** + * @event expand + * Fires when this node is expanded. + * @param {Node} this The expanding node + */ + "expand", + + /** + * @event collapse + * Fires when this node is collapsed. + * @param {Node} this The collapsing node + */ + "collapse", + + /** + * @event beforeexpand + * Fires before this node is expanded. + * @param {Node} this The expanding node + */ + "beforeexpand", + + /** + * @event beforecollapse + * Fires before this node is collapsed. + * @param {Node} this The collapsing node + */ + "beforecollapse", + + /** + * @event sort + * Fires when this TreeStore is sorted. + * @param {Node} node The node that is sorted. + */ + "sort", + + /** + * @event rootchange + * Fires whenever the root node is changed in the tree. + * @param {Ext.data.Model} root The new root + */ + "rootchange" + ]); + + me.addEvents( + /** + * @event rootchange + * Fires when the root node on this TreeStore is changed. + * @param {Ext.data.TreeStore} store This TreeStore + * @param {Node} The new root node. + */ + 'rootchange' + ); + + // + 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; + } + // + }, + + // 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 + * @param {Ext.data.Model/Ext.data.NodeInterface} 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() && 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(); + } + + 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 {Array} 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; + + node.set('loading', false); + if (successful) { + records = me.fillNode(node, records); + } + // 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]); + }, + + /** + * Create any new records when a write is returned from the server. + * @private + * @param {Array} 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; + } + } + } + }, + + /** + * Update any records when a write is returned from the server. + * @private + * @param {Array} 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; + } + } + } + }, + + /** + * Remove any records when a write is returned from the server. + * @private + * @param {Array} 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(); + 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); + } +}); \ No newline at end of file