/** * Node Store * @private */ Ext.define('Ext.data.NodeStore', { extend: 'Ext.data.Store', alias: 'store.node', requires: ['Ext.data.NodeInterface'], /** * @property {Boolean} isNodeStore * `true` in this class to identify an object as an instantiated NodeStore, or subclass thereof. */ isNodeStore: true, /** * @cfg {Ext.data.Model} node * The Record you want to bind this Store to. Note that * this record will be decorated with the Ext.data.NodeInterface if this is not the * case yet. */ node: null, /** * @cfg {Boolean} recursive * Set this to true if you want this NodeStore to represent * all the descendents of the node in its flat data collection. This is useful for * rendering a tree structure to a DataView and is being used internally by * the TreeView. Any records that are moved, removed, inserted or appended to the * node at any depth below the node this store is bound to will be automatically * updated in this Store's internal flat data structure. */ recursive: false, /** * @cfg {Boolean} rootVisible * False to not include the root node in this Stores collection. */ rootVisible: false, /** * @cfg {Ext.data.TreeStore} treeStore * The TreeStore that is used by this NodeStore's Ext.tree.View. */ /** * @protected * Recursion level counter. Incremented when expansion or collaping of a node is initiated, * including when nested nodes below the expanding/collapsing root begin expanding or collapsing. * * This ensures that collapsestart, collapsecomplete, expandstart and expandcomplete only * fire on the top level node being collapsed/expanded. * * Also, allows listeners to the `add` and `remove` events to know whether a collapse of expand is in progress. */ isExpandingOrCollapsing: 0, constructor: function(config) { var me = this, node; config = config || {}; Ext.apply(me, config); //<debug> if (Ext.isDefined(me.proxy)) { Ext.Error.raise("A NodeStore cannot be bound to a proxy. Instead bind it to a record " + "decorated with the NodeInterface by setting the node config."); } me.useModelWarning = false; //</debug> config.proxy = {type: 'proxy'}; me.callParent([config]); node = me.node; if (node) { me.node = null; me.setNode(node); } }, // NodeStores are never buffered or paged. They are loaded from the TreeStore to reflect all visible // nodes. // BufferedRenderer always asks for the *total* count, so this must return the count. getTotalCount: function() { return this.getCount(); }, setNode: function(node) { var me = this; if (me.node && me.node != node) { // We want to unbind our listeners on the old node me.mun(me.node, { expand: me.onNodeExpand, collapse: me.onNodeCollapse, append: me.onNodeAppend, insert: me.onNodeInsert, bulkremove: me.onBulkRemove, remove: me.onNodeRemove, sort: me.onNodeSort, scope: me }); me.node = null; } if (node) { Ext.data.NodeInterface.decorate(node.self); me.removeAll(); if (me.rootVisible) { me.add(node); } else if (!node.isExpanded() && me.treeStore.autoLoad !== false) { node.expand(); } me.mon(node, { expand: me.onNodeExpand, collapse: me.onNodeCollapse, append: me.onNodeAppend, insert: me.onNodeInsert, bulkremove: me.onBulkRemove, remove: me.onNodeRemove, sort: me.onNodeSort, scope: me }); me.node = node; if (node.isExpanded() && node.isLoaded()) { me.onNodeExpand(node, node.childNodes, true); } } }, onNodeSort: function(node, childNodes) { var me = this; if ((me.indexOf(node) !== -1 || (node === me.node && !me.rootVisible) && node.isExpanded())) { Ext.suspendLayouts(); me.onNodeCollapse(node, childNodes, true); me.onNodeExpand(node, childNodes, true); Ext.resumeLayouts(true); } }, // Triggered by a NodeInterface's bubbled "expand" event. onNodeExpand: function(parent, records, suppressEvent) { var me = this, insertIndex = me.indexOf(parent) + 1, toAdd = []; // Used by the TreeView to bracket recursive expand & collapse ops // and refresh the size. This is most effective when folder nodes are loaded, // and this method is able to recurse. if (!suppressEvent) { me.fireEvent('beforeexpand', parent, records, insertIndex); } me.handleNodeExpand(parent, records, 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. // The datachanged event is relayed by the TreeStore. Internally, that's not used. me.insert(insertIndex, toAdd); // Triggers the TreeView's onExpand method which calls refreshSize, // and fires its afteritemexpand event if (!suppressEvent) { me.fireEvent('expand', parent, records); } }, // 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; // recursive is hardcoded to true in TreeView. if (!me.recursive && parent !== me.node) { return; } if (parent !== this.node && !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]; // 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(); } } } } }, // Triggered by NodeInterface's bubbled bulkremove event onBulkRemove: function(parent, childNodes, isMove) { this.onNodeCollapse(parent, childNodes, true); }, // Triggered by a NodeInterface's bubbled "collapse" event. onNodeCollapse: function(parent, records, suppressEvent, callback, scope) { var me = this, collapseIndex = me.indexOf(parent) + 1, node, lastNodeIndexPlus, sibling, found; if (!me.recursive && parent !== me.node) { return; } // Used by the TreeView to bracket recursive expand & collapse ops. // The TreeViewsets up the animWrap object if we are animating. // It also caches the collapse callback to call when it receives the // end collapse event. See below. if (!suppressEvent) { me.fireEvent('beforecollapse', parent, records, collapseIndex, callback, scope); } // 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.data.contains(records[0])) { // Calculate the index *one beyond* the last node we are going to remove // Need to loop up the tree to find the nearest view sibling, since it could // exist at some level above the current node. node = parent; while (node.parentNode) { sibling = node.nextSibling; if (sibling) { found = true; lastNodeIndexPlus = me.indexOf(sibling); break; } else { node = node.parentNode; } } if (!found) { lastNodeIndexPlus = me.getCount(); } // Remove the whole collapsed node set. me.removeAt(collapseIndex, lastNodeIndexPlus - collapseIndex); } // Triggers the TreeView's onCollapse method which calls refreshSize, // and fires its afteritecollapse event if (!suppressEvent) { me.fireEvent('collapse', parent, records, collapseIndex); } }, onNodeAppend: function(parent, node, index) { var me = this, refNode, sibling; // Only react to a node append if it is to a node which is expanded, and is part of a tree if (me.isVisible(node)) { if (index === 0) { refNode = parent; } else { sibling = node.previousSibling; while (sibling.isExpanded() && sibling.lastChild) { sibling = sibling.lastChild; } refNode = sibling; } me.insert(me.indexOf(refNode) + 1, node); if (!node.isLeaf() && node.isExpanded()) { if (node.isLoaded()) { // Take a shortcut me.onNodeExpand(node, node.childNodes, true); } else if (!me.treeStore.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(); } } } }, onNodeInsert: function(parent, node, refNode) { var me = this, index = this.indexOf(refNode); if (index != -1 && me.isVisible(node)) { me.insert(index, node); if (!node.isLeaf() && node.isExpanded()) { if (node.isLoaded()) { // Take a shortcut me.onNodeExpand(node, node.childNodes, true); } else { node.set('expanded', false); node.expand(); } } } }, onNodeRemove: function(parent, node, isMove) { var me = this; if (me.indexOf(node) != -1) { // If the removed node is a non-leaf and is expanded, use the onCollapse method to get rid // of all descendants at any level. if (!node.isLeaf() && node.isExpanded()) { // onCollapse expects to be able to use the "collapsing" node's parentNode // and nextSibling pointers so temporarily reinstate them. // Reinstating them is safe because we pass the suppressEvents flag, and no user code // is executed. node.parentNode = node.removeContext.parentNode; node.nextSibling = node.removeContext.nextSibling; me.onNodeCollapse(node, node.childNodes, true); node.parentNode = node.nextSibling = null; } me.remove(node); } }, isVisible: function(node) { var parent = node.parentNode; while (parent) { // Hit root and it is expanded, the node is visible if (parent === this.node && parent.data.expanded) { return true; } // Hit a collapsed ancestor, the node is not visible if (!parent.data.expanded) { return false; } parent = parent.parentNode; } // Walked off the top - the node is not part of the tree structure return false; } });