Upgrade to ExtJS 4.0.7 - Released 10/19/2011
[extjs.git] / examples / ux / RowExpander.js
1 /*
2
3 This file is part of Ext JS 4
4
5 Copyright (c) 2011 Sencha Inc
6
7 Contact:  http://www.sencha.com/contact
8
9 GNU General Public License Usage
10 This file may be used under the terms of the GNU General Public License version 3.0 as published by the Free Software Foundation and appearing in the file LICENSE included in the packaging of this file.  Please review the following information to ensure the GNU General Public License version 3.0 requirements will be met: http://www.gnu.org/copyleft/gpl.html.
11
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
13
14 */
15 // feature idea to enable Ajax loading and then the content
16 // cache would actually make sense. Should we dictate that they use
17 // data or support raw html as well?
18
19 /**
20  * @class Ext.ux.RowExpander
21  * @extends Ext.AbstractPlugin
22  * Plugin (ptype = 'rowexpander') that adds the ability to have a Column in a grid which enables
23  * a second row body which expands/contracts.  The expand/contract behavior is configurable to react
24  * on clicking of the column, double click of the row, and/or hitting enter while a row is selected.
25  *
26  * @ptype rowexpander
27  */
28 Ext.define('Ext.ux.RowExpander', {
29     extend: 'Ext.AbstractPlugin',
30
31     requires: [
32         'Ext.grid.feature.RowBody',
33         'Ext.grid.feature.RowWrap'
34     ],
35
36     alias: 'plugin.rowexpander',
37
38     rowBodyTpl: null,
39
40     /**
41      * @cfg {Boolean} expandOnEnter
42      * <tt>true</tt> to toggle selected row(s) between expanded/collapsed when the enter
43      * key is pressed (defaults to <tt>true</tt>).
44      */
45     expandOnEnter: true,
46
47     /**
48      * @cfg {Boolean} expandOnDblClick
49      * <tt>true</tt> to toggle a row between expanded/collapsed when double clicked
50      * (defaults to <tt>true</tt>).
51      */
52     expandOnDblClick: true,
53
54     /**
55      * @cfg {Boolean} selectRowOnExpand
56      * <tt>true</tt> to select a row when clicking on the expander icon
57      * (defaults to <tt>false</tt>).
58      */
59     selectRowOnExpand: false,
60
61     rowBodyTrSelector: '.x-grid-rowbody-tr',
62     rowBodyHiddenCls: 'x-grid-row-body-hidden',
63     rowCollapsedCls: 'x-grid-row-collapsed',
64
65
66
67     renderer: function(value, metadata, record, rowIdx, colIdx) {
68         if (colIdx === 0) {
69             metadata.tdCls = 'x-grid-td-expander';
70         }
71         return '<div class="x-grid-row-expander">&#160;</div>';
72     },
73
74     /**
75      * @event expandbody
76      * <b<Fired through the grid's View</b>
77      * @param {HtmlElement} rowNode The &lt;tr> element which owns the expanded row.
78      * @param {Ext.data.Model} record The record providing the data.
79      * @param {HtmlElement} expandRow The &lt;tr> element containing the expanded data.
80      */
81     /**
82      * @event collapsebody
83      * <b<Fired through the grid's View.</b>
84      * @param {HtmlElement} rowNode The &lt;tr> element which owns the expanded row.
85      * @param {Ext.data.Model} record The record providing the data.
86      * @param {HtmlElement} expandRow The &lt;tr> element containing the expanded data.
87      */
88
89     constructor: function() {
90         this.callParent(arguments);
91         var grid = this.getCmp();
92         this.recordsExpanded = {};
93         // <debug>
94         if (!this.rowBodyTpl) {
95             Ext.Error.raise("The 'rowBodyTpl' config is required and is not defined.");
96         }
97         // </debug>
98         // TODO: if XTemplate/Template receives a template as an arg, should
99         // just return it back!
100         var rowBodyTpl = Ext.create('Ext.XTemplate', this.rowBodyTpl),
101             features = [{
102                 ftype: 'rowbody',
103                 columnId: this.getHeaderId(),
104                 recordsExpanded: this.recordsExpanded,
105                 rowBodyHiddenCls: this.rowBodyHiddenCls,
106                 rowCollapsedCls: this.rowCollapsedCls,
107                 getAdditionalData: this.getRowBodyFeatureData,
108                 getRowBodyContents: function(data) {
109                     return rowBodyTpl.applyTemplate(data);
110                 }
111             },{
112                 ftype: 'rowwrap'
113             }];
114
115         if (grid.features) {
116             grid.features = features.concat(grid.features);
117         } else {
118             grid.features = features;
119         }
120
121         // NOTE: features have to be added before init (before Table.initComponent)
122     },
123
124     init: function(grid) {
125         this.callParent(arguments);
126
127         // Columns have to be added in init (after columns has been used to create the
128         // headerCt). Otherwise, shared column configs get corrupted, e.g., if put in the
129         // prototype.
130         grid.headerCt.insert(0, this.getHeaderConfig());
131         grid.on('render', this.bindView, this, {single: true});
132     },
133
134     getHeaderId: function() {
135         if (!this.headerId) {
136             this.headerId = Ext.id();
137         }
138         return this.headerId;
139     },
140
141     getRowBodyFeatureData: function(data, idx, record, orig) {
142         var o = Ext.grid.feature.RowBody.prototype.getAdditionalData.apply(this, arguments),
143             id = this.columnId;
144         o.rowBodyColspan = o.rowBodyColspan - 1;
145         o.rowBody = this.getRowBodyContents(data);
146         o.rowCls = this.recordsExpanded[record.internalId] ? '' : this.rowCollapsedCls;
147         o.rowBodyCls = this.recordsExpanded[record.internalId] ? '' : this.rowBodyHiddenCls;
148         o[id + '-tdAttr'] = ' valign="top" rowspan="2" ';
149         if (orig[id+'-tdAttr']) {
150             o[id+'-tdAttr'] += orig[id+'-tdAttr'];
151         }
152         return o;
153     },
154
155     bindView: function() {
156         var view = this.getCmp().getView(),
157             viewEl;
158
159         if (!view.rendered) {
160             view.on('render', this.bindView, this, {single: true});
161         } else {
162             viewEl = view.getEl();
163             if (this.expandOnEnter) {
164                 this.keyNav = Ext.create('Ext.KeyNav', viewEl, {
165                     'enter' : this.onEnter,
166                     scope: this
167                 });
168             }
169             if (this.expandOnDblClick) {
170                 view.on('itemdblclick', this.onDblClick, this);
171             }
172             this.view = view;
173         }
174     },
175
176     onEnter: function(e) {
177         var view = this.view,
178             ds   = view.store,
179             sm   = view.getSelectionModel(),
180             sels = sm.getSelection(),
181             ln   = sels.length,
182             i = 0,
183             rowIdx;
184
185         for (; i < ln; i++) {
186             rowIdx = ds.indexOf(sels[i]);
187             this.toggleRow(rowIdx);
188         }
189     },
190
191     toggleRow: function(rowIdx) {
192         var rowNode = this.view.getNode(rowIdx),
193             row = Ext.get(rowNode),
194             nextBd = Ext.get(row).down(this.rowBodyTrSelector),
195             record = this.view.getRecord(rowNode),
196             grid = this.getCmp();
197
198         if (row.hasCls(this.rowCollapsedCls)) {
199             row.removeCls(this.rowCollapsedCls);
200             nextBd.removeCls(this.rowBodyHiddenCls);
201             this.recordsExpanded[record.internalId] = true;
202             this.view.fireEvent('expandbody', rowNode, record, nextBd.dom);
203         } else {
204             row.addCls(this.rowCollapsedCls);
205             nextBd.addCls(this.rowBodyHiddenCls);
206             this.recordsExpanded[record.internalId] = false;
207             this.view.fireEvent('collapsebody', rowNode, record, nextBd.dom);
208         }
209
210
211         // If Grid is auto-heighting itself, then perform a component layhout to accommodate the new height
212         if (!grid.isFixedHeight()) {
213             grid.doComponentLayout();
214         }
215         this.view.up('gridpanel').invalidateScroller();
216     },
217
218     onDblClick: function(view, cell, rowIdx, cellIndex, e) {
219
220         this.toggleRow(rowIdx);
221     },
222
223     getHeaderConfig: function() {
224         var me                = this,
225             toggleRow         = Ext.Function.bind(me.toggleRow, me),
226             selectRowOnExpand = me.selectRowOnExpand;
227
228         return {
229             id: this.getHeaderId(),
230             width: 24,
231             sortable: false,
232             resizable: false,
233             draggable: false,
234             hideable: false,
235             menuDisabled: true,
236             cls: Ext.baseCSSPrefix + 'grid-header-special',
237             renderer: function(value, metadata) {
238                 metadata.tdCls = Ext.baseCSSPrefix + 'grid-cell-special';
239
240                 return '<div class="' + Ext.baseCSSPrefix + 'grid-row-expander">&#160;</div>';
241             },
242             processEvent: function(type, view, cell, recordIndex, cellIndex, e) {
243                 if (type == "mousedown" && e.getTarget('.x-grid-row-expander')) {
244                     var row = e.getTarget('.x-grid-row');
245                     toggleRow(row);
246                     return selectRowOnExpand;
247                 }
248             }
249         };
250     }
251 });
252