/** * The TreePanel provides tree-structured UI representation of tree-structured data. * A TreePanel must be bound to a {@link Ext.data.TreeStore}. * * TreePanels support multiple columns through the {@link #columns} configuration. * * By default a TreePanel contains a single column which uses the `text` Field of * the store's nodes. * * Simple TreePanel using inline data: * * @example * var store = Ext.create('Ext.data.TreeStore', { * root: { * expanded: true, * children: [ * { text: "detention", leaf: true }, * { text: "homework", expanded: true, children: [ * { text: "book report", leaf: true }, * { text: "algebra", leaf: true} * ] }, * { text: "buy lottery tickets", leaf: true } * ] * } * }); * * Ext.create('Ext.tree.Panel', { * title: 'Simple Tree', * width: 200, * height: 150, * store: store, * rootVisible: false, * renderTo: Ext.getBody() * }); * * For the tree node config options (like `text`, `leaf`, `expanded`), see the documentation of * {@link Ext.data.NodeInterface NodeInterface} config options. * * Unless the TreeStore is configured with a {@link Ext.data.Model model} of your choosing, nodes in the {@link Ext.data.TreeStore} are by default, instances of {@link Ext.data.TreeModel}. * * # Heterogeneous node types. * * If the tree needs to use different data model classes at different levels there is much flexibility in how to specify this. * * ### Configuring the Reader. * If you configure the proxy's reader with a {@link Ext.data.reader.Reader#typeProperty typeProperty}, then the server is in control of which data model * types are created. A discriminator field is used in the raw data to decide which class to instantiate. * **If this is configured, then the data from the server is prioritized over other ways of determining node class**. * * @example * Ext.define('myApp.Territory', { * extend: 'Ext.data.TreeModel', * fields: [{ * name: 'text', * mapping: 'name' * }] * }); * Ext.define('myApp.Country', { * extend: 'Ext.data.TreeModel', * fields: [{ * name: 'text', * mapping: 'name' * }] * }); * Ext.define('myApp.City', { * extend: 'Ext.data.TreeModel', * fields: [{ * name: 'text', * mapping: 'name' * }] * }); * Ext.create('Ext.tree.Panel', { * renderTo: document.body, * height: 200, * width: 400, * title: 'Sales Areas - using typeProperty', * rootVisible: false, * store: { * // Child types use namespace of store's model by default * model: 'myApp.Territory', * proxy: { * type: 'memory', * reader: { * typeProperty: 'mtype' * } * }, * root: { * children: [{ * name: 'Europe, ME, Africa', * mtype: 'Territory', * children: [{ * name: 'UK of GB & NI', * mtype: 'Country', * children: [{ * name: 'London', * mtype: 'City', * leaf: true * }] * }] * }, { * name: 'North America', * mtype: 'Territory', * children: [{ * name: 'USA', * mtype: 'Country', * children: [{ * name: 'Redwood City', * mtype: 'City', * leaf: true * }] * }] * }] * } * } * }); * * ### Node being loaded decides. * You can declare your TreeModel subclasses with a {@link Ext.data.TreeModel#childType childType} which means that the node being loaded decides the * class to instantiate for all of its child nodes. * * It is important to note that if the root node is {@link Ext.tree.Panel#rootVisible hidden}, its type will default to the store's model type, and if left * as the default (`{@link Ext.data.TreeModel}`) this will have no knowledge of creation of special child node types. So be sure to specify a store model in this case: * * @example * Ext.define('myApp.TerritoryRoot', { * extend: 'Ext.data.TreeModel', * childType: 'myApp.Territory', * fields: [{ * name: 'text', * mapping: 'name' * }] * }); * Ext.define('myApp.Territory', { * extend: 'Ext.data.TreeModel', * childType: 'myApp.Country', * fields: [{ * name: 'text', * mapping: 'name' * }] * }); * Ext.define('myApp.Country', { * extend: 'Ext.data.TreeModel', * childType: 'myApp.City', * fields: [{ * name: 'text', * mapping: 'name' * }] * }); * Ext.define('myApp.City', { * extend: 'Ext.data.TreeModel', * fields: [{ * name: 'text', * mapping: 'name' * }] * }); * Ext.create('Ext.tree.Panel', { * renderTo: document.body, * height: 200, * width: 400, * title: 'Sales Areas', * rootVisible: false, * store: { * model: 'myApp.TerritoryRoot', // Needs to be this so it knows to create 'Country' child nodes * root: { * children: [{ * name: 'Europe, ME, Africa', * children: [{ * name: 'UK of GB & NI', * children: [{ * name: 'London', * leaf: true * }] * }] * }, { * name: 'North America', * children: [{ * name: 'USA', * children: [{ * name: 'Redwood City', * leaf: true * }] * }] * }] * } * } * }); * * # Data structure * * The {@link Ext.data.TreeStore TreeStore} maintains a {@link Ext.data.TreeStore#getRoot root node} and a hierarchical structure of {@link Ext.data.TreeModel node}s. * * The {@link Ext.tree.View UI} of the tree is driven by a {Ext.data.NodeStore NodeStore} which is a flattened view of *visible* nodes. * The NodeStore is dynamically updated to reflect the visibility state of nodes as nodes are added, removed or expanded. The UI * responds to mutation events fire by the NodeStore. * * Note that nodes have several more {@link Ext.data.Model#cfg-fields fields} in order to describe their state within the hierarchy. * * If you add store listeners to the {@link Ext.data.Store#event-update update} event, then you will recieve notification when any of this state changes. * You should check the array of modified field names passed to the listener to decide whether the listener should take action or ignore the event. */Ext.define('Ext.tree.Panel', { extend: 'Ext.panel.Table', alias: 'widget.treepanel', alternateClassName: ['Ext.tree.TreePanel', 'Ext.TreePanel'], requires: [ 'Ext.tree.View', 'Ext.selection.TreeModel', 'Ext.tree.Column', 'Ext.data.TreeStore', 'Ext.tree.NavigationModel' ], viewType: 'treeview', selType: 'treemodel', treeCls: Ext.baseCSSPrefix + 'tree-panel', /** * @cfg {Boolean} [rowLines=false] * Configure as true to separate rows with visible horizontal lines (depends on theme). */ rowLines: false, /** * @cfg {Boolean} [lines=true] * False to disable tree lines. */ lines: true, /** * @cfg {Boolean} [useArrows=false] * True to use Vista-style arrows in the tree. */ useArrows: false, /** * @cfg {Boolean} [singleExpand=false] * True if only 1 node per branch may be expanded. */ singleExpand: false, ddConfig: { enableDrag: true, enableDrop: true }, /** * @cfg {Boolean} animate * True to enable animated expand/collapse. Defaults to the value of {@link Ext#enableFx}. */ /** * @cfg {Boolean} [rootVisible=true] * False to hide the root node. * * Note that trees *always* have a root node. If you do not specify a {@link #cfg-root} node, one will be created. * * If the root node is not visible, then in order for a tree to appear to the end user, the root node is autoloaded with its child nodes. */ rootVisible: true, /** * @cfg {String} [displayField=text] * The field inside the model that will be used as the node's text. */ displayField: 'text', /** * @cfg {Ext.data.Model/Ext.data.TreeModel/Object} root * Allows you to not specify a store on this TreePanel. This is useful for creating a simple tree with preloaded * data without having to specify a TreeStore and Model. A store and model will be created and root will be passed * to that store. For example: * * Ext.create('Ext.tree.Panel', { * title: 'Simple Tree', * root: { * text: "Root node", * expanded: true, * children: [ * { text: "Child 1", leaf: true }, * { text: "Child 2", leaf: true } * ] * }, * renderTo: Ext.getBody() * }); */ root: null, // Required for the Lockable Mixin. These are the configurations which will be copied to the // normal and locked sub tablepanels normalCfgCopy: ['displayField', 'root', 'singleExpand', 'useArrows', 'lines', 'rootVisible', 'scroll'], lockedCfgCopy: ['displayField', 'root', 'singleExpand', 'useArrows', 'lines', 'rootVisible'], isTree: true, /** * @cfg {Boolean} hideHeaders * True to hide the headers. */ /** * @cfg {Boolean} folderSort * True to automatically prepend a leaf sorter to the store. */ /** * @cfg {Ext.data.TreeStore} store (required) * The {@link Ext.data.TreeStore Store} the tree should use as its data source. */ arrowCls: Ext.baseCSSPrefix + 'tree-arrows', linesCls: Ext.baseCSSPrefix + 'tree-lines', noLinesCls: Ext.baseCSSPrefix + 'tree-no-lines', autoWidthCls: Ext.baseCSSPrefix + 'autowidth-table', constructor: function(config) { config = config || {}; if (config.animate === undefined) { config.animate = Ext.isBoolean(this.animate) ? this.animate : Ext.enableFx; } this.enableAnimations = config.animate; delete config.animate; this.callParent([config]); }, initComponent: function() { var me = this, cls = [me.treeCls], store = me.store, view; if (me.useArrows) { cls.push(me.arrowCls); me.lines = false; } if (me.lines) { cls.push(me.linesCls); } else if (!me.useArrows) { cls.push(me.noLinesCls); } if (Ext.isString(store)) { store = me.store = Ext.StoreMgr.lookup(store); } else if (!store || !store.isStore) { store = Ext.apply({ type: 'tree', root: me.root, fields: me.fields, model: me.model, proxy: 'memory', folderSort: me.folderSort }, store); store = me.store = Ext.StoreMgr.lookup(store); } else if (me.root) { store = me.store = Ext.data.StoreManager.lookup(store); store.setRoot(me.root); if (me.folderSort !== undefined) { store.folderSort = me.folderSort; store.sort(); } } // Store must have the same idea about root visibility as us BEFORE callParent binds it. store.setRootVisible(me.rootVisible); // If there is no rootnode defined, then create one. if (!store.getRoot()) { store.setRoot({}); } me.viewConfig = Ext.apply({ rootVisible: me.rootVisible, animate: me.enableAnimations, singleExpand: me.singleExpand, node: store.getRoot(), hideHeaders: me.hideHeaders, navigationModel: 'tree' }, me.viewConfig); // If the user specifies the headers collection manually then dont inject our own if (!me.columns) { if (me.initialConfig.hideHeaders === undefined) { me.hideHeaders = true; } me.addCls(me.autoWidthCls); me.columns = [{ xtype : 'treecolumn', text : 'Name', flex : 1, dataIndex: me.displayField }]; } if (me.cls) { cls.push(me.cls); } me.cls = cls.join(' '); me.callParent(); view = me.getView(); // Relay events from the TreeView. // An injected LockingView relays events from its locked side's View me.relayEvents(view, [ /** * @event checkchange * Fires when a node with a checkbox's checked property changes * @param {Ext.data.TreeModel} node The node who's checked property was changed * @param {Boolean} checked The node's new checked state */ 'checkchange', /** * @event afteritemexpand * @inheritdoc Ext.tree.View#afteritemexpand */ 'afteritemexpand', /** * @event afteritemcollapse * @inheritdoc Ext.tree.View#afteritemcollapse */ 'afteritemcollapse' ]); }, getSelectionModel: function() { var result = this.callParent(); result.treeStore = this.getStore(); return result; }, // @private // Hook into the TreeStore. bindStore: function(store, initial) { var me = this, root = store.getRoot(), bufferedRenderer = me.bufferedRenderer; // Bind to store, and autocreate the BufferedRenderer. me.callParent(arguments); // If we're in a reconfigure (we already have a BufferedRenderer which is bound to our old store), // rebind the BufferedRenderer if (bufferedRenderer) { if (bufferedRenderer.store) { bufferedRenderer.bindStore(store); } } // Create a BufferedRenderer as a plugin if we have not already configured with one. else { bufferedRenderer = { xclass: 'Ext.grid.plugin.BufferedRenderer' }; Ext.copyTo(bufferedRenderer, me, 'variableRowHeight,numFromEdge,trailingBufferZone,leadingBufferZone,scrollToLoadBuffer'); me.bufferedRenderer = bufferedRenderer = me.addPlugin(bufferedRenderer); } // The TreeStore needs to know about this TreePanel's singleExpand constraint so that // it can ensure the compliance of NodeInterface.expandAll. store.singleExpand = me.singleExpand; // Monitor the TreeStore for the root node being changed. Return a Destroyable object me.storeListeners = me.mon(store, { destroyable: true, rootchange: me.onRootChange, scope: me }); // Relay store events. relayEvents always returns a Destroyable object. me.storeRelayers = me.relayEvents(store, [ /** * @event beforeload * @inheritdoc Ext.data.TreeStore#beforeload */ 'beforeload', /** * @event load * @inheritdoc Ext.data.TreeStore#load */ 'load' ]); // Relay store events with prefix. Return a Destroyable object me.rootRelayers = me.mon(root, { destroyable: true, /** * @event itemappend * @inheritdoc Ext.data.TreeStore#nodeappend */ append: me.createRelayer('itemappend'), /** * @event itemremove * @inheritdoc Ext.data.TreeStore#noderemove */ remove: me.createRelayer('itemremove'), /** * @event itemmove * @inheritdoc Ext.data.TreeStore#nodemove */ move: me.createRelayer('itemmove', [0, 4]), /** * @event iteminsert * @inheritdoc Ext.data.TreeStore#nodeinsert */ insert: me.createRelayer('iteminsert'), /** * @event beforeitemappend * @inheritdoc Ext.data.TreeStore#nodebeforeappend */ beforeappend: me.createRelayer('beforeitemappend'), /** * @event beforeitemremove * @inheritdoc Ext.data.TreeStore#nodebeforeremove */ beforeremove: me.createRelayer('beforeitemremove'), /** * @event beforeitemmove * @inheritdoc Ext.data.TreeStore#nodebeforemove */ beforemove: me.createRelayer('beforeitemmove'), /** * @event beforeiteminsert * @inheritdoc Ext.data.TreeStore#nodebeforeinsert */ beforeinsert: me.createRelayer('beforeiteminsert'), /** * @event itemexpand * @inheritdoc Ext.data.TreeStore#nodeexpand */ expand: me.createRelayer('itemexpand', [0, 1]), /** * @event itemcollapse * @inheritdoc Ext.data.TreeStore#nodecollapse */ collapse: me.createRelayer('itemcollapse', [0, 1]), /** * @event beforeitemexpand * @inheritdoc Ext.data.TreeStore#nodebeforeexpand */ beforeexpand: me.createRelayer('beforeitemexpand', [0, 1]), /** * @event beforeitemcollapse * @inheritdoc Ext.data.TreeStore#nodebeforecollapse */ beforecollapse: me.createRelayer('beforeitemcollapse', [0, 1]) }); // If rootVisible is false, we *might* need to expand the node. // If store is autoLoad, that will already have been kicked off. // If its already expanded, or in the process of loading, the TreeStore // has started that at the end of updateRoot if (!me.rootVisible && !store.autoLoad && !(root.isExpanded() || root.isLoading())) { // A hidden root must be expanded. // If it's loaded, set its expanded field (silently), and skip ahead to the onNodeExpand callback. if (root.isLoaded()) { root.data.expanded = true; store.onNodeExpand(root, root.childNodes); } // Root is not loaded; go through the expand mechanism to force a load else { root.data.expanded = false; root.expand(); } } // TreeStore must have an upward link to the TreePanel so that nodes can find their owning tree in NodeInterface.getOwnerTree store.ownerTree = me; if (!initial) { me.view.setRootNode(root, true); } }, // @private unbindStore: function() { var me = this, store = me.store; if (store) { me.callParent(); Ext.destroy(me.storeListeners, me.storeRelayers, me.rootRelayers); delete store.ownerTree; store.singleExpand = null; } }, /** * Sets root node of this tree. All trees *always* have a root node. It may be {@link #rootVisible hidden}. * * If the passed node has not already been loaded with child nodes, and has its expanded field set, this triggers the {@link #cfg-store} to load the child nodes of the root. * @param {Ext.data.TreeModel/Object} root * @return {Ext.data.TreeModel} The new root */ setRootNode: function() { return this.store.setRoot.apply(this.store, arguments); }, /** * Returns the root node for this tree. * @return {Ext.data.TreeModel} */ getRootNode: function() { return this.store.getRoot(); }, onRootChange: function(root) { this.view.setRootNode(root); }, /** * Retrieve an array of checked records. * @return {Ext.data.TreeModel[]} An array containing the checked records */ getChecked: function() { return this.getView().getChecked(); }, isItemChecked: function(rec) { return rec.get('checked'); }, /** * Expands a record that is loaded in the tree. * @param {Ext.data.Model} record The record to expand * @param {Boolean} [deep] True to expand nodes all the way down the tree hierarchy. * @param {Function} [callback] The function to run after the expand is completed * @param {Object} [scope] The scope of the callback function. */ expandNode: function(record, deep, callback, scope) { return this.getView().expand(record, deep, callback, scope || this); }, /** * Collapses a record that is loaded in the tree. * @param {Ext.data.Model} record The record to collapse * @param {Boolean} [deep] True to collapse nodes all the way up the tree hierarchy. * @param {Function} [callback] The function to run after the collapse is completed * @param {Object} [scope] The scope of the callback function. */ collapseNode: function(record, deep, callback, scope) { return this.getView().collapse(record, deep, callback, scope || this); }, /** * Expand all nodes * @param {Function} [callback] A function to execute when the expand finishes. * @param {Object} [scope] The scope of the callback function */ expandAll : function(callback, scope) { var me = this, root = me.getRootNode(), animate = me.enableAnimations; if (root) { if (!animate) { Ext.suspendLayouts(); } root.expand(true, callback, scope || me); if (!animate) { Ext.resumeLayouts(true); } } }, /** * Collapse all nodes * @param {Function} [callback] A function to execute when the collapse finishes. * @param {Object} [scope] The scope of the callback function */ collapseAll : function(callback, scope) { var me = this, root = me.getRootNode(), animate = me.enableAnimations, view = me.getView(); if (root) { if (!animate) { Ext.suspendLayouts(); } scope = scope || me; if (view.rootVisible) { root.collapse(true, callback, scope); } else { root.collapseChildren(true, callback, scope); } if (!animate) { Ext.resumeLayouts(true); } } }, /** * Expand the tree to the path of a particular node. * @param {String} path The path to expand. The path should include a leading separator. * @param {String} [field] The field to get the data from. Defaults to the model idProperty. * @param {String} [separator='/'] A separator to use. * @param {Function} [callback] A function to execute when the expand finishes. The callback will be called with * (success, lastNode) where success is if the expand was successful and lastNode is the last node that was expanded. * @param {Object} [scope] The scope of the callback function */ expandPath: function(path, field, separator, callback, scope) { var me = this, current = me.getRootNode(), index = 1, keys, expander; field = field || me.getRootNode().idProperty; separator = separator || '/'; if (Ext.isEmpty(path)) { Ext.callback(callback, scope || me, [false, null]); return; } keys = path.split(separator); if (current.get(field) != keys[1]) { // invalid root Ext.callback(callback, scope || me, [false, current]); return; } expander = function(){ if (++index === keys.length) { Ext.callback(callback, scope || me, [true, current]); return; } var node = current.findChild(field, keys[index]); if (!node) { Ext.callback(callback, scope || me, [false, current]); return; } current = node; current.expand(false, expander); }; current.expand(false, expander); }, /** * Expand the tree to the path of a particular node, then select it. * @param {String} path The path to select; A string of separated node IDs. * * The path should include a leading separator. eg `'/root/usermanagement/users'` * @param {String} [field] The field to get the data from. Defaults to the model idProperty. * @param {String} [separator='/'] A separator to use. * @param {Function} [callback] A function to execute when the select finishes. The callback will be called with * (bSuccess, oLastNode) where bSuccess is if the select was successful and oLastNode is the last node that was expanded. * @param {Object} [scope] The scope of the callback function */ selectPath: function(path, field, separator, callback, scope) { var me = this, root, keys, last; field = field || me.getRootNode().idProperty; separator = separator || '/'; keys = path.split(separator); last = keys.pop(); if (keys.length > 1) { me.expandPath(keys.join(separator), field, separator, function(success, node){ var lastNode = node; if (success && node) { node = node.findChild(field, last); if (node) { me.getSelectionModel().select(node); Ext.callback(callback, scope || me, [true, node]); return; } } Ext.callback(callback, scope || me, [false, lastNode]); }, me); } else { root = me.getRootNode(); if (root.getId() === last) { me.getSelectionModel().select(root); Ext.callback(callback, scope || me, [true, root]); } else { Ext.callback(callback, scope || me, [false, null]); } } }});