// 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? /** * @class Ext.ux.RowExpander * @extends Ext.AbstractPlugin * 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. * * @ptype rowexpander */ Ext.define('Ext.ux.RowExpander', { extend: 'Ext.AbstractPlugin', requires: [ 'Ext.grid.feature.RowBody', 'Ext.grid.feature.RowWrap' ], alias: 'plugin.rowexpander', rowBodyTpl: null, /** * @cfg {Boolean} expandOnEnter * true to toggle selected row(s) between expanded/collapsed when the enter * key is pressed (defaults to true). */ expandOnEnter: true, /** * @cfg {Boolean} expandOnDblClick * true to toggle a row between expanded/collapsed when double clicked * (defaults to true). */ expandOnDblClick: true, /** * @cfg {Boolean} selectRowOnExpand * true to select a row when clicking on the expander icon * (defaults to false). */ selectRowOnExpand: false, rowBodyTrSelector: '.x-grid-rowbody-tr', rowBodyHiddenCls: 'x-grid-row-body-hidden', rowCollapsedCls: 'x-grid-row-collapsed', renderer: function (value, metadata, record, rowIdx, colIdx) { if (colIdx === 0) { metadata.tdCls = 'x-grid-td-expander'; } return '
 
'; }, /** * @event expandbody * * @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 * * @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 () { this.callParent(arguments); var grid = this.getCmp(); this.recordsExpanded = {}; // if (!this.rowBodyTpl) { Ext.Error.raise("The 'rowBodyTpl' config is required and is not defined."); } // // TODO: if XTemplate/Template receives a template as an arg, should // just return it back! var rowBodyTpl = Ext.create('Ext.XTemplate', this.rowBodyTpl), features = [{ ftype: 'rowbody', columnId: this.getHeaderId(), recordsExpanded: this.recordsExpanded, rowBodyHiddenCls: this.rowBodyHiddenCls, rowCollapsedCls: this.rowCollapsedCls, getAdditionalData: this.getRowBodyFeatureData, getRowBodyContents: function (data) { return rowBodyTpl.applyTemplate(data); } }, { ftype: 'rowwrap' }]; if (grid.features) { grid.features = features.concat(grid.features); } else { grid.features = features; } // NOTE: features have to be added before init (before Table.initComponent) }, init: function (grid) { this.callParent(arguments); this.grid = grid; // 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. this.addExpander(); grid.on('render', this.bindView, this, { single: true }); grid.on('reconfigure', this.onReconfigure, this); }, onReconfigure: function () { this.addExpander(); }, addExpander: function () { this.grid.headerCt.insert(0, this.getHeaderConfig()); }, getHeaderId: function () { if (!this.headerId) { this.headerId = Ext.id(); } return this.headerId; }, getRowBodyFeatureData: function (data, idx, record, orig) { var o = Ext.grid.feature.RowBody.prototype.getAdditionalData.apply(this, arguments), id = this.columnId; o.rowBodyColspan = o.rowBodyColspan - 1; o.rowBody = this.getRowBodyContents(data); o.rowCls = this.recordsExpanded[record.internalId] ? '' : this.rowCollapsedCls; o.rowBodyCls = this.recordsExpanded[record.internalId] ? '' : this.rowBodyHiddenCls; o[id + '-tdAttr'] = ' valign="top" rowspan="2" '; if (orig[id + '-tdAttr']) { o[id + '-tdAttr'] += orig[id + '-tdAttr']; } return o; }, bindView: function () { var view = this.getCmp().getView(), viewEl; if (!view.rendered) { view.on('render', this.bindView, this, { single: true }); } else { viewEl = view.getEl(); if (this.expandOnEnter) { this.keyNav = Ext.create('Ext.KeyNav', viewEl, { 'enter': this.onEnter, scope: this }); } if (this.expandOnDblClick) { view.on('itemdblclick', this.onDblClick, this); } this.view = view; } }, onEnter: function (e) { var view = this.view, ds = view.store, sm = view.getSelectionModel(), sels = sm.getSelection(), ln = sels.length, i = 0, rowIdx; for (; i < ln; i++) { rowIdx = ds.indexOf(sels[i]); this.toggleRow(rowIdx); } }, toggleRow: function (rowIdx) { var view = this.view, rowNode = view.getNode(rowIdx), row = Ext.get(rowNode), nextBd = Ext.get(row).down(this.rowBodyTrSelector), record = view.getRecord(rowNode), grid = this.getCmp(); if (row.hasCls(this.rowCollapsedCls)) { row.removeCls(this.rowCollapsedCls); nextBd.removeCls(this.rowBodyHiddenCls); this.recordsExpanded[record.internalId] = true; view.refreshSize(); view.fireEvent('expandbody', rowNode, record, nextBd.dom); } else { row.addCls(this.rowCollapsedCls); nextBd.addCls(this.rowBodyHiddenCls); this.recordsExpanded[record.internalId] = false; view.refreshSize(); view.fireEvent('collapsebody', rowNode, record, nextBd.dom); } }, onDblClick: function (view, cell, rowIdx, cellIndex, e) { this.toggleRow(rowIdx); }, getHeaderConfig: function () { var me = this, toggleRow = Ext.Function.bind(me.toggleRow, me), selectRowOnExpand = me.selectRowOnExpand; return { id: this.getHeaderId(), width: 24, sortable: false, resizable: false, draggable: false, hideable: false, menuDisabled: true, cls: Ext.baseCSSPrefix + 'grid-header-special', renderer: function (value, metadata, record) { metadata.tdCls = Ext.baseCSSPrefix + 'grid-cell-special'; return '
 
'; }, processEvent: function (type, view, cell, recordIndex, cellIndex, e) { if (type == "mousedown" && e.getTarget('.x-grid-row-expander')) { var row = e.getTarget('.x-grid-row'); toggleRow(row); return selectRowOnExpand; } } }; } });