Skip to content

Commit

Permalink
PivotGrid - export using exceljs library (#13183)
Browse files Browse the repository at this point in the history
  • Loading branch information
EugeniyKiyashko committed May 28, 2020
1 parent 0e97f5c commit c4c994d
Show file tree
Hide file tree
Showing 7 changed files with 2,332 additions and 3 deletions.
3 changes: 2 additions & 1 deletion js/excel_exporter.js
@@ -1,4 +1,5 @@
import { exportDataGrid } from './exporter/exceljs/export_data_grid';
import { exportPivotGrid } from './exporter/exceljs/export_pivot_grid';

/**
* @name excelExporter
Expand Down Expand Up @@ -38,4 +39,4 @@ import { exportDataGrid } from './exporter/exceljs/export_data_grid';
* @type any
*/

export { exportDataGrid };
export { exportDataGrid, exportPivotGrid };
240 changes: 240 additions & 0 deletions js/exporter/exceljs/export_pivot_grid.js
@@ -0,0 +1,240 @@
import { isDefined, isString, isObject } from '../../core/utils/type'; // '../../core/utils/type';
import excelFormatConverter from '../excel_format_converter';
import messageLocalization from '../../localization/message';
import { extend } from '../../core/utils/extend'; // '../../core/utils/extend';

// docs.microsoft.com/en-us/office/troubleshoot/excel/determine-column-widths - "Description of how column widths are determined in Excel"
const MAX_DIGIT_WIDTH_IN_PIXELS = 7; // Calibri font with 11pt size

// support.office.com/en-us/article/change-the-column-width-and-row-height-72f5e3cc-994d-43e8-ae58-9774a0905f46 - "Column.Max - 255"
// support.office.com/en-us/article/excel-specifications-and-limits-1672b34d-7043-467e-8e27-269d656771c3 - "Column width limit - 255 characters"
const MAX_EXCEL_COLUMN_WIDTH = 255;

function exportPivotGrid(options) {
if(!isDefined(options)) return;

const {
customizeCell,
component,
worksheet,
topLeftCell,
keepColumnWidths,
loadPanel
} = _getFullOptions(options);

const initialLoadPanelOptions = extend({}, component.option('loadPanel'));
if('animation' in component.option('loadPanel')) {
loadPanel.animation = null;
}

component.option('loadPanel', loadPanel);

const wrapText = !!component.option('wordWrapEnabled');

worksheet.properties.outlineProperties = {
summaryBelow: false,
summaryRight: false
};

const cellRange = {
from: { row: topLeftCell.row, column: topLeftCell.column },
to: { row: topLeftCell.row, column: topLeftCell.column }
};

const dataProvider = component.getDataProvider();

return new Promise((resolve) => {
dataProvider.ready().done(() => {
const columns = dataProvider.getColumns();
const dataRowsCount = dataProvider.getRowsCount();

if(keepColumnWidths) {
_setColumnsWidth(worksheet, columns, cellRange.from.column);
}

const mergedCells = [];
const mergeRanges = [];

for(let rowIndex = 0; rowIndex < dataRowsCount; rowIndex++) {
const row = worksheet.getRow(cellRange.from.row + rowIndex);

_exportRow(rowIndex, columns.length, row, cellRange.from.column, dataProvider, customizeCell, undefined, mergedCells, mergeRanges, /* customizeCell, headerRowCount */ wrapText);

if(rowIndex >= 1) {
cellRange.to.row++;
}
}

_mergeCells(worksheet, topLeftCell, mergeRanges);

cellRange.to.column += columns.length > 0 ? columns.length - 1 : 0;

const worksheetViewSettings = worksheet.views[0] || {};

if(component.option('rtlEnabled')) {
worksheetViewSettings.rightToLeft = true;
}

if(Object.keys(worksheetViewSettings).indexOf('state') === -1) {
extend(worksheetViewSettings, { state: 'frozen', xSplit: cellRange.from.column + dataProvider.getFrozenArea().x - 1, ySplit: cellRange.from.row + dataProvider.getFrozenArea().y - 1 });
}

if(Object.keys(worksheetViewSettings).length > 0) {
worksheet.views = [worksheetViewSettings];
}

resolve(cellRange);
}).always(() => {
component.option('loadPanel', initialLoadPanelOptions);
});
});
}

function _getFullOptions(options) {
const fullOptions = extend({}, options);
if(!isDefined(fullOptions.topLeftCell)) {
fullOptions.topLeftCell = { row: 1, column: 1 };
} else if(isString(fullOptions.topLeftCell)) {
const { row, col } = fullOptions.worksheet.getCell(fullOptions.topLeftCell);
fullOptions.topLeftCell = { row, column: col };
}
if(!isDefined(fullOptions.keepColumnWidths)) {
fullOptions.keepColumnWidths = true;
}
if(!isDefined(fullOptions.loadPanel)) {
fullOptions.loadPanel = {};
}
if(!isDefined(fullOptions.loadPanel.enabled)) {
fullOptions.loadPanel.enabled = true;
}
if(!isDefined(fullOptions.loadPanel.text)) {
fullOptions.loadPanel.text = messageLocalization.format('dxDataGrid-exporting');
}

return fullOptions;
}

function _exportRow(rowIndex, cellCount, row, startColumnIndex, dataProvider, customizeCell, headerRowCount, mergedCells, mergeRanges, wrapText) {
const styles = dataProvider.getStyles();

for(let cellIndex = 0; cellIndex < cellCount; cellIndex++) {
const cellData = dataProvider.getCellData(rowIndex, cellIndex, true);
const gridCell = cellData.cellSourceData;

const excelCell = row.getCell(startColumnIndex + cellIndex);
excelCell.value = cellData.value;


if(isDefined(excelCell.value)) {
const { alignment: horizontalAlignment, format, dataType } = styles[dataProvider.getStyleId(rowIndex, cellIndex)];

let numberFormat = _tryConvertToExcelNumberFormat(format, dataType);
if(isDefined(numberFormat)) {
numberFormat = numberFormat.replace(/&quot;/g, '');
} else if(isString(excelCell.value) && /^[@=+-]/.test(excelCell.value)) {
numberFormat = '@';
}

_setNumberFormat(excelCell, numberFormat);
_setAlignment(excelCell, wrapText, horizontalAlignment);
}

if(isDefined(customizeCell)) {
customizeCell({
excelCell: excelCell,
gridCell: gridCell
});
}

const mergeRange = _tryGetMergeRange(rowIndex, cellIndex, mergedCells, dataProvider);
if(isDefined(mergeRange)) {
mergeRanges.push(mergeRange);
}
}
}

function _setNumberFormat(excelCell, numberFormat) {
excelCell.numFmt = numberFormat;
}

function _tryConvertToExcelNumberFormat(format, dataType) {
const newFormat = _formatObjectConverter(format, dataType);
const currency = newFormat.currency;

format = newFormat.format;
dataType = newFormat.dataType;

return excelFormatConverter.convertFormat(format, newFormat.precision, dataType, currency);
}

function _formatObjectConverter(format, dataType) {
const result = {
format: format,
precision: format && format.precision,
dataType: dataType
};

if(isObject(format)) {
return extend(result, format, {
format: format.formatter || format.type,
currency: format.currency
});
}

return result;
}

function _setAlignment(excelCell, wrapText, horizontalAlignment) {
excelCell.alignment = excelCell.alignment || {};

if(isDefined(wrapText)) {
excelCell.alignment.wrapText = wrapText;
}
if(isDefined(horizontalAlignment)) {
excelCell.alignment.horizontal = horizontalAlignment;
}

excelCell.alignment.vertical = 'top';
}

function _setColumnsWidth(worksheet, columns, startColumnIndex) {
if(!isDefined(columns)) {
return;
}
for(let i = 0; i < columns.length; i++) {
const columnWidth = columns[i].width;

if((typeof columnWidth === 'number') && isFinite(columnWidth)) {
worksheet.getColumn(startColumnIndex + i).width =
Math.min(MAX_EXCEL_COLUMN_WIDTH, Math.floor(columnWidth / MAX_DIGIT_WIDTH_IN_PIXELS * 100) / 100);
}
}
}

function _tryGetMergeRange(rowIndex, cellIndex, mergedCells, dataProvider) {
if(!mergedCells[rowIndex] || !mergedCells[rowIndex][cellIndex]) {
const cellMerge = dataProvider.getCellMerging(rowIndex, cellIndex);
if(cellMerge.colspan || cellMerge.rowspan) {
for(let i = rowIndex; i <= rowIndex + cellMerge.rowspan || 0; i++) {
for(let j = cellIndex; j <= cellIndex + cellMerge.colspan || 0; j++) {
if(!mergedCells[i]) {
mergedCells[i] = [];
}
mergedCells[i][j] = true;
}
}
return {
start: { row: rowIndex, column: cellIndex },
end: { row: rowIndex + (cellMerge.rowspan || 0), column: cellIndex + (cellMerge.colspan || 0) }
};
}
}
}

function _mergeCells(worksheet, topLeftCell, mergeRanges) {
mergeRanges.forEach((mergeRange) => {
worksheet.mergeCells(mergeRange.start.row + topLeftCell.row, mergeRange.start.column + topLeftCell.column, mergeRange.end.row + topLeftCell.row, mergeRange.end.column + topLeftCell.column);
});
}

export { exportPivotGrid, _getFullOptions };
@@ -0,0 +1 @@
import './exceljsParts/exceljs.pivotGrid.tests.js';
@@ -1,11 +1,12 @@
import $ from 'jquery';
import ExcelJS from 'exceljs';
import ExcelJSTestHelper from './ExcelJSTestHelper.js';
import { exportDataGrid } from 'excel_exporter';
import { exportDataGrid, exportPivotGrid } from 'excel_exporter';
import { initializeDxObjectAssign, clearDxObjectAssign } from './objectAssignHelper.js';
import { initializeDxArrayFind, clearDxArrayFind } from './arrayFindHelper.js';

import 'ui/data_grid/ui.data_grid';
import 'ui/pivot_grid/ui.pivot_grid';

const ExcelJSLocalizationFormatTests = {
runCurrencyTests(values) {
Expand All @@ -17,7 +18,6 @@ const ExcelJSLocalizationFormatTests = {
},
beforeEach: function() {
this.worksheet = new ExcelJS.Workbook().addWorksheet('Test sheet');
this.customizeCellCallCount = 0;
helper = new ExcelJSTestHelper(this.worksheet);
},
after: function() {
Expand Down Expand Up @@ -70,6 +70,68 @@ const ExcelJSLocalizationFormatTests = {
});
});
});
},
runPivotGridCurrencyTests(values) {
let helper;
QUnit.module('PivotGrid currency Format', {
before: function() {
initializeDxObjectAssign();
initializeDxArrayFind();
},
beforeEach: function() {
this.worksheet = new ExcelJS.Workbook().addWorksheet('Test sheet');
helper = new ExcelJSTestHelper(this.worksheet);
},
after: function() {
clearDxObjectAssign();
clearDxArrayFind();
}
}, () => {
values.forEach((currency) => {
QUnit.test(`Export [string x string x number] with format: { type: 'currency', currency: ${currency.value} }`, function(assert) {
const done = assert.async();
const topLeft = { row: 1, column: 1 };

const ds = {
fields: [
{ area: 'row', dataField: 'row1', dataType: 'string' },
{ area: 'column', dataField: 'col1', dataType: 'string' },
{ area: 'data', summaryType: 'count', dataType: 'number', format: { type: 'currency', currency: currency.value } }
],
store: [
{ row1: 'A', col1: 'a' },
]
};

$('#qunit-fixture').append('<div id=\'pivotGrid\'></div>');
const pivotGrid = $('#pivotGrid').dxPivotGrid({
showColumnGrandTotals: false,
showRowGrandTotals: false,
dataSource: ds
}).dxPivotGrid('instance');

const expectedCells = [[
{ excelCell: { value: '', type: ExcelJS.ValueType.String, dataType: 'string' }, gridCell: { rowType: 'header' } },
{ excelCell: { value: 'a', type: ExcelJS.ValueType.String, dataType: 'string' }, gridCell: { rowType: 'header' } }
], [
{ excelCell: { value: 'A', type: ExcelJS.ValueType.String, dataType: 'string' }, gridCell: { rowType: 'header' } },
{ excelCell: { value: 1, type: ExcelJS.ValueType.Number, dataType: 'number', numberFormat: currency.expected, }, gridCell: { rowType: 'header' } }
]];

helper._extendExpectedCells(expectedCells, topLeft);

exportPivotGrid({
component: pivotGrid,
worksheet: this.worksheet
}).then((cellRange) => {
helper.checkValues(expectedCells);
helper.checkCellFormat(expectedCells);
helper.checkCellRange(cellRange, { row: 2, column: 2 }, topLeft);
done();
});
});
});
});
}
};

Expand Down

0 comments on commit c4c994d

Please sign in to comment.