UNPKG

devexpress-reporting

Version:

DevExpress Reporting provides the capability to develop a reporting application to create and customize reports.

368 lines (367 loc) 18.2 kB
/** * DevExpress HTML/JS Reporting (designer\controls\xrCrossTab.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 { SerializableModel } from '@devexpress/analytics-core/analytics-elements'; import { checkModelReady, createObservableArrayMapCollection, getFullPath, getLocalization, getUniqueNameForNamedObjectsArray } from '@devexpress/analytics-core/analytics-internal'; import { deserializeArray, ModelSerializer, UndoEngine } from '@devexpress/analytics-core/analytics-utils'; import { FilterStringOptions } from '@devexpress/analytics-core/analytics-widgets'; import { subscribeProperties } from '@devexpress/analytics-core/analytics-wizard-internal'; import * as ko from 'knockout'; import { controlsFactory } from '../utils/settings'; import { CellCreator } from './crossTab/cellCreator'; import { CrossTabColumnDefinitionsModel, CrossTabRowDefinitionsModel, DefinitionUpdater, findcells } from './crossTab/definitions'; import { DataFieldLayout, TotalsPosition } from './crossTab/enums'; import { crossTabCellHeight, crossTabCellWidth } from './metadata/crosstab/definitions'; import { crossTabDataFieldInfo, crossTabGroupFieldInfo } from './metadata/crosstab/fields'; import { crossTabLayoutOptionsInfo } from './metadata/crosstab/layoutOptions'; import { crossTabStyles, crossTabStylesDefaults } from './metadata/properties/style'; import { dpi as dpiMetadata } from './metadata/properties/metadata'; import { ControlParameter } from './properties/controlParameter'; import { StyleModel } from './properties/style'; import { XRControlSurface, XRControlViewModel } from './xrControl'; import { XRCellsurface, XRCrossTabCellViewModel } from './xrCrossTabCell'; export class XRCrossTabViewModel extends XRControlViewModel { _getCreator(type, serializer, name) { return (item) => new type(item || {}, this, serializer, name); } _getArray(type, model, name, serializer) { return deserializeArray(model[name], this._getCreator(type, serializer, this.getNames()[name[0].toLowerCase() + name.slice(1)])); } _initStyles(root) { if (root && root.styles) { crossTabStyles.forEach(style => { let styleName = this[style.propertyName](); if (!styleName) { const newStyle = new StyleModel(crossTabStylesDefaults[style.propertyName], this); styleName = getUniqueNameForNamedObjectsArray(root.styles(), newStyle.name()); newStyle.name(styleName); root.styles.push(newStyle); this[style.propertyName](styleName); } }); } } _calcSize(definition) { let result = 0; definition.forEach(element => { result += element[(element instanceof CrossTabColumnDefinitionsModel ? 'width' : 'height')](); }); return result; } constructor(model, parent, serializer = new ModelSerializer()) { super(model, parent, serializer); this.getPath = (propertyName) => this.dsHelperProvider() && this.dsHelperProvider().getDataSourcePath(this['dataSource']()); this.dependentStyles = []; this._cells = ko.observableArray([]); this._disposables.push(this.isModelReady = ko.computed(() => { return checkModelReady(this.root) && !this.update(); })); this.layoutOptions = new CrossTabLayoutOptionsModel(model['LayoutOptions'] || {}, this, serializer); this.rowFields = this._getArray(CrossTabFieldModel, model, 'RowFields', serializer); this.columnFields = this._getArray(CrossTabFieldModel, model, 'ColumnFields', serializer); this.dataFields = this._getArray(CrossTabDataFieldModel, model, 'DataFields', serializer); this._rowDefinitions = this._getArray(CrossTabRowDefinitionsModel, model, 'RowDefinitions', serializer); this._columnDefinitions = this._getArray(CrossTabColumnDefinitionsModel, model, 'ColumnDefinitions', serializer); this.cells = this._getArray(XRCrossTabCellViewModel, model, 'Cells', serializer); this.controlParameters = deserializeArray(model['Parameters'], (item) => { return new ControlParameter(item, serializer, this.root.dataSourceHelper, this.root.dataBindingsProvider); }); this.controlParameters().forEach(x => !x.parameter() && !x.dataSource() && x.dataSource(this.root['dataSource'] && this.root['dataSource']())); this.cellCreator = CellCreator.createInstance(this); const cells = this.cellCreator.create(); this._cells(this.applyCells(cells, true)); this._disposables.push(...subscribeProperties([this.rowFields, this.columnFields, this.dataFields] .concat(Object.keys(this.layoutOptions).map(x => this.layoutOptions[x])), () => { this.updateLayout(); })); this._disposables.push(ko.computed(() => { this.size.width(this._calcSize(this._columnDefinitions())); })); this._disposables.push(this.size.width.subscribe(newWidth => { const columnDefinitions = this._columnDefinitions.peek(); const currentWidth = this._calcSize(columnDefinitions); columnDefinitions.forEach(element => { element.width(element.width() + (newWidth - currentWidth) * (element.width() / currentWidth)); }); this._columnDefinitions.valueHasMutated(); })); this._disposables.push(ko.computed(() => { this.size.height(this._calcSize(this._rowDefinitions())); })); this._disposables.push(this.size.height.subscribe(newHeight => { const rowDefinitions = this._rowDefinitions.peek(); const currentHeight = this._calcSize(rowDefinitions); rowDefinitions.forEach(element => { element.height(element.height() + (newHeight - currentHeight) * (element.height() / currentHeight)); }); this._rowDefinitions.valueHasMutated(); })); this._disposables.push(this.dataSource.subscribe((val) => this['dataMember'](null))); this._disposables.push(this.controlParameters.subscribe((args) => { args.forEach((change) => { if (!change.value.parameterName()) { change.value.parameterName(getUniqueNameForNamedObjectsArray(this.controlParameters(), 'controlParameter')); } }); }, null, 'arrayChange')); const dataMember = ko.pureComputed(() => getFullPath(this.getPath('dataMember'), this['dataMember']())); const parameters = ko.computed(() => this.controlParameters()); const disabled = ko.pureComputed(() => !this.dataSource()); this.filterString = new FilterStringOptions(this._filterString, dataMember, disabled); this.filterString.helper.parameters = parameters; this._disposables.push(dataMember); this._disposables.push(disabled); this._disposables.push(parameters); } removeChild(cell) { if (cell.canRemove()) { this.removeField(cell.dataLevel, cell.columnLevel, cell.rowLevel); } } removeField(dataLevel, columnLevel, rowLevel) { if (dataLevel > -1) { this.dataFields.splice(dataLevel, 1); } else if (columnLevel > -1) { this.columnFields.splice(columnLevel, 1); } else if (rowLevel > -1) { this.rowFields.splice(rowLevel, 1); } } initialize() { this._disposables.push(this.parentModel.subscribe((model) => { model && this._initStyles(this.root); })); this.parentModel() && this._initStyles(this.root); } updateLayout() { if (this.update()) return; let undo = UndoEngine.tryGetUndoEngine(this); undo && undo.start(); this.update(true); this.definitionUpdater = new DefinitionUpdater(this); this.cellCreator = CellCreator.createInstance(this); const cells = this.cellCreator.create(); const modelCells = this.applyCells(cells); this._cells(modelCells); const columnWidth = crossTabCellWidth.defaultVal / dpiMetadata.defaultVal * this.dpi(); const rowHeight = crossTabCellHeight.defaultVal / dpiMetadata.defaultVal * this.dpi(); const defs = this.definitionUpdater.update(modelCells, columnWidth, rowHeight); const sumWidth = defs.columnDefs.reduce((acc, value) => { acc += value.width(); return acc; }, 0); const sumHeight = defs.rowDefs.reduce((acc, value) => { acc += value.height(); return acc; }, 0); const maxWidth = Math.min(sumWidth, Math.max(this.size.width(), this.parentModel().size.width() - this.location.x())); this._columnDefinitions(defs.columnDefs); this._rowDefinitions(defs.rowDefs); this.size.width(maxWidth); if (ko.isObservable(this.size.width)) this.size.width.valueHasMutated(); this.size.height(sumHeight); this.update(false); undo = UndoEngine.tryGetUndoEngine(this); undo && undo.end(); } getFields() { return [].concat(this.rowFields(), this.columnFields(), this.dataFields()); } getNames() { return { 'columnFields': getLocalization('Column Field', 'DevExpress.XtraReports.UI.CrossTab.CrossTabColumnField'), 'rowFields': getLocalization('Row Field', 'DevExpress.XtraReports.UI.CrossTab.CrossTabRowField'), 'dataFields': getLocalization('Data Field', 'DevExpress.XtraReports.UI.CrossTab.CrossTabDataField') }; } onDelete() { const root = this.root; this.dependentStyles = []; crossTabStyles.forEach(style => { if (root.stylesHelper()) { const targetStyle = root.stylesHelper().removeUnusedStyle(this[style.propertyName]()); targetStyle && this.dependentStyles.push(targetStyle); } else { const targetStyle = root.findStyle(this[style.propertyName]()); targetStyle && root.styles.remove(targetStyle) && this.dependentStyles.push(targetStyle); } }); } beforeDeserialize() { const info = this.getInfo(); this.getInfo = () => { info.forEach(item => { let type; if (item.propertyName === 'rowFields' || item.propertyName === 'columnFields') type = CrossTabFieldModel; else if (item.propertyName === 'dataFields') type = CrossTabDataFieldModel; else if (item.propertyName === 'controlParameters') item.addHandler = () => new ControlParameter({}, new ModelSerializer(), this.root.dataSourceHelper, this.root.dataBindingsProvider); if (type) item.addHandler = this._getCreator(type, new ModelSerializer(), this.getNames()[item.propertyName]); }); return info; }; } isPropertyDisabled(propertyName) { if (this.dataSource() === null) { return propertyName === 'dataMember' || propertyName === 'filterString'; } } applyCells(cellsInfo, initOnly = false) { const newCells = []; const oldCells = []; cellsInfo.forEach((cell, index) => { let currentCell; cell.dependentFields = [this.dataFields()[cell.dataLevel], this.rowFields()[cell.rowLevel], this.columnFields()[cell.columnLevel]]; if (initOnly) currentCell = findcells(this.cells(), cell._columnIndex(), cell._rowIndex())[0]; else currentCell = this.cells().filter(x => { if (x.kind() != cell.kind()) return false; for (let i = 0; i < x.dependentFields.length; i++) { if (x.dependentFields[i] != cell.dependentFields[i]) return false; } return true; })[0]; if (!currentCell) { currentCell = controlsFactory().createControl(controlsFactory().controlsMap['XRCrossTabCell'].defaultVal, this); newCells.push(currentCell); } else { currentCell.reset(); oldCells.push(currentCell); } this.applyCell(cell, currentCell); }); for (let i = this.cells().length - 1; i >= 0; i--) { if (oldCells.indexOf(this.cells()[i]) == -1) { this.cells()[i].dispose(); this.cells.splice(i, 1); } } newCells.forEach(x => this.cells.push(x)); return oldCells.concat(newCells); } applyCell(from, to) { const info = from.getInfo(); info.forEach(item => { to[item.propertyName](from[item.propertyName]()); }); ['dataLevel', 'rowLevel', 'columnLevel'].forEach(key => { if (from[key] != null) to[key] = from[key]; }); if (from.field) to.field(from.field()); else if (to.field && to.field()) to.field(null); to.kind(from.kind()); to.dependentFields = from.dependentFields; } insertNewField(collectionName, insertPosition, fieldName, dataFieldLayout) { const newField = this.getInfo().filter(x => x.propertyName === collectionName)[0].addHandler(); dataFieldLayout && this.layoutOptions.dataFieldLayout(DataFieldLayout[dataFieldLayout]); this[collectionName].splice(insertPosition, 0, newField); newField && newField.setFieldName(fieldName); } customizeExpressionCategories(tools, categories) { const fieldsCategory = categories.filter(item => item.content.name == 'dx-expressioneditor-fields')[0]; fieldsCategory && categories.splice(categories.indexOf(fieldsCategory), 1); } } export class CrossTabLayoutOptionsModel extends SerializableModel { constructor(model, parent, serializer) { super(model, serializer); this.parent = parent; } isPropertyDisabled(name) { switch (name) { case 'cornerHeaderDisplayMode': return this.parent.rowFields().length == 0 && this.parent.columnFields().length == 0; case 'dataFieldLayout': return this.parent.dataFields().length < 2; case 'columnTotalsPosition': return this.parent.columnFields().length < 1; case 'rowTotalsPosition': return this.parent.rowFields().length == 0 || this.hierarchicalRowLayout(); case 'columnTotalHeaderPosition': return this.parent.columnFields().length < 2; case 'rowTotalHeaderPosition': return this.parent.rowFields().length < 2; case 'hierarchicalRowLayout': return this.parent.rowFields().length < 2 || this.rowTotalsPosition() === TotalsPosition[TotalsPosition.BeforeData]; } } getInfo() { return crossTabLayoutOptionsInfo; } } export class CrossTabFieldModel extends SerializableModel { constructor(model, parent, serializer, name) { super(model, serializer); this.getPath = (propertyName) => getFullPath(this.parent.getPath('dataMember'), this.parent['dataMember']()); this.isPropertyDisabled = (propertyName) => propertyName == 'fieldName' && this.parent.dataSource() == null; this.parent = parent; this._disposables.push(this.name = ko.pureComputed(() => { if (this.fieldName()) return name + ' (' + this.fieldName() + ')'; return name; })); if (this.crossTabSortBySummaryInfo) this.crossTabSortBySummaryInfo.getPath = (propertyName) => this.getPath(propertyName); } setFieldName(fullPath) { const parts = fullPath.split('.'); const dsHelper = this.parent.dsHelperProvider && this.parent.dsHelperProvider(); if (dsHelper && parts.length >= 2) { let dataSource; if (this.parent.getPath('') === parts[0]) dataSource = this.parent['dataSource'](); else { dataSource = dsHelper && (dsHelper.findDataSourceInfoByID(parts[0]) || dsHelper.findDataSourceInfoByRef(parts[0])); dataSource && this.parent['dataSource'](dataSource.data); } dataSource && this.parent['dataMember'](parts.slice(1, -1).join('.')); } this.fieldName(parts.pop()); } getInfo() { return crossTabGroupFieldInfo; } } export class CrossTabDataFieldModel extends CrossTabFieldModel { getInfo() { return crossTabDataFieldInfo; } } export class XRCrossTabSurface extends XRControlSurface { constructor(control, context) { super(control, context); this.controls = ko.observableArray(); this._disposables.push(createObservableArrayMapCollection(control.cells, this.controls, (item) => new XRCellsurface(item, context))); this.selectiontemplate = 'dxrd-crosstab'; } selectLine(selection, cell, isMultiSelect, isRow) { if (!isMultiSelect) selection.initialize(this); const model = this.getControlModel(); const surface = cell.surface; let cells; if (isRow) cells = findcells(model.cells(), null, cell._rowIndex()); else cells = findcells(model.cells(), cell._columnIndex()); cells.forEach(cell => { if (isMultiSelect) { selection.selectionWithCtrl(cell.surface); selection.applySelection(); } else selection.selecting({ control: cell.surface, cancel: false }); }); if (!isMultiSelect) selection.swapFocusedItem(surface); } }