]>
Commit | Line | Data |
---|---|---|
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 | |
10 | Ext.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 ' ';\r | |
497 | }\r | |
498 | };\r | |
499 | }\r | |
500 | });\r |