]> git.proxmox.com Git - extjs.git/blame - extjs/classic/classic/src/grid/plugin/RowExpander.js
add extjs 6.0.1 sources
[extjs.git] / extjs / classic / classic / src / grid / plugin / RowExpander.js
CommitLineData
6527f429
DM
1// feature idea to enable Ajax loading and then the content\r
2// cache would actually make sense. Should we dictate that they use\r
3// data or support raw html as well?\r
4\r
5/**\r
6 * Plugin (ptype = 'rowexpander') that adds the ability to have a Column in a grid which enables\r
7 * a second row body which expands/contracts. The expand/contract behavior is configurable to react\r
8 * on clicking of the column, double click of the row, and/or hitting enter while a row is selected.\r
9 */\r
10Ext.define('Ext.grid.plugin.RowExpander', {\r
11 extend: 'Ext.plugin.Abstract',\r
12 lockableScope: 'normal',\r
13\r
14 requires: [\r
15 'Ext.grid.feature.RowBody'\r
16 ],\r
17\r
18 alias: 'plugin.rowexpander',\r
19\r
20 /**\r
21 * @cfg {Number} [columnWidth=24]\r
22 * The width of the row expander column which contains the [+]/[-] icons to toggle row expansion.\r
23 */\r
24 columnWidth: 24,\r
25\r
26 /**\r
27 * @cfg {Ext.XTemplate} rowBodyTpl\r
28 * An XTemplate which, when passed a record data object, produces HTML for the expanded row content.\r
29 *\r
30 * Note that if this plugin is applied to a lockable grid, the rowBodyTpl applies to the normal (unlocked) side.\r
31 * See {@link #lockedTpl}\r
32 *\r
33 */\r
34 rowBodyTpl: null,\r
35\r
36 /**\r
37 * @cfg {Ext.XTemplate} [lockedTpl]\r
38 * An XTemplate which, when passed a record data object, produces HTML for the expanded row content *on the locked side of a lockable grid*.\r
39 */\r
40 lockedTpl: null,\r
41\r
42 /**\r
43 * @cfg {Boolean} expandOnEnter\r
44 * This config is no longer supported. The Enter key initiated the grid's actinoable mode.\r
45 */\r
46\r
47 /**\r
48 * @cfg {Boolean} expandOnDblClick\r
49 * `true` to toggle a row between expanded/collapsed when double clicked\r
50 * (defaults to `true`).\r
51 */\r
52 expandOnDblClick: true,\r
53\r
54 /**\r
55 * @cfg {Boolean} selectRowOnExpand\r
56 * `true` to select a row when clicking on the expander icon\r
57 * (defaults to `false`).\r
58 */\r
59 selectRowOnExpand: false,\r
60\r
61 /**\r
62 * @cfg {Number}\r
63 * The width of the Row Expander column header\r
64 */\r
65 headerWidth: 24,\r
66 \r
67 /**\r
68 * @cfg {Boolean} [bodyBefore=false]\r
69 * Configure as `true` to put the row expander body *before* the data row.\r
70 * \r
71 */\r
72 bodyBefore: false,\r
73\r
74 rowBodyTrSelector: '.' + Ext.baseCSSPrefix + 'grid-rowbody-tr',\r
75 rowBodyHiddenCls: Ext.baseCSSPrefix + 'grid-row-body-hidden',\r
76 rowCollapsedCls: Ext.baseCSSPrefix + 'grid-row-collapsed',\r
77\r
78 addCollapsedCls: {\r
79 fn: function(out, values, parent) {\r
80 var me = this.rowExpander;\r
81 if (!me.recordsExpanded[values.record.internalId]) {\r
82 values.itemClasses.push(me.rowCollapsedCls);\r
83 }\r
84 this.nextTpl.applyOut(values, out, parent);\r
85 },\r
86\r
87 syncRowHeights: function(lockedItem, normalItem) {\r
88 this.rowExpander.syncRowHeights(lockedItem, normalItem);\r
89 },\r
90\r
91 // We need a high priority to get in ahead of the outerRowTpl\r
92 // so we can setup row data\r
93 priority: 20000\r
94 },\r
95\r
96 /**\r
97 * @event expandbody\r
98 * **Fired through the grid's View**\r
99 * @param {HTMLElement} rowNode The <tr> element which owns the expanded row.\r
100 * @param {Ext.data.Model} record The record providing the data.\r
101 * @param {HTMLElement} expandRow The <tr> element containing the expanded data.\r
102 */\r
103 /**\r
104 * @event collapsebody\r
105 * **Fired through the grid's View.**\r
106 * @param {HTMLElement} rowNode The <tr> element which owns the expanded row.\r
107 * @param {Ext.data.Model} record The record providing the data.\r
108 * @param {HTMLElement} expandRow The <tr> element containing the expanded data.\r
109 */\r
110\r
111 setCmp: function(grid) {\r
112 var me = this,\r
113 features;\r
114\r
115 me.callParent(arguments);\r
116\r
117 me.recordsExpanded = {};\r
118 // <debug>\r
119 if (!me.rowBodyTpl) {\r
120 Ext.raise("The 'rowBodyTpl' config is required and is not defined.");\r
121 }\r
122 // </debug>\r
123\r
124 me.rowBodyTpl = Ext.XTemplate.getTpl(me, 'rowBodyTpl');\r
125 features = me.getFeatureConfig(grid);\r
126\r
127 if (grid.features) {\r
128 grid.features = Ext.Array.push(features, grid.features);\r
129 } else {\r
130 grid.features = features;\r
131 }\r
132 // NOTE: features have to be added before init (before Table.initComponent)\r
133 },\r
134\r
135 /**\r
136 * @protected\r
137 * @return {Array} And array of Features or Feature config objects.\r
138 * Returns the array of Feature configurations needed to make the RowExpander work.\r
139 * May be overridden in a subclass to modify the returned array.\r
140 */\r
141 getFeatureConfig: function(grid) {\r
142 var me = this,\r
143 features = [],\r
144 featuresCfg = {\r
145 ftype: 'rowbody',\r
146 rowExpander: me,\r
147 bodyBefore: me.bodyBefore,\r
148 recordsExpanded: me.recordsExpanded,\r
149 rowBodyHiddenCls: me.rowBodyHiddenCls,\r
150 rowCollapsedCls: me.rowCollapsedCls,\r
151 setupRowData: me.getRowBodyFeatureData,\r
152 setup: me.setup\r
153 };\r
154\r
155 features.push(Ext.apply({\r
156 lockableScope: 'normal',\r
157 getRowBodyContents: me.getRowBodyContentsFn(me.rowBodyTpl)\r
158 }, featuresCfg));\r
159\r
160 // Locked side will need a copy to keep the two DOM structures symmetrical.\r
161 // A lockedTpl config is available to create content in locked side.\r
162 // The enableLocking flag is set early in Ext.panel.Table#initComponent if any columns are locked.\r
163 if (grid.enableLocking) {\r
164 features.push(Ext.apply({\r
165 lockableScope: 'locked',\r
166 getRowBodyContents: me.lockedTpl ? me.getRowBodyContentsFn(me.lockedTpl) : function() {return '';}\r
167 }, featuresCfg));\r
168 }\r
169\r
170 return features;\r
171 },\r
172 \r
173 getRowBodyContentsFn: function(rowBodyTpl) {\r
174 var me = this;\r
175 return function (rowValues) {\r
176 rowBodyTpl.owner = me;\r
177 return rowBodyTpl.applyTemplate(rowValues.record.getData());\r
178 };\r
179 },\r
180\r
181 init: function(grid) {\r
182 if (grid.lockable) {\r
183 grid = grid.normalGrid;\r
184 }\r
185\r
186 var me = this,\r
187 ownerLockable = grid.ownerLockable,\r
188 view, lockedView;\r
189\r
190 me.callParent(arguments);\r
191 me.grid = grid;\r
192 view = me.view = grid.getView();\r
193\r
194 // Bind to view for key and mouse events\r
195 // Add row processor which adds collapsed class\r
196 me.bindView(view);\r
197 view.addRowTpl(me.addCollapsedCls).rowExpander = me;\r
198\r
199 // If the owning grid is lockable, ensure the collapsed class is applied to the locked side by adding a row processor.\r
200 if (ownerLockable) {\r
201 me.addExpander(ownerLockable.lockedGrid.headerCt.items.getCount() ? ownerLockable.lockedGrid : grid);\r
202\r
203 // If our client grid part of a lockable grid, we listen to its ownerLockable's beforereconfigure\r
204 lockedView = ownerLockable.lockedGrid.getView();\r
205\r
206 // Bind to locked view for key and mouse events\r
207 // Add row processor which adds collapsed class\r
208 me.bindView(lockedView);\r
209 lockedView.addRowTpl(me.addCollapsedCls).rowExpander = me;\r
210 ownerLockable.mon(ownerLockable, {\r
211 processcolumns: me.onLockableProcessColumns,\r
212 lockcolumn: me.onColumnLock,\r
213 unlockcolumn: me.onColumnUnlock,\r
214 scope: me\r
215 });\r
216\r
217 // Process items added.\r
218 // It may be a re-rendering by the buffered renderer of an expanded item.\r
219 // If so, schedule a syncRowHeights call.\r
220 me.viewListeners = view.on({\r
221 itemadd: me.onItemAdd,\r
222 scope: me\r
223 });\r
224 } else {\r
225 me.addExpander(grid);\r
226 grid.on('beforereconfigure', me.beforeReconfigure, me);\r
227 }\r
228 },\r
229\r
230 onItemAdd: function(newRecords, startIndex, newItems) {\r
231 var me = this,\r
232 ownerLockable = me.grid.ownerLockable,\r
233 lockableSyncRowHeights = me.lockableSyncRowHeights || (me.lockableSyncRowHeights = Ext.Function.createAnimationFrame(ownerLockable.syncRowHeights, ownerLockable)),\r
234 len = newItems.length,\r
235 i;\r
236\r
237 // If any added items are expanded, we will need a syncRowHeights call on next animation frame\r
238 for (i = 0; i < len; i++) {\r
239 if (!Ext.fly(newItems[i]).hasCls(me.rowCollapsedCls)) {\r
240 lockableSyncRowHeights();\r
241 return;\r
242 }\r
243 }\r
244 },\r
245\r
246 beforeReconfigure: function(grid, store, columns, oldStore, oldColumns) {\r
247 var me = this;\r
248\r
249 if (me.viewListeners) {\r
250 me.viewListeners.destroy(); \r
251 }\r
252 \r
253 if (columns) {\r
254 me.expanderColumn = new Ext.grid.Column(me.getHeaderConfig()); \r
255 columns.unshift(me.expanderColumn);\r
256 }\r
257 \r
258 },\r
259\r
260 onLockableProcessColumns: function(lockable, lockedHeaders, normalHeaders) {\r
261 this.addExpander(lockedHeaders.length ? lockable.lockedGrid : lockable.normalGrid);\r
262 },\r
263\r
264 /**\r
265 * @private\r
266 * Inject the expander column into the correct grid.\r
267 *\r
268 * If we are expanding the normal side of a lockable grid, poke the column into the locked side if the locked side has columns\r
269 */\r
270 addExpander: function(expanderGrid) {\r
271 var me = this;\r
272\r
273 me.grid = expanderGrid;\r
274 me.expanderColumn = expanderGrid.headerCt.insert(0, me.getHeaderConfig());\r
275\r
276 // If a CheckboxModel, it must now put its checkbox in at position one because this\r
277 // cell always gets in at position zero, and spans 2 columns.\r
278 expanderGrid.getSelectionModel().injectCheckbox = 1;\r
279 },\r
280\r
281 getRowBodyFeatureData: function(record, idx, rowValues) {\r
282 var me = this;\r
283\r
284 me.self.prototype.setupRowData.apply(me, arguments);\r
285\r
286 rowValues.rowBody = me.getRowBodyContents(rowValues);\r
287 rowValues.rowBodyCls = me.recordsExpanded[record.internalId] ? '' : me.rowBodyHiddenCls;\r
288 },\r
289\r
290 setup: function(rows, rowValues){\r
291 var me = this,\r
292 lockable = me.grid.ownerLockable;\r
293\r
294 me.self.prototype.setup.apply(me, arguments);\r
295\r
296 // If we are lockable, and we are setting up the side which has the expander column, it is row spanning so we don't have to colspan it\r
297 if (lockable && Ext.Array.indexOf(me.grid.columnManager.getColumns(), me.rowExpander.expanderColumn) !== -1) {\r
298 rowValues.rowBodyColspan -= 1;\r
299 }\r
300 },\r
301\r
302 bindView: function(view) {\r
303 view.on('itemkeydown', this.onKeyDown, this);\r
304 if (this.expandOnDblClick) {\r
305 view.on('itemdblclick', this.onDblClick, this);\r
306 }\r
307 },\r
308\r
309 onKeyDown: function(view, record, row, rowIdx, e) {\r
310 var me = this,\r
311 key = e.getKey(),\r
312 pos = view.getNavigationModel().getPosition(),\r
313 isCollapsed;\r
314\r
315 if (pos) {\r
316 row = Ext.fly(row);\r
317 isCollapsed = row.hasCls(me.rowCollapsedCls);\r
318\r
319 // + key on collapsed or - key on expanded\r
320 if (((key === 107 || (key === 187 && e.shiftKey)) && isCollapsed) || ((key === 109 || key === 189) && !isCollapsed)) {\r
321 me.toggleRow(rowIdx, record);\r
322 }\r
323 }\r
324 },\r
325\r
326 onDblClick: function(view, record, row, rowIdx, e) {\r
327 this.toggleRow(rowIdx, record);\r
328 },\r
329\r
330 toggleRow: function(rowIdx, record) {\r
331 var me = this,\r
332 view = me.view,\r
333 bufferedRenderer = view.bufferedRenderer,\r
334 scroller = view.getScrollable(),\r
335 fireView = view,\r
336 rowNode = view.getNode(rowIdx),\r
337 normalRow = Ext.fly(rowNode),\r
338 lockedRow,\r
339 nextBd = normalRow.down(me.rowBodyTrSelector, true),\r
340 wasCollapsed = normalRow.hasCls(me.rowCollapsedCls),\r
341 addOrRemoveCls = wasCollapsed ? 'removeCls' : 'addCls',\r
342\r
343 // The expander column should be rowSpan="2" only when the expander is expanded\r
344 rowSpan = wasCollapsed ? 2 : 1,\r
345 ownerLockable = me.grid.ownerLockable,\r
346 expanderCell;\r
347\r
348 normalRow[addOrRemoveCls](me.rowCollapsedCls);\r
349 Ext.fly(nextBd)[addOrRemoveCls](me.rowBodyHiddenCls);\r
350 me.recordsExpanded[record.internalId] = wasCollapsed;\r
351\r
352 // Sync the collapsed/hidden classes on the locked side\r
353 if (me.grid.ownerLockable) {\r
354\r
355 // It's the top level grid's LockingView that does the firing when there's a lockable assembly involved.\r
356 fireView = ownerLockable.getView();\r
357\r
358 // Only attempt to toggle lockable side if it is visible.\r
359 if (ownerLockable.lockedGrid.isVisible()) {\r
360\r
361 view = ownerLockable.view.lockedGrid.view;\r
362\r
363 // Process the locked side.\r
364 lockedRow = Ext.fly(view.getNode(rowIdx));\r
365 // Just because the grid is locked, doesn't mean we'll necessarily have a locked row.\r
366 if (lockedRow) {\r
367 lockedRow[addOrRemoveCls](me.rowCollapsedCls);\r
368\r
369 // If there is a template for expander content in the locked side, toggle that side too\r
370 nextBd = lockedRow.down(me.rowBodyTrSelector, true);\r
371 Ext.fly(nextBd)[addOrRemoveCls](me.rowBodyHiddenCls);\r
372 }\r
373 }\r
374 }\r
375\r
376 // Here is where we set the rowSpan on this plugin's row expander cell.\r
377 // It should be rowSpan="2" only when the expander row is visible.\r
378 if (me.expanderColumn) {\r
379 expanderCell = Ext.fly(view.getRow(rowIdx)).down(me.expanderColumn.getCellSelector(), true);\r
380 if (expanderCell) {\r
381 expanderCell.rowSpan = rowSpan; \r
382 }\r
383 }\r
384 \r
385 fireView.fireEvent(wasCollapsed ? 'expandbody' : 'collapsebody', rowNode, record, nextBd);\r
386\r
387 // Layout needed of we are shrinkwrapping height, or there are locked/unlocked sides to sync\r
388 // Will sync the expander row heights between locked and normal sides\r
389 if (view.getSizeModel().height.shrinkWrap || ownerLockable) {\r
390 view.refreshSize(true);\r
391 }\r
392 // If we are using the touch scroller, ensure that the scroller knows about\r
393 // the correct scrollable range\r
394 if (scroller) {\r
395 if (bufferedRenderer) {\r
396 bufferedRenderer.refreshSize();\r
397 } else {\r
398 scroller.refresh(true);\r
399 }\r
400 } \r
401 },\r
402\r
403 // Called from TableLayout.finishedLayout\r
404 syncRowHeights: function(lockedItem, normalItem) {\r
405 var me = this,\r
406 lockedBd = Ext.fly(lockedItem).down(me.rowBodyTrSelector),\r
407 normalBd = Ext.fly(normalItem).down(me.rowBodyTrSelector),\r
408 lockedHeight,\r
409 normalHeight;\r
410\r
411 // If expanded, we have to ensure expander row heights are synched\r
412 if (normalBd.isVisible()) {\r
413\r
414 // If heights are different, expand the smallest one\r
415 if ((lockedHeight = lockedBd.getHeight()) !== (normalHeight = normalBd.getHeight())) {\r
416 if (lockedHeight > normalHeight) {\r
417 normalBd.setHeight(lockedHeight);\r
418 } else {\r
419 lockedBd.setHeight(normalHeight);\r
420 }\r
421 }\r
422 }\r
423 // When not expanded we do not control the heights\r
424 else {\r
425 lockedBd.dom.style.height = normalBd.dom.style.height = '';\r
426 }\r
427 },\r
428\r
429 onColumnUnlock: function(lockable, column) {\r
430 var me = this,\r
431 lockedColumns;\r
432 \r
433 lockable = me.grid.ownerLockable;\r
434 lockedColumns = lockable.lockedGrid.visibleColumnManager.getColumns();\r
435 \r
436 // User has unlocked all columns and left only the expander column in the locked side.\r
437 if (lockedColumns.length === 1) {\r
438 if (lockedColumns[0] === me.expanderColumn) {\r
439 lockable.unlock(me.expanderColumn);\r
440 me.grid = lockable.normalGrid;\r
441 } else {\r
442 lockable.lock(me.expanderColumn, 0);\r
443 }\r
444 }\r
445 },\r
446\r
447 onColumnLock: function(lockable, column) {\r
448 var me = this,\r
449 lockedColumns,\r
450 lockedGrid;\r
451 \r
452 lockable = me.grid.ownerLockable;\r
453 lockedColumns = lockable.lockedGrid.visibleColumnManager.getColumns();\r
454 \r
455 // User has unlocked all columns and left only the expander column in the locked side.\r
456 if (lockedColumns.length === 1) {\r
457 me.grid = lockedGrid = lockable.lockedGrid;\r
458 lockedGrid.headerCt.insert(0, me.expanderColumn);\r
459 }\r
460 },\r
461\r
462 getHeaderConfig: function() {\r
463 var me = this,\r
464 lockable = me.grid.ownerLockable;\r
465\r
466 return {\r
467 width: me.headerWidth,\r
468 ignoreExport: true,\r
469 lockable: false,\r
470 autoLock: true,\r
471 sortable: false,\r
472 resizable: false,\r
473 draggable: false,\r
474 hideable: false,\r
475 menuDisabled: true,\r
476 tdCls: Ext.baseCSSPrefix + 'grid-cell-special',\r
477 innerCls: Ext.baseCSSPrefix + 'grid-cell-inner-row-expander',\r
478 renderer: function() {\r
479 return '<div class="' + Ext.baseCSSPrefix + 'grid-row-expander" role="presentation" tabIndex="0"></div>';\r
480 },\r
481 processEvent: function(type, view, cell, rowIndex, cellIndex, e, record) {\r
482 if ((type === "click" && e.getTarget('.' + Ext.baseCSSPrefix + 'grid-row-expander')) || (type === 'keydown' && e.getKey() === e.SPACE)) {\r
483 me.toggleRow(rowIndex, record);\r
484 return me.selectRowOnExpand;\r
485 }\r
486 },\r
487\r
488 // This column always migrates to the locked side if the locked side is visible.\r
489 // It has to report this correctly so that editors can position things correctly\r
490 isLocked: function() {\r
491 return lockable && (lockable.lockedGrid.isVisible() || this.locked);\r
492 },\r
493\r
494 // In an editor, this shows nothing.\r
495 editRenderer: function() {\r
496 return '&#160;';\r
497 }\r
498 };\r
499 }\r
500});\r