/**
* 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',
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: {
fn: function(out, values, parent) {
// If a summary record comes through the rendering pipeline,
// render it simply instead of proceeding through the tplchain
if (values.record.isSummary) {
this.summaryFeature.outputSummaryRecord(values.record, values, out, parent);
}
else {
this.nextTpl.applyOut(values, out, parent);
}
},
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;
me.readDataOptions = {
recordCreator: me.readSummaryRecord.bind(me)
};
// 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;
// Define on the instance to store info needed by summary renderers.
me.summaryData = {};
me.groupInfo = {};
// Cell widths in the summary table are set directly into the cells.
// There's no <colgroup><col>
// Some browsers use content box and some use border box when applying the style width
// of a TD
if (!me.summaryTableCls) {
me.summaryTableCls = Ext.baseCSSPrefix + 'grid-item';
}
// We have been configured with another class. Revert to building the selector
if (me.hasOwnProperty('summaryRowCls')) {
me.summaryRowSelector = '.' + me.summaryRowCls;
}
},
bindStore: function(grid, store) {
var me = this,
reader = store && store.getProxy() && store.getProxy().getReader();
me.readerListeners = Ext.destroy(me.readerListeners);
if (reader && (me.remoteRoot || reader.getSummaryRootProperty())) {
me.readerListeners = reader.on({
scope: me,
destroyable: true,
rawdata: me.onReaderRawData
});
}
},
onReaderRawData: function(data) {
// Invalidate potentially existing summaryRows to force recalculation
this.summaryRows = null;
this.readerRawData = data;
},
/**
* Toggle whether or not to show the summary row.
* @param {Boolean} visible True to show the summary row
* @param fromLockingPartner (private)
*/
toggleSummaryRow: function(visible, fromLockingPartner) {
var me = this,
prev = me.showSummaryRow,
doRefresh;
visible = visible != null ? !!visible : !me.showSummaryRow;
me.showSummaryRow = visible;
if (visible && visible !== prev) {
// If being shown, something may have changed while not visible, so
// force the summary records to recalculate
me.updateSummaryRow = true;
}
// If there is another side to be toggled, then toggle it (as long as we are not
// already being commanded from that other side);
// Then refresh the whole arrangement.
if (me.lockingPartner) {
if (!fromLockingPartner) {
me.lockingPartner.toggleSummaryRow(visible, true);
doRefresh = true;
}
}
else {
doRefresh = true;
}
if (doRefresh) {
me.grid.ownerGrid.getView().refreshView();
}
},
createRenderer: function(column, record) {
var me = this,
ownerGroup = record.ownerGroup,
summaryData = ownerGroup ? me.summaryData[ownerGroup] : me.summaryData,
// Use the column.getItemId() for columns without a dataIndex.
// The populateRecord method does the same.
dataIndex = column.dataIndex || column.getItemId();
return function(value, metaData) {
return column.summaryRenderer
? column.summaryRenderer(record.data[dataIndex], summaryData, dataIndex, metaData)
// For no summaryRenderer, return the field value in the Feature record.
: record.data[dataIndex];
};
},
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, this.summaryItemCls ],
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.summaryType || column.summaryRenderer) {
column.renderer = this.createRenderer(column, summaryRecord);
}
else {
column.renderer = Ext.emptyFn;
}
}
// Use the base template to render a summary row
view.rowValues = values;
view.self.prototype.rowTpl.applyOut(values, out, parent);
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 {Ext.util.Group} group The group from which to calculate the value
* @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 isGrouped = !!group,
item = isGrouped ? group : store;
if (type) {
if (Ext.isFunction(type)) {
if (isGrouped) {
return item.aggregate(field, type);
}
else {
return item.aggregate(type, null, false, [field]);
}
}
switch (type) {
case 'count':
return item.count();
case 'min':
return item.min(field);
case 'max':
return item.max(field);
case 'sum':
return item.sum(field);
case 'average':
return item.average(field);
default:
return '';
}
}
},
getRawData: function() {
var data = this.readerRawData;
if (data) {
return data;
}
// Synchronous Proxies such as Memory proxy will set keepRawData to true
// on their Reader instances, and may have been loaded before we were bound
// to the store. Or the Reader may have been configured with keepRawData: true
// manually.
// In these cases, the Reader should have rawData on the instance.
return this.view.getStore().getProxy().getReader().rawData;
},
generateSummaryData: function(groupField) {
var me = this,
summaryRows = me.summaryRows,
convertedSummaryRow = {},
remoteData = {},
remoteRoot = me.remoteRoot,
storeReader, reader, rawData, i, len, rows, store;
// Summary rows may have been cached by previous run
if (!summaryRows) {
rawData = me.getRawData();
if (!rawData) {
return;
}
// Construct a new Reader instance of the same type to avoid munging the one
// in the store:
store = me.view.store;
storeReader = store.getProxy().getReader();
reader = Ext.apply({
type: storeReader.type // not a config
}, storeReader.getConfig());
// Be sure to create a reader for the summaryModel since it may have different
// field types. For example, a count summary on a date column. That may not be
// be ideal UX, but there's no clear winner for a summary on a date...
reader.model = store.getModel().getSummaryModel();
// reset reader root and rebuild extractors to extract summaries data
if (remoteRoot) {
reader.summaryRootProperty = remoteRoot;
}
reader = Ext.Factory.reader(reader);
// At this point summaryRows is still raw data, e.g. XML node
summaryRows = reader.getSummaryRoot(rawData);
if (summaryRows) {
rows = reader.extractData(summaryRows, me.readDataOptions);
me.summaryRows = summaryRows = rows;
}
// By the next time the configuration may change
reader.destroy();
// We also no longer need the whole raw dataset
me.readerRawData = null;
}
if (summaryRows) {
for (i = 0, len = summaryRows.length; i < len; i++) {
convertedSummaryRow = summaryRows[i];
if (groupField) {
remoteData[convertedSummaryRow[groupField]] = convertedSummaryRow;
}
}
}
return groupField ? remoteData : convertedSummaryRow;
},
readSummaryRecord: function(data, model) {
var idProp = model.idProperty,
hasId = data[idProp] != null,
rec = new model(data);
// By instantiating the Model we get proper field types for the summary record,
// but since we only want the underlying data, we may need to remove the id it
// assigns.
data = rec.data;
if (!hasId) {
delete data[idProp];
}
return data;
},
setSummaryData: function(record, colId, summaryValue, groupName) {
var summaryData = this.summaryData;
if (groupName) {
if (!summaryData[groupName]) {
summaryData[groupName] = {};
}
summaryData[groupName][colId] = summaryValue;
}
else {
summaryData[colId] = summaryValue;
}
},
destroy: function() {
Ext.destroy(this.readerListeners);
this.readerRawData = this.summaryRows = null;
this.callParent();
}
});