/**
* 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', {
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, 'syncSrc');
// eslint-disable-next-line vars-on-top
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);
}
}
}
},
init: function(grid) {
var me = this,
view = me.view,
dock = me.dock;
me.callParent([grid]);
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'],
/* eslint-disable indent, max-len */
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>'
],
/* eslint-enable indent, max-len */
scrollable: {
x: false,
y: false
},
hidden: !me.showSummaryRow,
itemId: 'summaryBar',
cls: [ me.dockedSummaryCls, me.dockedSummaryCls + '-' + dock ],
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,
record, newRowDom, oldRowDom, p;
if (!view.rendered) {
return;
}
record = me.createSummaryRecord(view);
newRowDom = Ext.fly(view.createRowElement(record, -1)).down(selector, true);
if (!newRowDom) {
return;
}
// 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(),
len = columns.length,
i, column, summaryEl, el, width, innerCt;
if (me.showSummaryRow && view.refreshCounter) {
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.scrollbar.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();
}
});