/** * A small abstract class that contains the shared behaviour for any summary * calculations to be used in the grid. */ Ext.define('Ext.grid.feature.AbstractSummary', { extend: 'Ext.grid.feature.Feature', alias: 'feature.abstractsummary', summaryRowCls: Ext.baseCSSPrefix + 'grid-row-summary', summaryTableCls: Ext.plainTableCls + ' ' + Ext.baseCSSPrefix + 'grid-table', summaryRowSelector: '.' + Ext.baseCSSPrefix + 'grid-row-summary', // High priority rowTpl interceptor which sees summary rows early, and renders them correctly and then aborts the row rendering chain. // This will only see action when summary rows are being updated and Table.onUpdate->Table.bufferRender renders the individual updated sumary row. summaryRowTpl: { before: function(values, out) { // If a summary record comes through the rendering pipeline, render it simply, and return false from the // before method which aborts the tpl chain if (values.record.isSummary) { this.summaryFeature.outputSummaryRecord(values.record, values, out); return false; } }, priority: 1000 }, /** * @cfg {Boolean} * True to show the summary row. */ showSummaryRow: true, // Listen for store updates. Eg, from an Editor. init: function() { var me = this; me.view.summaryFeature = me; me.rowTpl = me.view.self.prototype.rowTpl; // Add a high priority interceptor which renders summary records simply // This will only see action ona bufferedRender situation where summary records are updated. me.view.addRowTpl(me.summaryRowTpl).summaryFeature = me; }, /** * Toggle whether or not to show the summary row. * @param {Boolean} visible True to show the summary row */ toggleSummaryRow: function(visible) { this.showSummaryRow = !!visible; }, outputSummaryRecord: function(summaryRecord, contextValues, out) { var view = contextValues.view, savedRowValues = view.rowValues, columns = contextValues.columns || view.headerCt.getVisibleGridColumns(), colCount = columns.length, i, column, // Set up a row rendering values object so that we can call the rowTpl directly to inject // the markup of a grid row into the output stream. values = { view: view, record: summaryRecord, rowStyle: '', rowClasses: [ this.summaryRowCls ], itemClasses: [], recordIndex: -1, rowId: view.getRowId(summaryRecord), columns: columns }; // Because we are using the regular row rendering pathway, temporarily swap out the renderer for the summaryRenderer for (i = 0; i < colCount; i++) { column = columns[i]; column.savedRenderer = column.renderer; if (column.summaryRenderer) { column.renderer = column.summaryRenderer; } else if (!column.summaryType) { column.renderer = Ext.emptyFn; } // Summary records may contain values based upon the column's ID if the column is not mapped from a field if (!column.dataIndex) { column.dataIndex = column.id; } } // Use the base template to render a summary row view.rowValues = values; view.self.prototype.rowTpl.applyOut(values, out); view.rowValues = savedRowValues; // Restore regular column renderers for (i = 0; i < colCount; i++) { column = columns[i]; column.renderer = column.savedRenderer; column.savedRenderer = null; } }, /** * Get the summary data for a field. * @private * @param {Ext.data.Store} store The store to get the data from * @param {String/Function} type The type of aggregation. If a function is specified it will * be passed to the stores aggregate function. * @param {String} field The field to aggregate on * @param {Boolean} group True to aggregate in grouped mode * @return {Number/String/Object} See the return type for the store functions. * if the group parameter is `true` An object is returned with a property named for each group who's * value is the summary value. */ getSummary: function(store, type, field, group){ var records = group.records; if (type) { if (Ext.isFunction(type)) { return store.getAggregate(type, null, records, [field]); } switch (type) { case 'count': return records.length; case 'min': return store.getMin(records, field); case 'max': return store.getMax(records, field); case 'sum': return store.getSum(records, field); case 'average': return store.getAverage(records, field); default: return ''; } } }, /** * Used by the Grouping Feature when {@link #showSummaryRow} is `true`. * * Generates group summary data for the whole store. * @private * @return {Object} An object hash keyed by group name containing summary records. */ generateSummaryData: function(){ var me = this, store = me.view.store, groups = store.groups.items, reader = store.proxy.reader, len = groups.length, groupField = me.getGroupField(), data = {}, lockingPartner = me.lockingPartner, i, group, record, root, summaryRows, hasRemote, convertedSummaryRow, remoteData; /** * @cfg {String} [remoteRoot=undefined] * The name of the property which contains the Array of summary objects. * It allows to use server-side calculated summaries. */ if (me.remoteRoot && reader.rawData) { hasRemote = true; remoteData = {}; // reset reader root and rebuild extractors to extract summaries data root = reader.root; reader.root = me.remoteRoot; reader.buildExtractors(true); summaryRows = reader.getRoot(reader.rawData)||[]; len = summaryRows.length; // Ensure the Reader has a data conversion function to convert a raw data row into a Record data hash if (!reader.convertRecordData) { reader.buildExtractors(); } for (i = 0; i < len; ++i) { convertedSummaryRow = {}; // Convert a raw data row into a Record's hash object using the Reader reader.convertRecordData(convertedSummaryRow, summaryRows[i]); remoteData[convertedSummaryRow[groupField]] = convertedSummaryRow; } // restore initial reader configuration reader.root = root; reader.buildExtractors(true); } for (i = 0; i < len; ++i) { group = groups[i]; // Something has changed or it doesn't exist, populate it if (hasRemote || group.isDirty() || !group.hasAggregate()) { if (hasRemote) { record = me.populateRemoteRecord(group, remoteData); } else { record = me.populateRecord(group); } // Clear the dirty state of the group if this is the only Summary, or this is the right hand (normal grid's) summary if (!lockingPartner || (me.view.ownerCt === me.view.ownerCt.ownerLockable.normalGrid)) { group.commit(); } } else { record = group.getAggregateRecord(); } data[group.key] = record; } return data; }, populateRemoteRecord: function(group, data) { var record = group.getAggregateRecord(true), groupData = data[group.key], field; record.beginEdit(); for (field in groupData) { if (groupData.hasOwnProperty(field)) { if (field !== record.idProperty) { record.set(field, groupData[field]); } } } record.endEdit(true); record.commit(true); return record; }, populateRecord: function(group){ var me = this, view = me.grid.ownerLockable ? me.grid.ownerLockable.view : me.view, store = me.view.store, record = group.getAggregateRecord(), // Use the full column set, regardless of locking columns = view.headerCt.getGridColumns(), len = columns.length, i, column, fieldName; record.beginEdit(); for (i = 0; i < len; ++i) { column = columns[i]; // Use the column id if there's no mapping, could be a calculated field fieldName = column.dataIndex || column.id; record.set(fieldName, me.getSummary(store, column.summaryType, fieldName, group)); } record.endEdit(true); record.commit(); return record; } });