]>
Commit | Line | Data |
---|---|---|
6527f429 DM |
1 | /**\r |
2 | * This feature is used to place a summary row at the bottom of the grid. If using a grouping,\r | |
3 | * see {@link Ext.grid.feature.GroupingSummary}. There are 2 aspects to calculating the summaries,\r | |
4 | * calculation and rendering.\r | |
5 | *\r | |
6 | * ## Calculation\r | |
7 | * The summary value needs to be calculated for each column in the grid. This is controlled\r | |
8 | * by the summaryType option specified on the column. There are several built in summary types,\r | |
9 | * which can be specified as a string on the column configuration. These call underlying methods\r | |
10 | * on the store:\r | |
11 | *\r | |
12 | * - {@link Ext.data.Store#count count}\r | |
13 | * - {@link Ext.data.Store#sum sum}\r | |
14 | * - {@link Ext.data.Store#min min}\r | |
15 | * - {@link Ext.data.Store#max max}\r | |
16 | * - {@link Ext.data.Store#average average}\r | |
17 | *\r | |
18 | * Alternatively, the summaryType can be a function definition. If this is the case,\r | |
19 | * the function is called with an array of records to calculate the summary value.\r | |
20 | *\r | |
21 | * ## Rendering\r | |
22 | * Similar to a column, the summary also supports a summaryRenderer function. This\r | |
23 | * summaryRenderer is called before displaying a value. The function is optional, if\r | |
24 | * not specified the default calculated value is shown. The summaryRenderer is called with:\r | |
25 | *\r | |
26 | * - value {Object} - The calculated value.\r | |
27 | * - summaryData {Object} - Contains all raw summary values for the row.\r | |
28 | * - field {String} - The name of the field we are calculating\r | |
29 | * - metaData {Object} - A collection of metadata about the current cell; can be used or modified by the renderer.\r | |
30 | *\r | |
31 | * ## Example Usage\r | |
32 | *\r | |
33 | * @example\r | |
34 | * Ext.define('TestResult', {\r | |
35 | * extend: 'Ext.data.Model',\r | |
36 | * fields: ['student', {\r | |
37 | * name: 'mark',\r | |
38 | * type: 'int'\r | |
39 | * }]\r | |
40 | * });\r | |
41 | *\r | |
42 | * Ext.create('Ext.grid.Panel', {\r | |
43 | * width: 400,\r | |
44 | * height: 200,\r | |
45 | * title: 'Summary Test',\r | |
46 | * style: 'padding: 20px',\r | |
47 | * renderTo: document.body,\r | |
48 | * features: [{\r | |
49 | * ftype: 'summary'\r | |
50 | * }],\r | |
51 | * store: {\r | |
52 | * model: 'TestResult',\r | |
53 | * data: [{\r | |
54 | * student: 'Student 1',\r | |
55 | * mark: 84\r | |
56 | * },{\r | |
57 | * student: 'Student 2',\r | |
58 | * mark: 72\r | |
59 | * },{\r | |
60 | * student: 'Student 3',\r | |
61 | * mark: 96\r | |
62 | * },{\r | |
63 | * student: 'Student 4',\r | |
64 | * mark: 68\r | |
65 | * }]\r | |
66 | * },\r | |
67 | * columns: [{\r | |
68 | * dataIndex: 'student',\r | |
69 | * text: 'Name',\r | |
70 | * summaryType: 'count',\r | |
71 | * summaryRenderer: function(value, summaryData, dataIndex) {\r | |
72 | * return Ext.String.format('{0} student{1}', value, value !== 1 ? 's' : '');\r | |
73 | * }\r | |
74 | * }, {\r | |
75 | * dataIndex: 'mark',\r | |
76 | * text: 'Mark',\r | |
77 | * summaryType: 'average'\r | |
78 | * }]\r | |
79 | * });\r | |
80 | */\r | |
81 | Ext.define('Ext.grid.feature.Summary', {\r | |
82 | \r | |
83 | /* Begin Definitions */\r | |
84 | \r | |
85 | extend: 'Ext.grid.feature.AbstractSummary',\r | |
86 | \r | |
87 | alias: 'feature.summary',\r | |
88 | \r | |
89 | /**\r | |
90 | * @cfg {String} dock\r | |
91 | * Configure `'top'` or `'bottom'` top create a fixed summary row either above or below the scrollable table.\r | |
92 | *\r | |
93 | */\r | |
94 | dock: undefined,\r | |
95 | \r | |
96 | dockedSummaryCls: Ext.baseCSSPrefix + 'docked-summary',\r | |
97 | \r | |
98 | panelBodyCls: Ext.baseCSSPrefix + 'summary-',\r | |
99 | \r | |
100 | // turn off feature events.\r | |
101 | hasFeatureEvent: false,\r | |
102 | \r | |
103 | fullSummaryTpl: [\r | |
104 | '{%',\r | |
105 | 'var me = this.summaryFeature,',\r | |
106 | ' record = me.summaryRecord,',\r | |
107 | ' view = values.view,',\r | |
108 | ' bufferedRenderer = view.bufferedRenderer;',\r | |
109 | \r | |
110 | 'this.nextTpl.applyOut(values, out, parent);',\r | |
111 | 'if (!me.disabled && me.showSummaryRow && view.store.isLast(values.record)) {',\r | |
112 | 'if (bufferedRenderer) {',\r | |
113 | ' bufferedRenderer.variableRowHeight = true;',\r | |
114 | '}',\r | |
115 | 'me.outputSummaryRecord((record && record.isModel) ? record : me.createSummaryRecord(view), values, out, parent);',\r | |
116 | '}',\r | |
117 | '%}', {\r | |
118 | priority: 300,\r | |
119 | \r | |
120 | beginRowSync: function (rowSync) {\r | |
121 | rowSync.add('fullSummary', this.summaryFeature.summaryRowSelector);\r | |
122 | },\r | |
123 | \r | |
124 | syncContent: function(destRow, sourceRow, columnsToUpdate) {\r | |
125 | destRow = Ext.fly(destRow, 'syncDest');\r | |
126 | sourceRow = Ext.fly(sourceRow, 'sycSrc');\r | |
127 | var owner = this.owner,\r | |
128 | selector = owner.summaryRowSelector,\r | |
129 | destSummaryRow = destRow.down(selector, true),\r | |
130 | sourceSummaryRow = sourceRow.down(selector, true);\r | |
131 | \r | |
132 | // Sync just the updated columns in the summary row.\r | |
133 | if (destSummaryRow && sourceSummaryRow) {\r | |
134 | \r | |
135 | // If we were passed a column set, only update those, otherwise do the entire row\r | |
136 | if (columnsToUpdate) {\r | |
137 | this.summaryFeature.view.updateColumns(destSummaryRow, sourceSummaryRow, columnsToUpdate);\r | |
138 | } else {\r | |
139 | Ext.fly(destSummaryRow).syncContent(sourceSummaryRow);\r | |
140 | }\r | |
141 | }\r | |
142 | }\r | |
143 | }\r | |
144 | ],\r | |
145 | \r | |
146 | init: function(grid) {\r | |
147 | var me = this,\r | |
148 | view = me.view,\r | |
149 | dock = me.dock;\r | |
150 | \r | |
151 | me.callParent(arguments);\r | |
152 | \r | |
153 | if (dock) {\r | |
154 | grid.addBodyCls(me.panelBodyCls + dock);\r | |
155 | grid.headerCt.on({\r | |
156 | add: me.onStoreUpdate,\r | |
157 | afterlayout: me.onStoreUpdate,\r | |
158 | scope: me\r | |
159 | });\r | |
160 | grid.on({\r | |
161 | beforerender: function() {\r | |
162 | var tableCls = [me.summaryTableCls];\r | |
163 | if (view.columnLines) {\r | |
164 | tableCls[tableCls.length] = view.ownerCt.colLinesCls;\r | |
165 | }\r | |
166 | me.summaryBar = grid.addDocked({\r | |
167 | childEls: ['innerCt', 'item'],\r | |
168 | renderTpl: [\r | |
169 | '<div id="{id}-innerCt" data-ref="innerCt" role="presentation">',\r | |
170 | '<table id="{id}-item" data-ref="item" cellPadding="0" cellSpacing="0" class="' + tableCls.join(' ') + '">',\r | |
171 | '<tr class="' + me.summaryRowCls + '"></tr>',\r | |
172 | '</table>',\r | |
173 | '</div>'\r | |
174 | ],\r | |
175 | scrollable: {\r | |
176 | x: false,\r | |
177 | y: false\r | |
178 | },\r | |
179 | hidden: !me.showSummaryRow,\r | |
180 | itemId: 'summaryBar',\r | |
181 | cls: [ me.dockedSummaryCls, me.dockedSummaryCls + '-' + dock ],\r | |
182 | xtype: 'component',\r | |
183 | dock: dock,\r | |
184 | weight: 10000000\r | |
185 | })[0];\r | |
186 | },\r | |
187 | afterrender: function() {\r | |
188 | grid.getView().getScrollable().addPartner(me.summaryBar.getScrollable());\r | |
189 | me.onStoreUpdate();\r | |
190 | },\r | |
191 | single: true\r | |
192 | });\r | |
193 | \r | |
194 | // Stretch the innerCt of the summary bar upon headerCt layout\r | |
195 | grid.headerCt.afterComponentLayout = Ext.Function.createSequence(grid.headerCt.afterComponentLayout, function() {\r | |
196 | var width = this.getTableWidth(),\r | |
197 | innerCt = me.summaryBar.innerCt;\r | |
198 | \r | |
199 | me.summaryBar.item.setWidth(width);\r | |
200 | \r | |
201 | // "this" is the HeaderContainer. Its tooNarrow flag is set by its layout if the columns overflow.\r | |
202 | // Must not measure+set in after layout phase, this is a write phase.\r | |
203 | if (this.tooNarrow) {\r | |
204 | width += Ext.getScrollbarSize().width;\r | |
205 | }\r | |
206 | innerCt.setWidth(width);\r | |
207 | });\r | |
208 | } else {\r | |
209 | if (grid.bufferedRenderer) {\r | |
210 | me.wrapsItem = true;\r | |
211 | view.addRowTpl(Ext.XTemplate.getTpl(me, 'fullSummaryTpl')).summaryFeature = me;\r | |
212 | view.on('refresh', me.onViewRefresh, me);\r | |
213 | } else {\r | |
214 | me.wrapsItem = false;\r | |
215 | me.view.addFooterFn(me.renderSummaryRow);\r | |
216 | }\r | |
217 | }\r | |
218 | \r | |
219 | grid.ownerGrid.on({\r | |
220 | beforereconfigure: me.onBeforeReconfigure,\r | |
221 | columnmove: me.onStoreUpdate,\r | |
222 | scope: me\r | |
223 | });\r | |
224 | me.bindStore(grid, grid.getStore());\r | |
225 | },\r | |
226 | \r | |
227 | onBeforeReconfigure: function(grid, store) {\r | |
228 | this.summaryRecord = null;\r | |
229 | \r | |
230 | if (store) {\r | |
231 | this.bindStore(grid, store);\r | |
232 | }\r | |
233 | },\r | |
234 | \r | |
235 | bindStore: function(grid, store) {\r | |
236 | var me = this;\r | |
237 | \r | |
238 | Ext.destroy(me.storeListeners);\r | |
239 | me.storeListeners = store.on({\r | |
240 | scope: me,\r | |
241 | destroyable: true,\r | |
242 | update: me.onStoreUpdate,\r | |
243 | datachanged: me.onStoreUpdate\r | |
244 | });\r | |
245 | \r | |
246 | me.callParent([grid, store]);\r | |
247 | },\r | |
248 | \r | |
249 | renderSummaryRow: function(values, out, parent) {\r | |
250 | var view = values.view,\r | |
251 | me = view.findFeature('summary'),\r | |
252 | record, rows;\r | |
253 | \r | |
254 | // If we get to here we won't be buffered\r | |
255 | if (!me.disabled && me.showSummaryRow) {\r | |
256 | record = me.summaryRecord;\r | |
257 | \r | |
258 | out.push('<table cellpadding="0" cellspacing="0" class="' + me.summaryItemCls + '" style="table-layout: fixed; width: 100%;">');\r | |
259 | me.outputSummaryRecord((record && record.isModel) ? record : me.createSummaryRecord(view), values, out, parent);\r | |
260 | out.push('</table>');\r | |
261 | }\r | |
262 | },\r | |
263 | \r | |
264 | toggleSummaryRow: function(visible /* private */, fromLockingPartner) {\r | |
265 | var me = this,\r | |
266 | bar = me.summaryBar;\r | |
267 | \r | |
268 | me.callParent([visible, fromLockingPartner]);\r | |
269 | if (bar) {\r | |
270 | bar.setVisible(me.showSummaryRow);\r | |
271 | me.onViewScroll();\r | |
272 | }\r | |
273 | },\r | |
274 | \r | |
275 | getSummaryBar: function() {\r | |
276 | return this.summaryBar;\r | |
277 | },\r | |
278 | \r | |
279 | vetoEvent: function(record, row, rowIndex, e) {\r | |
280 | return !e.getTarget(this.summaryRowSelector);\r | |
281 | },\r | |
282 | \r | |
283 | onViewScroll: function() {\r | |
284 | this.summaryBar.setScrollX(this.view.getScrollX());\r | |
285 | },\r | |
286 | \r | |
287 | onViewRefresh: function(view) {\r | |
288 | var me = this,\r | |
289 | record, row;\r | |
290 | \r | |
291 | // Only add this listener if in buffered mode, if there are no rows then\r | |
292 | // we won't have anything rendered, so we need to push the row in here\r | |
293 | if (!me.disabled && me.showSummaryRow && !view.all.getCount()) {\r | |
294 | record = me.createSummaryRecord(view);\r | |
295 | row = Ext.fly(view.getNodeContainer()).createChild({\r | |
296 | tag: 'table',\r | |
297 | cellpadding: 0,\r | |
298 | cellspacing: 0,\r | |
299 | cls: me.summaryItemCls,\r | |
300 | style: 'table-layout: fixed; width: 100%'\r | |
301 | }, false, true);\r | |
302 | row.appendChild(Ext.fly(view.createRowElement(record, -1)).down(me.summaryRowSelector, true));\r | |
303 | }\r | |
304 | },\r | |
305 | \r | |
306 | createSummaryRecord: function (view) {\r | |
307 | var me = this,\r | |
308 | columns = view.headerCt.getGridColumns(),\r | |
309 | remoteRoot = me.remoteRoot,\r | |
310 | summaryRecord = me.summaryRecord,\r | |
311 | colCount = columns.length, i, column,\r | |
312 | dataIndex, summaryValue, modelData;\r | |
313 | \r | |
314 | if (!summaryRecord) {\r | |
315 | modelData = {\r | |
316 | id: view.id + '-summary-record'\r | |
317 | };\r | |
318 | summaryRecord = me.summaryRecord = new Ext.data.Model(modelData);\r | |
319 | }\r | |
320 | \r | |
321 | // Set the summary field values\r | |
322 | summaryRecord.beginEdit();\r | |
323 | \r | |
324 | if (remoteRoot) {\r | |
325 | summaryValue = me.generateSummaryData();\r | |
326 | \r | |
327 | if (summaryValue) {\r | |
328 | summaryRecord.set(summaryValue);\r | |
329 | }\r | |
330 | }\r | |
331 | else {\r | |
332 | for (i = 0; i < colCount; i++) {\r | |
333 | column = columns[i];\r | |
334 | \r | |
335 | // In summary records, if there's no dataIndex, then the value in regular rows must come from a renderer.\r | |
336 | // We set the data value in using the column ID.\r | |
337 | dataIndex = column.dataIndex || column.getItemId();\r | |
338 | \r | |
339 | // We need to capture this value because it could get overwritten when setting on the model if there\r | |
340 | // is a convert() method on the model.\r | |
341 | summaryValue = me.getSummary(view.store, column.summaryType, dataIndex);\r | |
342 | summaryRecord.set(dataIndex, summaryValue);\r | |
343 | \r | |
344 | // Capture the columnId:value for the summaryRenderer in the summaryData object.\r | |
345 | me.setSummaryData(summaryRecord, column.getItemId(), summaryValue);\r | |
346 | }\r | |
347 | }\r | |
348 | \r | |
349 | summaryRecord.endEdit(true);\r | |
350 | // It's not dirty\r | |
351 | summaryRecord.commit(true);\r | |
352 | summaryRecord.isSummary = true;\r | |
353 | \r | |
354 | return summaryRecord;\r | |
355 | },\r | |
356 | \r | |
357 | onStoreUpdate: function() {\r | |
358 | var me = this,\r | |
359 | view = me.view,\r | |
360 | selector = me.summaryRowSelector,\r | |
361 | dock = me.dock,\r | |
362 | record, newRowDom, oldRowDom, p;\r | |
363 | \r | |
364 | if (!view.rendered) {\r | |
365 | return;\r | |
366 | }\r | |
367 | \r | |
368 | record = me.createSummaryRecord(view);\r | |
369 | newRowDom = Ext.fly(view.createRowElement(record, -1)).down(selector, true);\r | |
370 | \r | |
371 | if (!newRowDom) {\r | |
372 | return;\r | |
373 | }\r | |
374 | \r | |
375 | // Summary row is inside the docked summaryBar Component\r | |
376 | if (dock) {\r | |
377 | p = me.summaryBar.item.dom.firstChild;\r | |
378 | oldRowDom = p.firstChild;\r | |
379 | }\r | |
380 | // Summary row is a regular row in a THEAD inside the View.\r | |
381 | // Downlinked through the summary record's ID\r | |
382 | else {\r | |
383 | oldRowDom = me.view.el.down(selector, true);\r | |
384 | \r | |
385 | // If the old row doesn't exist, it means that the store update we are\r | |
386 | // reacting to is a remove of the last row. So we will be appending\r | |
387 | // to the node container.\r | |
388 | p = oldRowDom ? oldRowDom.parentNode : view.getNodeContainer();\r | |
389 | }\r | |
390 | \r | |
391 | if (p) {\r | |
392 | p.insertBefore(newRowDom, oldRowDom);\r | |
393 | if (oldRowDom) {\r | |
394 | p.removeChild(oldRowDom);\r | |
395 | }\r | |
396 | }\r | |
397 | // If docked, the updated row will need sizing because it's outside the View\r | |
398 | if (dock) {\r | |
399 | me.onColumnHeaderLayout();\r | |
400 | }\r | |
401 | },\r | |
402 | \r | |
403 | // Synchronize column widths in the docked summary Component\r | |
404 | onColumnHeaderLayout: function() {\r | |
405 | var view = this.view,\r | |
406 | columns = view.headerCt.getVisibleGridColumns(),\r | |
407 | column,\r | |
408 | len = columns.length, i,\r | |
409 | summaryEl = this.summaryBar.el,\r | |
410 | el;\r | |
411 | \r | |
412 | for (i = 0; i < len; i++) {\r | |
413 | column = columns[i];\r | |
414 | el = summaryEl.down(view.getCellSelector(column), true);\r | |
415 | if (el) {\r | |
416 | Ext.fly(el).setWidth(column.width || (column.lastBox ? column.lastBox.width : 100));\r | |
417 | }\r | |
418 | }\r | |
419 | },\r | |
420 | \r | |
421 | destroy: function() {\r | |
422 | var me = this;\r | |
423 | me.summaryRecord = me.storeListeners = Ext.destroy(me.storeListeners);\r | |
424 | me.callParent();\r | |
425 | }\r | |
426 | });\r | |
427 | \r |