Skip to content

Commit 3289f66

Browse files
committed
fix(excel-exporter): do not block UI thread when exporting excel #6673
1 parent c4dc979 commit 3289f66

File tree

3 files changed

+123
-15
lines changed

3 files changed

+123
-15
lines changed

projects/igniteui-angular/src/lib/services/csv/char-separated-value-data.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,14 +75,13 @@ export class CharSeparatedValueData {
7575
}
7676

7777
private processRecord(record, keys, escapeChars): string {
78-
let recordData = '';
79-
for (const keyName of keys) {
80-
81-
const value = (record[keyName] !== undefined) ? record[keyName] : this._isSpecialData ? record : '';
82-
recordData += this.processField(value, this._escapeCharacters);
78+
const recordData = new Array(keys.length);
79+
for (let index = 0; index < keys.length; index++) {
80+
const value = (record[keys[index]] !== undefined) ? record[keys[index]] : this._isSpecialData ? record : '';
81+
recordData[index] = this.processField(value, this._escapeCharacters);
8382
}
8483

85-
return recordData.slice(0, -this._delimiterLength) + this._eor;
84+
return recordData.join('').slice(0, -this._delimiterLength) + this._eor;
8685
}
8786

8887
private processDataRecords(currentData, keys, escapeChars) {

projects/igniteui-angular/src/lib/services/excel/excel-exporter.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { IgxBaseExporter } from '../exporter-common/base-export-service';
99
import { ExportUtilities } from '../exporter-common/export-utilities';
1010
import { WorksheetData } from './worksheet-data';
1111
import { IBaseEventArgs } from '../../core/utils';
12+
import { WorksheetFile } from './excel-files';
1213

1314
export interface IExcelExportEndedEventArgs extends IBaseEventArgs {
1415
xlsx: JSZip;
@@ -53,16 +54,22 @@ export class IgxExcelExporterService extends IgxBaseExporter {
5354
@Output()
5455
public onExportEnded = new EventEmitter<IExcelExportEndedEventArgs>();
5556

56-
private static populateFolder(folder: IExcelFolder, zip: JSZip, worksheetData: WorksheetData): any {
57+
private static populateFolderAsync(folder: IExcelFolder, zip: JSZip, worksheetData: WorksheetData, done: () => void): any {
5758
for (const childFolder of folder.childFolders(worksheetData)) {
5859
const folderIntance = ExcelElementsFactory.getExcelFolder(childFolder);
5960
const zipFolder = zip.folder(folderIntance.folderName);
60-
IgxExcelExporterService.populateFolder(folderIntance, zipFolder, worksheetData);
61+
IgxExcelExporterService.populateFolderAsync(folderIntance, zipFolder, worksheetData, done);
6162
}
6263

6364
for (const childFile of folder.childFiles(worksheetData)) {
6465
const fileInstance = ExcelElementsFactory.getExcelFile(childFile);
65-
fileInstance.writeElement(zip, worksheetData);
66+
if (fileInstance instanceof WorksheetFile) {
67+
(fileInstance as WorksheetFile).writeElementAsync(zip, worksheetData, () => {
68+
done();
69+
});
70+
} else {
71+
fileInstance.writeElement(zip, worksheetData);
72+
}
6673
}
6774
}
6875

@@ -81,12 +88,11 @@ export class IgxExcelExporterService extends IgxBaseExporter {
8188
this._xlsx = new JSZip();
8289

8390
const rootFolder = ExcelElementsFactory.getExcelFolder(ExcelFolderTypes.RootExcelFolder);
84-
IgxExcelExporterService.populateFolder(rootFolder, this._xlsx, worksheetData);
85-
86-
this._xlsx.generateAsync(IgxExcelExporterService.ZIP_OPTIONS).then((result) => {
87-
this.saveFile(result, options.fileName);
88-
89-
this.onExportEnded.emit({ xlsx: this._xlsx });
91+
IgxExcelExporterService.populateFolderAsync(rootFolder, this._xlsx, worksheetData, () => {
92+
this._xlsx.generateAsync(IgxExcelExporterService.ZIP_OPTIONS).then((result) => {
93+
this.saveFile(result, options.fileName);
94+
this.onExportEnded.emit({ xlsx: this._xlsx });
95+
});
9096
});
9197
}
9298

projects/igniteui-angular/src/lib/services/excel/excel-files.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ExcelStrings } from './excel-strings';
33
import { WorksheetData } from './worksheet-data';
44

55
import * as JSZip from 'jszip';
6+
import { yieldingLoop } from '../../core/utils';
67

78
/**
89
* @hidden
@@ -55,6 +56,10 @@ export class ThemeFile implements IExcelFile {
5556
*/
5657
export class WorksheetFile implements IExcelFile {
5758
private static MIN_WIDTH = 8.34;
59+
private maxOutlineLevel = 0;
60+
private dimension = '';
61+
private freezePane = '';
62+
private rowHeight = '';
5863

5964
public writeElement(folder: JSZip, worksheetData: WorksheetData) {
6065
const sheetData = [];
@@ -131,6 +136,104 @@ export class WorksheetFile implements IExcelFile {
131136
worksheetData.isTreeGridData, maxOutlineLevel));
132137
}
133138

139+
public writeElementAsync(folder: JSZip, worksheetData: WorksheetData, done: () => void) {
140+
this.prepareDataAsync(worksheetData, (cols, rows) => {
141+
const hasTable = !worksheetData.isEmpty && worksheetData.options.exportAsTable;
142+
folder.file('sheet1.xml', ExcelStrings.getSheetXML(
143+
this.dimension, this.freezePane, cols, rows, hasTable, worksheetData.isTreeGridData, this.maxOutlineLevel));
144+
done();
145+
});
146+
}
147+
148+
private prepareDataAsync(worksheetData: WorksheetData, done: (cols: string, rows: string) => void) {
149+
let sheetData = '';
150+
let cols = '';
151+
const dictionary = worksheetData.dataDictionary;
152+
153+
if (worksheetData.isEmpty) {
154+
sheetData += '<sheetData/>';
155+
this.dimension = 'A1';
156+
} else {
157+
sheetData += '<sheetData>';
158+
const height = worksheetData.options.rowHeight;
159+
this.rowHeight = height ? ' ht="' + height + '" customHeight="1"' : '';
160+
sheetData += `<row r="1"${this.rowHeight}>`;
161+
162+
for (let i = 0; i < worksheetData.columnCount; i++) {
163+
const column = ExcelStrings.getExcelColumn(i) + 1;
164+
const value = dictionary.saveValue(worksheetData.keys[i], i, true);
165+
sheetData += `<c r="${column}" t="s"><v>${value}</v></c>`;
166+
}
167+
sheetData += '</row>';
168+
169+
this.dimension = 'A1:' + ExcelStrings.getExcelColumn(worksheetData.columnCount - 1) + worksheetData.rowCount;
170+
cols += '<cols>';
171+
172+
for (let i = 0; i < worksheetData.columnCount; i++) {
173+
const width = dictionary.columnWidths[i];
174+
// Use the width provided in the options if it exists
175+
const widthInTwips = worksheetData.options.columnWidth ?
176+
worksheetData.options.columnWidth :
177+
Math.max(((width / 96) * 14.4), WorksheetFile.MIN_WIDTH);
178+
179+
cols += `<col min="${(i + 1)}" max="${(i + 1)}" width="${widthInTwips}" customWidth="1"/>`;
180+
}
181+
182+
cols += '</cols>';
183+
184+
if (worksheetData.indexOfLastPinnedColumn !== -1 &&
185+
!worksheetData.options.ignorePinning &&
186+
!worksheetData.options.ignoreColumnsOrder) {
187+
const frozenColumnCount = worksheetData.indexOfLastPinnedColumn + 1;
188+
const firstCell = ExcelStrings.getExcelColumn(frozenColumnCount) + '1';
189+
this.freezePane = `<pane xSplit="${frozenColumnCount}" topLeftCell="${firstCell}" activePane="topRight" state="frozen"/>`;
190+
}
191+
192+
this.processDataRecordsAsync(worksheetData, (rows) => {
193+
sheetData += rows;
194+
sheetData += '</sheetData>';
195+
done(cols, sheetData);
196+
});
197+
}
198+
}
199+
200+
private processDataRecordsAsync(worksheetData: WorksheetData, done: (rows: string) => void) {
201+
let dataRecords = '';
202+
const height = worksheetData.options.rowHeight;
203+
this.rowHeight = height ? ' ht="' + height + '" customHeight="1"' : '';
204+
205+
yieldingLoop(worksheetData.rowCount - 1, 1000,
206+
(i) => {
207+
dataRecords += this.processRow(worksheetData, i + 1);
208+
},
209+
() => {
210+
done(dataRecords);
211+
});
212+
}
213+
214+
private processRow(worksheetData: WorksheetData, i: number) {
215+
const rowData = new Array(worksheetData.columnCount + 2);
216+
if (!worksheetData.isTreeGridData) {
217+
rowData[0] = `<row r="${(i + 1)}"${this.rowHeight}>`;
218+
} else {
219+
const originalData = worksheetData.data[i].originalRowData;
220+
const sCollapsed = (!originalData.expanded) ? '' : (originalData.expanded === true) ? '' : ` collapsed="1"`;
221+
const sHidden = (originalData.parent && this.hasCollapsedParent(originalData)) ? ` hidden="1"` : '';
222+
const rowOutlineLevel = originalData.level ? originalData.level : 0;
223+
const sOutlineLevel = rowOutlineLevel > 0 ? ` outlineLevel="${rowOutlineLevel}"` : '';
224+
this.maxOutlineLevel = this.maxOutlineLevel < rowOutlineLevel ? rowOutlineLevel : this.maxOutlineLevel;
225+
rowData[0] = `<row r="${(i + 1)}"${this.rowHeight}${sOutlineLevel}${sCollapsed}${sHidden}>`;
226+
}
227+
228+
for (let j = 0; j < worksheetData.columnCount; j++) {
229+
const cellData = WorksheetFile.getCellData(worksheetData, i, j);
230+
rowData[j + 1] = cellData;
231+
}
232+
rowData[worksheetData.columnCount + 1] = '</row>';
233+
234+
return rowData.join('');
235+
}
236+
134237
private hasCollapsedParent(rowData) {
135238
let result = !rowData.parent.expanded;
136239
while (rowData.parent) {

0 commit comments

Comments
 (0)