]> git.proxmox.com Git - extjs.git/blame - extjs/classic/classic/src/grid/feature/Summary.js
add extjs 6.0.1 sources
[extjs.git] / extjs / classic / classic / src / grid / feature / Summary.js
CommitLineData
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
81Ext.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