// feature idea to enable Ajax loading and then the content // cache would actually make sense. Should we dictate that they use // data or support raw html as well? /** * Plugin (ptype = 'rowexpander') that adds the ability to have a Column in a grid which enables * a second row body which expands/contracts. The expand/contract behavior is configurable to react * on clicking of the column, double click of the row, and/or hitting enter while a row is selected. */ Ext.define('Ext.ux.RowExpander', { extend: 'Ext.AbstractPlugin', lockableScope: 'normal', requires: [ 'Ext.grid.feature.RowBody', 'Ext.grid.feature.RowWrap' ], alias: 'plugin.rowexpander', rowBodyTpl: null, /** * @cfg {Boolean} expandOnEnter * <tt>true</tt> to toggle selected row(s) between expanded/collapsed when the enter * key is pressed (defaults to <tt>true</tt>). */ expandOnEnter: true, /** * @cfg {Boolean} expandOnDblClick * <tt>true</tt> to toggle a row between expanded/collapsed when double clicked * (defaults to <tt>true</tt>). */ expandOnDblClick: true, /** * @cfg {Boolean} selectRowOnExpand * <tt>true</tt> to select a row when clicking on the expander icon * (defaults to <tt>false</tt>). */ selectRowOnExpand: false, rowBodyTrSelector: '.x-grid-rowbody-tr', rowBodyHiddenCls: 'x-grid-row-body-hidden', rowCollapsedCls: 'x-grid-row-collapsed', /** * @event expandbody * <b<Fired through the grid's View</b> * @param {HTMLElement} rowNode The <tr> element which owns the expanded row. * @param {Ext.data.Model} record The record providing the data. * @param {HTMLElement} expandRow The <tr> element containing the expanded data. */ /** * @event collapsebody * <b<Fired through the grid's View.</b> * @param {HTMLElement} rowNode The <tr> element which owns the expanded row. * @param {Ext.data.Model} record The record providing the data. * @param {HTMLElement} expandRow The <tr> element containing the expanded data. */ constructor: function() { var me = this, grid, rowBodyTpl, features; me.callParent(arguments); grid = me.getCmp(); me.recordsExpanded = {}; // <debug> if (!me.rowBodyTpl) { Ext.Error.raise("The 'rowBodyTpl' config is required and is not defined."); } // </debug> me.rowBodyTpl = Ext.XTemplate.getTpl(me, 'rowBodyTpl'); rowBodyTpl = this.rowBodyTpl; features = [{ ftype: 'rowbody', lockableScope: 'normal', columnId: me.getHeaderId(), recordsExpanded: me.recordsExpanded, rowBodyHiddenCls: me.rowBodyHiddenCls, rowCollapsedCls: me.rowCollapsedCls, getAdditionalData: me.getRowBodyFeatureData, getRowBodyContents: function(data) { return rowBodyTpl.applyTemplate(data); } },{ ftype: 'rowwrap', lockableScope: 'normal' }, // In case the client grid is lockable (At this stage we cannot know; plugins are constructed early) // push a Feature into the locked side which sets up the initially collapsed row state correctly { ftype: 'feature', lockableScope: 'locked', getAdditionalData: function(data, idx, record, result) { if (!me.recordsExpanded[record.internalId]) { result.rowCls = (result.rowCls || '') + ' ' + me.rowCollapsedCls; } } }]; if (grid.features) { grid.features = Ext.Array.push(features, grid.features); } else { grid.features = features; } // NOTE: features have to be added before init (before Table.initComponent) }, init: function(grid) { var me = this, reconfigurable = grid; me.callParent(arguments); me.grid = grid; me.view = grid.getView(); // Columns have to be added in init (after columns has been used to create the headerCt). // Otherwise, shared column configs get corrupted, e.g., if put in the prototype. me.addExpander(); me.bindView(me.view); // If our client grid is the normal side of a lockable grid, we listen to its lockable owner's beforereconfigure // and also bind to the locked grid's view for dblclick and keydown events if (reconfigurable.ownerLockable) { reconfigurable = reconfigurable.ownerLockable; me.bindView(reconfigurable.lockedGrid.getView()); } reconfigurable.on('beforereconfigure', me.beforeReconfigure, me); }, beforeReconfigure: function(grid, store, columns, oldStore, oldColumns) { var expander = this.getHeaderConfig(); expander.locked = true; columns.unshift(expander); }, /** * @private * Inject the expander column into the correct grid. * * If we are expanding the normal side of a lockable grid, poke the column into the locked side */ addExpander: function() { var me = this, expanderGrid = me.grid, expanderHeader = me.getHeaderConfig(); // If this is the normal side of a lockable grid, find the other side. if (expanderGrid.ownerLockable) { expanderGrid = expanderGrid.ownerLockable.lockedGrid; expanderGrid.width += expanderHeader.width; } expanderGrid.headerCt.insert(0, expanderHeader); }, getHeaderId: function() { if (!this.headerId) { this.headerId = Ext.id(); } return this.headerId; }, getRowBodyFeatureData: function(data, idx, record, orig) { var me = this, o = me.self.prototype.getAdditionalData.apply(this, arguments), id = me.columnId; o.rowBodyColspan = o.rowBodyColspan - 1; o.rowBody = me.getRowBodyContents(data); o.rowCls = me.recordsExpanded[record.internalId] ? '' : me.rowCollapsedCls; o.rowBodyCls = me.recordsExpanded[record.internalId] ? '' : me.rowBodyHiddenCls; o[id + '-tdAttr'] = ' valign="top" rowspan="2" '; if (orig[id+'-tdAttr']) { o[id+'-tdAttr'] += orig[id+'-tdAttr']; } return o; }, bindView: function(view) { if (this.expandOnEnter) { view.on('itemkeydown', this.onKeyDown, this); } if (this.expandOnDblClick) { view.on('itemdblclick', this.onDblClick, this); } }, onKeyDown: function(view, record, row, rowIdx, e) { if (e.getKey() == e.ENTER) { var ds = view.store, sels = view.getSelectionModel().getSelection(), ln = sels.length, i = 0; for (; i < ln; i++) { rowIdx = ds.indexOf(sels[i]); this.toggleRow(rowIdx, sels[i]); } } }, onDblClick: function(view, record, row, rowIdx, e) { this.toggleRow(rowIdx, record); }, toggleRow: function(rowIdx, record) { var me = this, view = me.view, rowNode = view.getNode(rowIdx), row = Ext.fly(rowNode, '_rowExpander'), nextBd = row.down(me.rowBodyTrSelector, true), isCollapsed = row.hasCls(me.rowCollapsedCls), addOrRemoveCls = isCollapsed ? 'removeCls' : 'addCls', rowHeight; // Suspend layouts because of possible TWO views having their height change Ext.suspendLayouts(); row[addOrRemoveCls](me.rowCollapsedCls); Ext.fly(nextBd)[addOrRemoveCls](me.rowBodyHiddenCls); me.recordsExpanded[record.internalId] = isCollapsed; view.refreshSize(); view.fireEvent(isCollapsed ? 'expandbody' : 'collapsebody', row.dom, record, nextBd); // Sync the height and class of the row on the locked side if (me.grid.ownerLockable) { view = me.grid.ownerLockable.lockedGrid.view; rowHeight = row.getHeight(); row = Ext.fly(view.getNode(rowIdx), '_rowExpander'); row.setHeight(rowHeight); row[addOrRemoveCls](me.rowCollapsedCls); view.refreshSize(); } // Coalesce laying out due to view size changes Ext.resumeLayouts(true); }, getHeaderConfig: function() { var me = this; return { id: me.getHeaderId(), width: 24, lockable: false, sortable: false, resizable: false, draggable: false, hideable: false, menuDisabled: true, cls: Ext.baseCSSPrefix + 'grid-header-special', renderer: function(value, metadata) { metadata.tdCls = Ext.baseCSSPrefix + 'grid-cell-special'; return '<div class="' + Ext.baseCSSPrefix + 'grid-row-expander"> </div>'; }, processEvent: function(type, view, cell, rowIndex, cellIndex, e, record) { if (type == "mousedown" && e.getTarget('.x-grid-row-expander')) { me.toggleRow(rowIndex, record); return me.selectRowOnExpand; } } }; } });