devexpress-reporting
Version:
DevExpress Reporting provides the capability to develop a reporting application to create and customize reports.
381 lines (380 loc) • 21.5 kB
JavaScript
/**
* DevExpress HTML/JS Reporting (designer\internal\_crossTabConverter.js)
* Version: 25.1.3
* Build date: Jun 26, 2025
* Copyright (c) 2012 - 2025 Developer Express Inc. ALL RIGHTS RESERVED
* License: https://www.devexpress.com/Support/EULAs/universal.xml
*/
import { findFirstItemMatchesCondition, formatUnicorn } from '@devexpress/analytics-core/analytics-internal';
import { getLocalization, ModelSerializer, UndoEngine } from '@devexpress/analytics-core/analytics-utils';
import * as ko from 'knockout';
import { CellKind, DataFieldLayout, TotalsPosition } from '../controls/crossTab/enums';
import { appearanceInfo } from '../controls/metadata/pivotgrid/pivotgridfield';
import { XRChartViewModel } from '../controls/xrChart';
import { CrossTabDataFieldModel } from '../controls/xrCrossTab';
import { XRCrossTabCellViewModel } from '../controls/xrCrossTabCell';
import { XRPivotGridViewModel } from '../controls/xrPivotgrid';
import { StylesHelper, stylesProperties } from '../helpers/_styleHelper';
import { ControlConverterService } from '../services/_controlConverterService';
import { controlsFactory } from '../utils/settings';
import { BaseConverter } from './_baseConverter';
export class CrossTabConverter extends BaseConverter {
constructor(_selectionProvider, _context) {
super();
this._selectionProvider = _selectionProvider;
this._context = _context;
this._detailLink = 'https://docs.devexpress.com/XtraReports/DevExpress.XtraReports.UI.XRPivotGrid#convert-to-the-cross-tab-control';
this.popupOptions.height = 316;
this.popupOptions.confirmMessage = getLocalization('The Cross Tab control does not support specific Pivot Grid functionality. Do you want to continue?', 'ASPxReportsStringId.ReportDesigner_ConvertPivotGridToCrossTab_Message_Confirmation');
this.popupOptions.infoMessage = getLocalization("You can use the 'Revert to Original Pivot Grid' action in the Cross Tab properties window to restore the Pivot Grid control.", 'ASPxReportsStringId.ReportDesigner_ConvertPivotGridToCrossTab_Message_Info');
this.popupOptions.linkText = getLocalization('[More infomation about Cross Tab conversion]', 'ASPxReportsStringId.ReportDesigner_ConvertPivotGridToCrossTab_Message_LinkText');
this.popupOptions.linkUrl = this._detailLink;
}
_applyChanges() {
this._warnings = [];
const pivotGrid = this._model;
this._undoEngine = UndoEngine.tryGetUndoEngine(pivotGrid);
this._undoEngine && this._undoEngine.start();
const crossTab = controlsFactory().createControl(controlsFactory().controlsMap['XRCrossTab'].defaultVal, pivotGrid.parentModel());
pivotGrid.getInfo().forEach(item => {
if (crossTab[item.propertyName] && ko.isObservable(pivotGrid[item.propertyName]))
crossTab[item.propertyName](pivotGrid[item.propertyName]());
});
crossTab.location.x(pivotGrid.location.x());
crossTab.location.y(pivotGrid.location.y());
crossTab.name('CrossTab_' + pivotGrid.name());
this._convertOptions(pivotGrid, crossTab);
const convertedFields = this._convertFields(pivotGrid, crossTab);
this._convertStyles(pivotGrid, crossTab, convertedFields);
this._applyVisibility(pivotGrid, crossTab);
this._applyText(pivotGrid, crossTab);
this._validateChartLinked(pivotGrid);
if (ko.isObservable(crossTab.size.width))
crossTab.size.width.valueHasMutated();
this._saveOriginalLayout(pivotGrid, crossTab);
}
_convertStyles(pivotGrid, crossTab, convertedFields) {
const root = crossTab.root;
const resultStyleGroups = [];
crossTab.cells().forEach(cell => {
const deafultStyle = root.findStyle(cell.styleName());
let cellStyle = StylesHelper.generateStyle(deafultStyle, crossTab);
this._applyStyles(pivotGrid, cell, cellStyle);
pivotGrid.fields().forEach(field => cell.field() == convertedFields[field.name()] && this._applyStyles(field, cell, cellStyle));
cellStyle = StylesHelper.styleEqualityComparer(deafultStyle, cellStyle) ? deafultStyle : cellStyle;
let styleAdded = false;
for (const group of resultStyleGroups) {
if (group.style == cellStyle) {
group.cells.push(cell);
styleAdded = true;
break;
}
}
!styleAdded && resultStyleGroups.push({ style: cellStyle, cells: [cell] });
});
crossTab.onDelete();
if (resultStyleGroups.length === 1) {
this._prepareGeneralStyle(resultStyleGroups, crossTab, root);
return;
}
if (resultStyleGroups.length === 3)
if (this._prepareStandardStyles(resultStyleGroups, crossTab, root))
return;
this._prepareNoStyles(resultStyleGroups, crossTab);
}
_prepareNoStyles(resultStyleGroups, crossTab) {
resultStyleGroups.forEach(group => group.cells.forEach(x => stylesProperties.forEach(element => {
const value = group.style[element] && group.style.isPropertyModified(element) && group.style[element]();
if (value) {
x[element](group.style[element]());
}
})));
crossTab['generalStyleName']('');
crossTab['headerAreaStyleName']('');
crossTab['dataAreaStyleName']('');
crossTab['totalAreaStyleName']('');
}
_prepareGeneralStyle(resultStyleGroups, crossTab, root) {
const generalStyle = resultStyleGroups[0].style;
generalStyle.name(crossTab['generalStyleName']());
root.styles.push(generalStyle);
crossTab['headerAreaStyleName']('');
crossTab['dataAreaStyleName']('');
crossTab['totalAreaStyleName']('');
}
_prepareStandardStyles(resultStyleGroups, crossTab, root) {
const headerStyle = findFirstItemMatchesCondition(resultStyleGroups, (item) => item.cells.every(x => XRCrossTabCellViewModel.cellKinds.Header.indexOf(x.kind()) !== -1)).style;
headerStyle.name(crossTab['headerAreaStyleName']());
if (!headerStyle)
return false;
const dataStyle = findFirstItemMatchesCondition(resultStyleGroups, (item) => item.cells.every(x => XRCrossTabCellViewModel.cellKinds.Data.indexOf(x.kind()) !== -1)).style;
dataStyle.name(crossTab['dataAreaStyleName']());
if (!dataStyle)
return false;
const totalStyle = findFirstItemMatchesCondition(resultStyleGroups, (item) => item.cells.every(x => XRCrossTabCellViewModel.cellKinds.Total.indexOf(x.kind()) !== -1)).style;
if (!totalStyle)
return false;
totalStyle.name(crossTab['totalAreaStyleName']());
crossTab['generalStyleName']('');
root.styles.push(headerStyle, dataStyle, totalStyle);
return true;
}
_applyStyles(source, cell, cellStyle) {
XRCrossTabCellViewModel.cellKinds.Header.indexOf(cell.kind()) == -1 && this._applyStyle(source.appearances.cellAppearance, cell);
if (cell.kind() == CellKind.Corner || cell.kind() == CellKind.DataHeader) {
this._applyStyle(source.appearances.fieldHeaderAppearance, cellStyle);
}
if (cell.kind() === CellKind.Data) {
this._applyStyle(source.appearances.cellAppearance, cellStyle);
}
else if (cell.isBindable()) {
this._applyStyle(source.appearances.fieldValueAppearance, cellStyle);
}
if (cell.kind() == CellKind.RowTotalHeader || cell.kind() == CellKind.ColumnTotalHeader) {
this._applyStyle(source.appearances.fieldValueTotalAppearance, cellStyle);
if (cell.rowLevel === undefined && cell.columnLevel == undefined) {
this._applyStyle(source.appearances.fieldValueGrandTotalAppearance, cellStyle);
}
}
if (cell.kind() == CellKind.GrandTotal && (cell.rowLevel === undefined || cell.columnLevel == undefined) ||
cell.kind() == CellKind.RowTotal && cell.rowLevel == undefined || cell.kind() == CellKind.ColumnTotal && cell.columnLevel == undefined) {
this._applyStyle(source.appearances.grandTotalCellAppearance, cellStyle);
}
else if (cell.kind() == CellKind.RowTotal || cell.kind() == CellKind.ColumnTotal) {
this._applyStyle(source.appearances.totalCellAppearance, cellStyle);
}
}
_applyStyle(style, target) {
appearanceInfo.forEach(element => {
const propertyName = element.propertyName;
if (propertyName == 'textOptions') {
let result = '';
const vertical = style.textOptions.textVerticalAlignment();
if (vertical == 'Center')
result += 'Middle';
else if (vertical != 'Default')
result += vertical;
const horizontal = style.textOptions.textHorizontalAlignment();
if (horizontal == 'Near')
result += 'Left';
if (horizontal == 'Far')
result += 'Right';
if (horizontal == 'Center')
result = 'Center';
result && target['textAlignment'](result);
}
else {
const value = style[propertyName] && style[propertyName]() && style[propertyName]();
if (value) {
target[propertyName](value);
}
}
});
}
_convertOptions(pivotGrid, crossTab) {
crossTab.layoutOptions.columnTotalsPosition(pivotGrid['optionsView'].columnTotalsLocation() === 'Far' ?
TotalsPosition[TotalsPosition.AfterData] : TotalsPosition[TotalsPosition.BeforeData]);
crossTab.layoutOptions.rowTotalsPosition(pivotGrid['optionsView'].rowTotalsLocation() === 'Far' ?
TotalsPosition[TotalsPosition.AfterData] : TotalsPosition[TotalsPosition.BeforeData]);
crossTab['printOptions'].printTotalsForSingleValues(pivotGrid['optionsView'].showTotalsForSingleValues());
crossTab['printOptions'].repeatColumnHeaders(pivotGrid['optionsPrint'].printColumnAreaOnEveryPage());
crossTab['printOptions'].repeatRowHeaders(pivotGrid['optionsPrint'].printRowAreaOnEveryPage());
crossTab.layoutOptions.dataFieldLayout(pivotGrid['optionsDataField'].area() === 'RowArea'
? DataFieldLayout[DataFieldLayout.InColumn] : DataFieldLayout[DataFieldLayout.InRow]);
}
_convertFields(pivotGrid, crossTab) {
const convertedFields = {};
const addField = (type, pivotField) => {
const field = crossTab.getInfo().filter(info => info.propertyName === type)[0].addHandler();
this._copyPropertiesToField(field, pivotField);
crossTab[type].push(field);
const dependentCell = crossTab.cells().filter(cell => cell.field() == field)[0];
dependentCell.size.width(pivotField['width']());
convertedFields[pivotField.name()] = field;
};
pivotGrid.fields().forEach(x => {
if (x['unboundExpression']()) {
this._warnings.push(formatUnicorn('Cannot convert Field {0} - unbound expression is not supported', x.name()));
}
else {
if (x.area() == 'ColumnArea')
addField('columnFields', x);
else if (x.area() == 'RowArea')
addField('rowFields', x);
else if (x.area() == 'DataArea')
addField('dataFields', x);
else
this._warnings.push(formatUnicorn('Cannot convert Field {0} - area is not supported', x.name()));
}
});
return convertedFields;
}
_copyPropertiesToField(crossTabField, pivotField) {
crossTabField.fieldName(pivotField.fieldName());
if (crossTabField instanceof CrossTabDataFieldModel) {
if (pivotField.summaryType() != 'Custom') {
crossTabField['summaryType'](pivotField.summaryType());
crossTabField['summaryDisplayType'](pivotField.summaryDisplayType());
}
else
this._warnings.push(formatUnicorn('Pivot Field {0} with a Custom Summary Type is not supported.', pivotField.fieldName()));
}
else {
if (pivotField.groupInterval() != 'Custom') {
crossTabField.crossTabGroupInterval(pivotField.groupInterval());
crossTabField.crossTabGroupIntervalNumericRange(pivotField.groupIntervalNumericRange());
}
else {
this._warnings.push(formatUnicorn('Pivot Field {0} with a Custom Group Interval is not supported.', pivotField.fieldName()));
}
crossTabField.sortOrder(pivotField.sortOrder() === 'Ascending' ? 'Ascending' : 'Descending');
if (pivotField.sortBySummaryInfo.summaryType() != 'Custom') {
crossTabField.crossTabSortBySummaryInfo.fieldName(pivotField.sortBySummaryInfo.fieldName());
crossTabField.crossTabSortBySummaryInfo.summaryType(pivotField.sortBySummaryInfo.summaryType());
}
else {
this._warnings.push(formatUnicorn('Pivot Field {0} with a Custom Summary Type is not supported.', pivotField.fieldName()));
}
}
}
_saveOriginalLayout(pivotGrid, crossTab) {
const originalDataSource = pivotGrid.dataSource();
const originalDataMember = pivotGrid.dataMember();
pivotGrid.dataSource(null);
pivotGrid.dataMember(null);
const layout = new ModelSerializer().serialize(pivotGrid);
ControlConverterService.getXmlStringFromJson(layout, result => {
const parentControls = pivotGrid.parentModel()['controls'];
crossTab.originalPivotGridLayout(result);
parentControls.splice(parentControls.indexOf(pivotGrid), 1, crossTab);
this._selectionProvider.focused(crossTab.surface);
this.popupOptions.visible(false);
this._warnings.forEach(x => console.warn(x));
pivotGrid.dataSource(originalDataSource);
pivotGrid.dataMember(originalDataMember);
this._undoEngine && this._undoEngine.end();
}, error => {
this._undoEngine && this._undoEngine.end();
this._undoEngine && this._undoEngine.undo();
});
}
_applyVisibility(pivotGrid, crossTab) {
crossTab.cells().forEach(cell => {
if ((!pivotGrid['optionsView'].showColumnTotals() && cell.kind() == CellKind.ColumnTotalHeader && cell.columnLevel !== undefined)
|| (!pivotGrid['optionsView'].showColumnGrandTotals() && cell.kind() == CellKind.ColumnTotalHeader && cell.columnLevel === undefined)) {
cell.columnVisible(false);
}
if ((!pivotGrid['optionsView'].showRowTotals() && cell.rowLevel !== undefined && cell.kind() == CellKind.RowTotalHeader)
|| (!pivotGrid['optionsView'].showRowGrandTotals() && cell.rowLevel === undefined && cell.kind() == CellKind.RowTotalHeader)) {
cell.rowVisible(false);
}
});
}
_applyText(pivotGrid, crossTab) {
crossTab.cells().forEach(cell => {
const cellKind = cell.kind();
let formatInfo;
const pivotGridFieldItem = this._findRelatedPivotGridItem(pivotGrid, cell.dataLevel, cell.columnLevel, cell.rowLevel);
if (cellKind == CellKind.ColumnHeader || cellKind == CellKind.RowHeader) {
formatInfo = pivotGridFieldItem.valueFormat;
}
else if ((cellKind === CellKind.ColumnTotalHeader && cell.columnLevel !== undefined)
|| (cellKind === CellKind.RowTotalHeader && cell.rowLevel !== undefined)) {
formatInfo = !!pivotGridFieldItem.totalValueFormat.formatString() ? pivotGridFieldItem.totalValueFormat : {
formatType: ko.observable('Numeric'),
formatString: ko.observable('{0} Total')
};
}
else {
if (pivotGridFieldItem) {
const isTotal = cellKind === CellKind.RowTotal || cellKind === CellKind.ColumnTotal;
const isGrandTotal = cellKind === CellKind.GrandTotal;
let cellFormat = !pivotGridFieldItem.cellFormat.formatString() ? null : pivotGridFieldItem.cellFormat;
const totalCellFormat = !pivotGridFieldItem.totalCellFormat.formatString() ? cellFormat : pivotGridFieldItem.totalCellFormat;
if (isGrandTotal)
cellFormat = !pivotGridFieldItem.grandTotalCellFormat.formatString() ? totalCellFormat : pivotGridFieldItem.grandTotalCellFormat;
if (isTotal)
cellFormat = totalCellFormat || cellFormat;
if (cellFormat == null || !cellFormat.formatString()) {
if (pivotGridFieldItem.summaryDisplayType().indexOf('Percent') === 0) {
cellFormat = {
formatType: ko.observable('Numeric'),
formatString: ko.observable('{0:p}')
};
}
else {
if (pivotGridFieldItem.summaryDisplayType().indexOf('Index') === 0)
cellFormat = {
formatType: ko.observable('Numeric'),
formatString: ko.observable('{0:f2}')
};
else if (pivotGridFieldItem.summaryType() !== 'Count' && pivotGridFieldItem.summaryType() !== 'CountDistinct' && pivotGridFieldItem.summaryDisplayType().indexOf('RankIn') === -1) {
const fieldType = pivotGridFieldItem.getFieldType();
if (['Float', 'Double', 'Decimal'].some(x => x === fieldType) && ((pivotGridFieldItem.groupInterval() !== 'Default' || !!pivotGridFieldItem.unboundExpression()) || pivotGridFieldItem.unboundType() === 'Decimal'))
cellFormat = {
formatType: ko.observable('Numeric'),
formatString: ko.observable('{0:c}')
};
}
}
}
formatInfo = cellFormat;
}
}
if (formatInfo && formatInfo.formatType() !== 'None' && formatInfo.formatString()) {
cell.textFormatString(formatInfo.formatString());
}
if (cellKind == CellKind.Corner || cellKind == CellKind.DataHeader) {
if (pivotGridFieldItem.caption && pivotGridFieldItem.caption())
cell.text(pivotGridFieldItem.caption());
}
});
}
_findRelatedPivotGridItem(pivotGrid, dataLevel, columnLevel, rowLevel) {
if (dataLevel > -1) {
return pivotGrid.fields().filter(x => x.area() === 'DataArea')[dataLevel];
}
else if (columnLevel > -1) {
return pivotGrid.fields().filter(x => x.area() === 'ColumnArea')[columnLevel];
}
else if (rowLevel > -1) {
return pivotGrid.fields().filter(x => x.area() === 'RowArea')[rowLevel];
}
}
_validateChartLinked(pivotGrid) {
const controlsHelper = this._context() && this._context().controlsHelper;
controlsHelper && controlsHelper.allControls().forEach(control => {
if (control instanceof XRChartViewModel) {
if (control.dataSource() == pivotGrid) {
this._warnings.push(formatUnicorn('Chart {0} uses PivotGrid as a DataSource, but the CrossTab can not be linked with Chart.', control.name()));
}
}
});
}
}
export class PivotGridConverter extends BaseConverter {
constructor(_selectionProvider) {
super();
this._selectionProvider = _selectionProvider;
this.popupOptions.confirmMessage = getLocalization('All changes made to the Cross Tab will be lost. ' +
'Do you want to continue?', 'ReportStringId.UD_Msg_RevertCrossTabToPivotGrid');
this.popupOptions.height = 240;
}
_applyChanges() {
const model = this._model;
if (!model.originalPivotGridLayout())
return;
ControlConverterService.getControlModelFromXmlString(model.originalPivotGridLayout(), result => {
const parentControls = model.parentModel()['controls'];
const pivotGrid = new XRPivotGridViewModel(result, model.parentModel());
pivotGrid.location.x(model.location.x());
pivotGrid.location.y(model.location.y());
pivotGrid.dataSource(model.dataSource());
pivotGrid.dataMember(model['dataMember']());
parentControls.splice(parentControls.indexOf(model), 1, pivotGrid);
model.onDelete();
this._selectionProvider.focused(pivotGrid.surface);
this.popupOptions.visible(false);
}, error => { });
}
}