Summary.js 17 KB

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