|
|
@@ -0,0 +1,503 @@
|
|
|
+/**
|
|
|
+ * This feature is used to place a summary row at the bottom of the grid. If using a grouping,
|
|
|
+ * see {@link Ext.grid.feature.GroupingSummary}. There are 2 aspects to calculating the summaries,
|
|
|
+ * calculation and rendering.
|
|
|
+ *
|
|
|
+ * ## Calculation
|
|
|
+ * The summary value needs to be calculated for each column in the grid. This is controlled
|
|
|
+ * by the summaryType option specified on the column. There are several built in summary types,
|
|
|
+ * which can be specified as a string on the column configuration. These call underlying methods
|
|
|
+ * on the store:
|
|
|
+ *
|
|
|
+ * - {@link Ext.data.Store#count count}
|
|
|
+ * - {@link Ext.data.Store#sum sum}
|
|
|
+ * - {@link Ext.data.Store#min min}
|
|
|
+ * - {@link Ext.data.Store#max max}
|
|
|
+ * - {@link Ext.data.Store#average average}
|
|
|
+ *
|
|
|
+ * Alternatively, the summaryType can be a function definition. If this is the case,
|
|
|
+ * the function is called with an array of records to calculate the summary value.
|
|
|
+ *
|
|
|
+ * ## Rendering
|
|
|
+ * Similar to a column, the summary also supports a summaryRenderer function. This
|
|
|
+ * summaryRenderer is called before displaying a value. The function is optional, if
|
|
|
+ * not specified the default calculated value is shown. The summaryRenderer is called with:
|
|
|
+ *
|
|
|
+ * - value {Object} - The calculated value.
|
|
|
+ * - summaryData {Object} - Contains all raw summary values for the row.
|
|
|
+ * - field {String} - The name of the field we are calculating
|
|
|
+ * - metaData {Object} - A collection of metadata about the current cell; can be used or modified by the renderer.
|
|
|
+ *
|
|
|
+ * ## Example Usage
|
|
|
+ *
|
|
|
+ * @example
|
|
|
+ * Ext.define('TestResult', {
|
|
|
+ * extend: 'Ext.data.Model',
|
|
|
+ * fields: ['student', {
|
|
|
+ * name: 'mark',
|
|
|
+ * type: 'int'
|
|
|
+ * }]
|
|
|
+ * });
|
|
|
+ *
|
|
|
+ * Ext.create('Ext.grid.Panel', {
|
|
|
+ * width: 400,
|
|
|
+ * height: 200,
|
|
|
+ * title: 'Summary Test',
|
|
|
+ * style: 'padding: 20px',
|
|
|
+ * renderTo: document.body,
|
|
|
+ * features: [{
|
|
|
+ * ftype: 'summary'
|
|
|
+ * }],
|
|
|
+ * store: {
|
|
|
+ * model: 'TestResult',
|
|
|
+ * data: [{
|
|
|
+ * student: 'Student 1',
|
|
|
+ * mark: 84
|
|
|
+ * },{
|
|
|
+ * student: 'Student 2',
|
|
|
+ * mark: 72
|
|
|
+ * },{
|
|
|
+ * student: 'Student 3',
|
|
|
+ * mark: 96
|
|
|
+ * },{
|
|
|
+ * student: 'Student 4',
|
|
|
+ * mark: 68
|
|
|
+ * }]
|
|
|
+ * },
|
|
|
+ * columns: [{
|
|
|
+ * dataIndex: 'student',
|
|
|
+ * text: 'Name',
|
|
|
+ * summaryType: 'count',
|
|
|
+ * summaryRenderer: function(value, summaryData, dataIndex) {
|
|
|
+ * return Ext.String.format('{0} student{1}', value, value !== 1 ? 's' : '');
|
|
|
+ * }
|
|
|
+ * }, {
|
|
|
+ * dataIndex: 'mark',
|
|
|
+ * text: 'Mark',
|
|
|
+ * summaryType: 'average'
|
|
|
+ * }]
|
|
|
+ * });
|
|
|
+ */
|
|
|
+Ext.define('Ext.grid.feature.Summary', {
|
|
|
+
|
|
|
+ /* Begin Definitions */
|
|
|
+
|
|
|
+ extend: 'Ext.grid.feature.AbstractSummary',
|
|
|
+
|
|
|
+ alias: 'feature.summary',
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @cfg {String} dock
|
|
|
+ * Configure `'top'` or `'bottom'` top create a fixed summary row either above or below the scrollable table.
|
|
|
+ *
|
|
|
+ */
|
|
|
+ dock: undefined,
|
|
|
+
|
|
|
+ summaryItemCls: Ext.baseCSSPrefix + 'grid-row-summary-item',
|
|
|
+ dockedSummaryCls: Ext.baseCSSPrefix + 'docked-summary',
|
|
|
+
|
|
|
+ summaryRowCls: Ext.baseCSSPrefix + 'grid-row-summary ' + Ext.baseCSSPrefix + 'grid-row-total',
|
|
|
+ summaryRowSelector: '.' + Ext.baseCSSPrefix + 'grid-row-summary.' + Ext.baseCSSPrefix + 'grid-row-total',
|
|
|
+
|
|
|
+ panelBodyCls: Ext.baseCSSPrefix + 'summary-',
|
|
|
+
|
|
|
+ // turn off feature events.
|
|
|
+ hasFeatureEvent: false,
|
|
|
+
|
|
|
+ fullSummaryTpl: {
|
|
|
+ fn: function (out, values, parent) {
|
|
|
+ var me = this.summaryFeature,
|
|
|
+ record = me.summaryRecord,
|
|
|
+ view = values.view,
|
|
|
+ bufferedRenderer = view.bufferedRenderer;
|
|
|
+
|
|
|
+ this.nextTpl.applyOut(values, out, parent);
|
|
|
+
|
|
|
+ if (!me.disabled && me.showSummaryRow && !view.addingRows && view.store.isLast(values.record)) {
|
|
|
+ if (bufferedRenderer && !me.dock) {
|
|
|
+ bufferedRenderer.variableRowHeight = true;
|
|
|
+ }
|
|
|
+ me.outputSummaryRecord((record && record.isModel) ? record : me.createSummaryRecord(view), values, out, parent);
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ priority: 300,
|
|
|
+
|
|
|
+ beginRowSync: function (rowSync) {
|
|
|
+ rowSync.add('fullSummary', this.summaryFeature.summaryRowSelector);
|
|
|
+ },
|
|
|
+
|
|
|
+ syncContent: function (destRow, sourceRow, columnsToUpdate) {
|
|
|
+ destRow = Ext.fly(destRow, 'syncDest');
|
|
|
+ sourceRow = Ext.fly(sourceRow, 'sycSrc');
|
|
|
+ var summaryFeature = this.summaryFeature,
|
|
|
+ selector = summaryFeature.summaryRowSelector,
|
|
|
+ destSummaryRow = destRow.down(selector, true),
|
|
|
+ sourceSummaryRow = sourceRow.down(selector, true);
|
|
|
+
|
|
|
+ // Sync just the updated columns in the summary row.
|
|
|
+ if (destSummaryRow && sourceSummaryRow) {
|
|
|
+
|
|
|
+ // If we were passed a column set, only update those, otherwise do the entire row
|
|
|
+ if (columnsToUpdate) {
|
|
|
+ this.summaryFeature.view.updateColumns(destSummaryRow, sourceSummaryRow, columnsToUpdate);
|
|
|
+ } else {
|
|
|
+ Ext.fly(destSummaryRow).syncContent(sourceSummaryRow);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // override
|
|
|
+ fixed: undefined,
|
|
|
+ fixedSummaryCls: Ext.baseCSSPrefix + 'fixed-summary',
|
|
|
+
|
|
|
+ init: function (grid) {
|
|
|
+ var me = this,
|
|
|
+ view = me.view,
|
|
|
+ dock = me.dock,
|
|
|
+ fixed = me.fixed;
|
|
|
+
|
|
|
+ me.callParent([grid]);
|
|
|
+
|
|
|
+ // when 'fixed' is true, 'dock' must be a defined value, default 'bottom'
|
|
|
+ if(fixed) {
|
|
|
+ dock = me.dock = dock || 'bottom';
|
|
|
+ }
|
|
|
+ if (dock) {
|
|
|
+ grid.addBodyCls(me.panelBodyCls + dock);
|
|
|
+ grid.headerCt.on({
|
|
|
+ add: me.onStoreUpdate,
|
|
|
+ // we need to fire onStoreUpdate afterlayout for docked items
|
|
|
+ // to re-run the renderSummaryRow on show/hide columns.
|
|
|
+ afterlayout: me.onStoreUpdate,
|
|
|
+ remove: me.onStoreUpdate,
|
|
|
+ scope: me
|
|
|
+ });
|
|
|
+ grid.on({
|
|
|
+ beforerender: function () {
|
|
|
+ var tableCls = [me.summaryTableCls];
|
|
|
+ if (view.columnLines) {
|
|
|
+ tableCls[tableCls.length] = view.ownerCt.colLinesCls;
|
|
|
+ }
|
|
|
+ me.summaryBar = grid.addDocked({
|
|
|
+ childEls: ['innerCt', 'item'],
|
|
|
+ renderTpl: [
|
|
|
+ '<div id="{id}-innerCt" data-ref="innerCt" role="presentation">',
|
|
|
+ '<table id="{id}-item" data-ref="item" cellPadding="0" cellSpacing="0" class="' + tableCls.join(' ') + '">',
|
|
|
+ '<tr class="' + me.summaryRowCls + '"></tr>',
|
|
|
+ '</table>',
|
|
|
+ '</div>'
|
|
|
+ ],
|
|
|
+ scrollable: {
|
|
|
+ x: false,
|
|
|
+ y: false
|
|
|
+ },
|
|
|
+ hidden: !me.showSummaryRow,
|
|
|
+ itemId: 'summaryBar',
|
|
|
+ cls: [me.dockedSummaryCls, me.dockedSummaryCls + '-' + dock, (fixed ? me.fixedSummaryCls : '')],
|
|
|
+ xtype: 'component',
|
|
|
+ dock: dock,
|
|
|
+ weight: 10000000
|
|
|
+ })[0];
|
|
|
+ },
|
|
|
+ afterrender: function () {
|
|
|
+ grid.getView().getScrollable().addPartner(me.summaryBar.getScrollable(), 'x');
|
|
|
+ me.onStoreUpdate();
|
|
|
+ me.columnSizer = me.summaryBar.el;
|
|
|
+ },
|
|
|
+ single: true
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ if (grid.bufferedRenderer) {
|
|
|
+ me.wrapsItem = true;
|
|
|
+ view.addRowTpl(me.fullSummaryTpl).summaryFeature = me;
|
|
|
+ view.on('refresh', me.onViewRefresh, me);
|
|
|
+ } else {
|
|
|
+ me.wrapsItem = false;
|
|
|
+ me.view.addFooterFn(me.renderSummaryRow);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ grid.headerCt.on({
|
|
|
+ afterlayout: me.afterHeaderCtLayout,
|
|
|
+ scope: me
|
|
|
+ });
|
|
|
+
|
|
|
+ grid.ownerGrid.on({
|
|
|
+ beforereconfigure: me.onBeforeReconfigure,
|
|
|
+ columnmove: me.onStoreUpdate,
|
|
|
+ scope: me
|
|
|
+ });
|
|
|
+ me.bindStore(grid, grid.getStore());
|
|
|
+ },
|
|
|
+
|
|
|
+ onBeforeReconfigure: function (grid, store) {
|
|
|
+ this.summaryRecord = null;
|
|
|
+
|
|
|
+ if (store) {
|
|
|
+ this.bindStore(grid, store);
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ bindStore: function (grid, store) {
|
|
|
+ var me = this;
|
|
|
+
|
|
|
+ Ext.destroy(me.storeListeners);
|
|
|
+ me.storeListeners = store.on({
|
|
|
+ scope: me,
|
|
|
+ destroyable: true,
|
|
|
+ update: me.onStoreUpdate,
|
|
|
+ datachanged: me.onStoreUpdate
|
|
|
+ });
|
|
|
+
|
|
|
+ me.callParent([grid, store]);
|
|
|
+ },
|
|
|
+
|
|
|
+ renderSummaryRow: function (values, out, parent) {
|
|
|
+ var view = values.view,
|
|
|
+ me = view.findFeature('summary'),
|
|
|
+ record;
|
|
|
+
|
|
|
+ // If we get to here we won't be buffered
|
|
|
+ if (!me.disabled && me.showSummaryRow && !view.addingRows && !view.updatingRows) {
|
|
|
+ record = me.summaryRecord;
|
|
|
+
|
|
|
+ out.push('<table cellpadding="0" cellspacing="0" class="' + me.summaryItemCls + '" style="table-layout: fixed; width: 100%;">');
|
|
|
+ me.outputSummaryRecord((record && record.isModel) ? record : me.createSummaryRecord(view), values, out, parent);
|
|
|
+ out.push('</table>');
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ toggleSummaryRow: function (visible, fromLockingPartner) {
|
|
|
+ var me = this,
|
|
|
+ bar = me.summaryBar;
|
|
|
+
|
|
|
+ me.callParent([visible, fromLockingPartner]);
|
|
|
+ if (bar) {
|
|
|
+ bar.setVisible(me.showSummaryRow);
|
|
|
+ me.onViewScroll();
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ getSummaryBar: function () {
|
|
|
+ return this.summaryBar;
|
|
|
+ },
|
|
|
+
|
|
|
+ getSummaryRowPlaceholder: function (view) {
|
|
|
+ var placeholderCls = this.summaryItemCls,
|
|
|
+ nodeContainer, row;
|
|
|
+
|
|
|
+ nodeContainer = Ext.fly(view.getNodeContainer());
|
|
|
+
|
|
|
+ if (!nodeContainer) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ row = nodeContainer.down('.' + placeholderCls, true);
|
|
|
+
|
|
|
+ if (!row) {
|
|
|
+ row = nodeContainer.createChild({
|
|
|
+ tag: 'table',
|
|
|
+ cellpadding: 0,
|
|
|
+ cellspacing: 0,
|
|
|
+ cls: placeholderCls,
|
|
|
+ style: 'table-layout: fixed; width: 100%',
|
|
|
+ children: [{
|
|
|
+ tag: 'tbody' // Ensure tBodies property is present on the row
|
|
|
+ }]
|
|
|
+ }, false, true);
|
|
|
+ }
|
|
|
+
|
|
|
+ return row;
|
|
|
+ },
|
|
|
+
|
|
|
+ vetoEvent: function (record, row, rowIndex, e) {
|
|
|
+ return !e.getTarget(this.summaryRowSelector);
|
|
|
+ },
|
|
|
+
|
|
|
+ onViewScroll: function () {
|
|
|
+ this.summaryBar.setScrollX(this.view.getScrollX());
|
|
|
+ },
|
|
|
+
|
|
|
+ onViewRefresh: function (view) {
|
|
|
+ var me = this,
|
|
|
+ record, row;
|
|
|
+
|
|
|
+ // Only add this listener if in buffered mode, if there are no rows then
|
|
|
+ // we won't have anything rendered, so we need to push the row in here
|
|
|
+ if (!me.disabled && me.showSummaryRow && !view.all.getCount()) {
|
|
|
+ record = me.createSummaryRecord(view);
|
|
|
+ row = me.getSummaryRowPlaceholder(view);
|
|
|
+ row.tBodies[0].appendChild(view.createRowElement(record, -1).querySelector(me.summaryRowSelector));
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ createSummaryRecord: function (view) {
|
|
|
+ var me = this,
|
|
|
+ columns = view.headerCt.getGridColumns(),
|
|
|
+ remoteRoot = me.remoteRoot,
|
|
|
+ summaryRecord = me.summaryRecord || (me.summaryRecord = new Ext.data.Model({
|
|
|
+ id: view.id + '-summary-record'
|
|
|
+ })),
|
|
|
+ colCount = columns.length,
|
|
|
+ i, column,
|
|
|
+ dataIndex, summaryValue;
|
|
|
+
|
|
|
+ // Set the summary field values
|
|
|
+ summaryRecord.beginEdit();
|
|
|
+
|
|
|
+ if (remoteRoot) {
|
|
|
+ summaryValue = me.generateSummaryData();
|
|
|
+
|
|
|
+ if (summaryValue) {
|
|
|
+ summaryRecord.set(summaryValue);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ for (i = 0; i < colCount; i++) {
|
|
|
+ column = columns[i];
|
|
|
+
|
|
|
+ // In summary records, if there's no dataIndex, then the value in regular rows must come from a renderer.
|
|
|
+ // We set the data value in using the column ID.
|
|
|
+ dataIndex = column.dataIndex || column.getItemId();
|
|
|
+
|
|
|
+ // We need to capture this value because it could get overwritten when setting on the model if there
|
|
|
+ // is a convert() method on the model.
|
|
|
+ summaryValue = me.getSummary(view.store, column.summaryType, dataIndex);
|
|
|
+ summaryRecord.set(dataIndex, summaryValue);
|
|
|
+
|
|
|
+ // Capture the columnId:value for the summaryRenderer in the summaryData object.
|
|
|
+ me.setSummaryData(summaryRecord, column.getItemId(), summaryValue);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ summaryRecord.endEdit(true);
|
|
|
+ // It's not dirty
|
|
|
+ summaryRecord.commit(true);
|
|
|
+ summaryRecord.isSummary = true;
|
|
|
+
|
|
|
+ return summaryRecord;
|
|
|
+ },
|
|
|
+
|
|
|
+ onStoreUpdate: function () {
|
|
|
+ var me = this,
|
|
|
+ view = me.view,
|
|
|
+ selector = me.summaryRowSelector,
|
|
|
+ dock = me.dock,
|
|
|
+ fixed = me.fixed,
|
|
|
+ record, newRowDom, oldRowDom, newCellDoms, p;
|
|
|
+
|
|
|
+ if (!view.rendered) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ record = me.createSummaryRecord(view);
|
|
|
+ newRowDom = Ext.fly(view.createRowElement(record, -1)).down(selector, true);
|
|
|
+
|
|
|
+ if (!newRowDom) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if(fixed) {
|
|
|
+ newCellDoms = newRowDom.children;
|
|
|
+ for(let i = newCellDoms.length - 1; i >= 0 ; i--) {
|
|
|
+ let innerText = newCellDoms[i].innerText.trim();
|
|
|
+ if(innerText.length === 0) {
|
|
|
+ newRowDom.removeChild(newCellDoms[i])
|
|
|
+ }else {
|
|
|
+ newCellDoms[i].style.removeProperty('width');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Summary row is inside the docked summaryBar Component
|
|
|
+ if (dock) {
|
|
|
+ p = me.summaryBar.item.dom.firstChild;
|
|
|
+ oldRowDom = p.firstChild;
|
|
|
+
|
|
|
+ p.insertBefore(newRowDom, oldRowDom);
|
|
|
+ p.removeChild(oldRowDom);
|
|
|
+ }
|
|
|
+ // Summary row is a regular row in a THEAD inside the View.
|
|
|
+ // Downlinked through the summary record's ID
|
|
|
+ else {
|
|
|
+ oldRowDom = view.el.down(selector, true);
|
|
|
+ p = oldRowDom && oldRowDom.parentNode;
|
|
|
+
|
|
|
+ if (p) {
|
|
|
+ p.removeChild(oldRowDom);
|
|
|
+ }
|
|
|
+
|
|
|
+ // We're always inserting the new summary row into the last rendered row,
|
|
|
+ // unless no rows exist. In that case we will be appending to the special
|
|
|
+ // placeholder in the node container.
|
|
|
+ p = view.getRow(view.all.last());
|
|
|
+
|
|
|
+ if (p) {
|
|
|
+ p = p.parentElement;
|
|
|
+ }
|
|
|
+ // View might not have nodeContainer yet.
|
|
|
+ else {
|
|
|
+ p = me.getSummaryRowPlaceholder(view);
|
|
|
+ p = p && p.tBodies && p.tBodies[0];
|
|
|
+ }
|
|
|
+
|
|
|
+ if (p) {
|
|
|
+ p.appendChild(newRowDom);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // Synchronize column widths in the docked summary Component or the inline summary row
|
|
|
+ // depending on whether we are docked or not.
|
|
|
+ afterHeaderCtLayout: function (headerCt) {
|
|
|
+ var me = this,
|
|
|
+ view = me.view,
|
|
|
+ columns = view.getVisibleColumnManager().getColumns(),
|
|
|
+ column,
|
|
|
+ len = columns.length,
|
|
|
+ i,
|
|
|
+ summaryEl,
|
|
|
+ el, width, innerCt;
|
|
|
+
|
|
|
+ if (me.showSummaryRow && view.refreshCounter) {
|
|
|
+ if(me.fixed) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (me.dock) {
|
|
|
+ summaryEl = me.summaryBar.el;
|
|
|
+ width = headerCt.getTableWidth();
|
|
|
+ innerCt = me.summaryBar.innerCt;
|
|
|
+
|
|
|
+ // Stretch the innerCt of the summary bar upon headerCt layout
|
|
|
+ me.summaryBar.item.setWidth(width);
|
|
|
+
|
|
|
+ // headerCt's tooNarrow flag is set by its layout if the columns overflow.
|
|
|
+ // Must not measure+set in after layout phase, this is a write phase.
|
|
|
+ if (headerCt.tooNarrow) {
|
|
|
+ width += Ext.getScrollbarSize().width;
|
|
|
+ }
|
|
|
+ innerCt.setWidth(width);
|
|
|
+ } else {
|
|
|
+ summaryEl = Ext.fly(Ext.fly(view.getNodeContainer()).down('.' + me.summaryItemCls, true));
|
|
|
+ }
|
|
|
+
|
|
|
+ // If the layout was in response to a clearView, there'll be no summary element
|
|
|
+ if (summaryEl) {
|
|
|
+ for (i = 0; i < len; i++) {
|
|
|
+ column = columns[i];
|
|
|
+ el = summaryEl.down(view.getCellSelector(column), true);
|
|
|
+ if (el) {
|
|
|
+ Ext.fly(el).setWidth(column.width || (column.lastBox ? column.lastBox.width : 100));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ destroy: function () {
|
|
|
+ var me = this;
|
|
|
+ me.summaryRecord = me.storeListeners = Ext.destroy(me.storeListeners);
|
|
|
+ me.callParent();
|
|
|
+ }
|
|
|
+});
|