@slickgrid-universal/text-export 5.0.0-beta.0 → 5.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,463 +1,463 @@
1
- import { TextEncoder } from 'text-encoding-utf-8';
2
- import type {
3
- Column,
4
- ContainerService,
5
- ExternalResource,
6
- GridOption,
7
- KeyTitlePair,
8
- Locale,
9
- PubSubService,
10
- SlickDataView,
11
- SlickGrid,
12
- TextExportOption,
13
- TextExportService as BaseTextExportService,
14
- TranslaterService,
15
- } from '@slickgrid-universal/common';
16
- import {
17
- Constants,
18
- DelimiterType,
19
- FileType,
20
- // utility functions
21
- exportWithFormatterWhenDefined,
22
- getTranslationPrefix,
23
- htmlEntityDecode,
24
- } from '@slickgrid-universal/common';
25
- import { addWhiteSpaces, deepCopy, getHtmlStringOutput, stripTags, titleCase } from '@slickgrid-universal/utils';
26
-
27
- const DEFAULT_EXPORT_OPTIONS: TextExportOption = {
28
- delimiter: DelimiterType.comma,
29
- filename: 'export',
30
- format: FileType.csv,
31
- useUtf8WithBom: true,
32
- };
33
-
34
- type ExportTextDownloadOption = {
35
- filename: string;
36
- content: string;
37
- format: FileType | string;
38
- mimeType: string;
39
- useUtf8WithBom?: boolean;
40
- };
41
-
42
- export class TextExportService implements ExternalResource, BaseTextExportService {
43
- protected _delimiter = ',';
44
- protected _exportQuoteWrapper = '';
45
- protected _exportOptions!: TextExportOption;
46
- protected _fileFormat = FileType.csv;
47
- protected _lineCarriageReturn = '\n';
48
- protected _grid!: SlickGrid;
49
- protected _groupedColumnHeaders?: Array<KeyTitlePair>;
50
- protected _columnHeaders: Array<KeyTitlePair> = [];
51
- protected _hasGroupedItems = false;
52
- protected _locales!: Locale;
53
- protected _pubSubService!: PubSubService | null;
54
- protected _translaterService: TranslaterService | undefined;
55
-
56
- /** ExcelExportService class name which is use to find service instance in the external registered services */
57
- readonly className = 'TextExportService';
58
-
59
- constructor() { }
60
-
61
- protected get _datasetIdPropName(): string {
62
- return this._gridOptions && this._gridOptions.datasetIdPropertyName || 'id';
63
- }
64
-
65
- /** Getter of SlickGrid DataView object */
66
- get _dataView(): SlickDataView {
67
- return this._grid?.getData<SlickDataView>();
68
- }
69
-
70
- /** Getter for the Grid Options pulled through the Grid Object */
71
- protected get _gridOptions(): GridOption {
72
- return this._grid?.getOptions() ?? {} as GridOption;
73
- }
74
-
75
- dispose() {
76
- this._pubSubService?.unsubscribeAll();
77
- }
78
-
79
- /**
80
- * Initialize the Service
81
- * @param grid
82
- * @param containerService
83
- */
84
- init(grid: SlickGrid, containerService: ContainerService): void {
85
- this._grid = grid;
86
- this._pubSubService = containerService.get<PubSubService>('PubSubService');
87
-
88
- // get locales provided by user in main file or else use default English locales via the Constants
89
- this._locales = this._gridOptions && this._gridOptions.locales || Constants.locales;
90
- this._translaterService = this._gridOptions?.translater;
91
-
92
- if (this._gridOptions.enableTranslate && (!this._translaterService || !this._translaterService.translate)) {
93
- throw new Error('[Slickgrid-Universal] requires a Translate Service to be passed in the "translater" Grid Options when "enableTranslate" is enabled. (example: this.gridOptions = { enableTranslate: true, translater: this.translaterService })');
94
- }
95
- }
96
-
97
- /**
98
- * Function to export the Grid result to an Excel CSV format using javascript for it to produce the CSV file.
99
- * This is a WYSIWYG export to file output (What You See is What You Get)
100
- *
101
- * NOTES: The column position needs to match perfectly the JSON Object position because of the way we are pulling the data,
102
- * which means that if any column(s) got moved in the UI, it has to be reflected in the JSON array output as well
103
- *
104
- * Example: exportToFile({ format: FileType.csv, delimiter: DelimiterType.comma })
105
- */
106
- exportToFile(options?: TextExportOption): Promise<boolean> {
107
- if (!this._grid || !this._dataView || !this._pubSubService) {
108
- throw new Error('[Slickgrid-Universal] it seems that the SlickGrid & DataView objects and/or PubSubService are not initialized did you forget to enable the grid option flag "enableTextExport"?');
109
- }
110
-
111
- return new Promise(resolve => {
112
- this._pubSubService?.publish(`onBeforeExportToTextFile`, true);
113
- this._exportOptions = deepCopy({ ...DEFAULT_EXPORT_OPTIONS, ...this._gridOptions.textExportOptions, ...options });
114
- this._delimiter = this._exportOptions.delimiterOverride || this._exportOptions.delimiter || '';
115
- this._fileFormat = this._exportOptions.format || FileType.csv;
116
-
117
- // get the CSV output from the grid data
118
- const dataOutput = this.getDataOutput();
119
-
120
- // trigger a download file
121
- // wrap it into a setTimeout so that the EventAggregator has enough time to start a pre-process like showing a spinner
122
- setTimeout(() => {
123
- const downloadOptions = {
124
- filename: `${this._exportOptions.filename}.${this._fileFormat}`,
125
- format: this._fileFormat || FileType.csv,
126
- mimeType: this._exportOptions.mimeType || 'text/plain',
127
- useUtf8WithBom: (this._exportOptions && this._exportOptions.hasOwnProperty('useUtf8WithBom')) ? this._exportOptions.useUtf8WithBom : true
128
- };
129
-
130
- // start downloading but add the content property only on the start download not on the event itself
131
- this.startDownloadFile({ ...downloadOptions, content: dataOutput } as ExportTextDownloadOption); // add content property
132
- this._pubSubService?.publish(`onAfterExportToTextFile`, downloadOptions as ExportTextDownloadOption);
133
- resolve(true);
134
- }, 0);
135
- });
136
- }
137
-
138
- /**
139
- * Triggers download file with file format.
140
- * IE(6-10) are not supported
141
- * All other browsers will use plain javascript on client side to produce a file download.
142
- * @param options
143
- */
144
- startDownloadFile(options: ExportTextDownloadOption): void {
145
- // make sure no html entities exist in the data
146
- const csvContent = htmlEntityDecode(options.content);
147
-
148
- // dealing with Excel CSV export and UTF-8 is a little tricky.. We will use Option #2 to cover older Excel versions
149
- // Option #1: we need to make Excel knowing that it's dealing with an UTF-8, A correctly formatted UTF8 file can have a Byte Order Mark as its first three octets
150
- // reference: http://stackoverflow.com/questions/155097/microsoft-excel-mangles-diacritics-in-csv-files
151
- // Option#2: use a 3rd party extension to javascript encode into UTF-16
152
- let outputData: Uint8Array | string;
153
- if (options.format === FileType.csv) {
154
- outputData = new TextEncoder('utf-8').encode(csvContent);
155
- } else {
156
- outputData = csvContent;
157
- }
158
-
159
- // create a Blob object for the download
160
- const blob = new Blob([options.useUtf8WithBom ? '\uFEFF' : '', outputData], {
161
- type: options.mimeType
162
- });
163
-
164
- // when using IE/Edge, then use different download call
165
- if (typeof (navigator as any).msSaveOrOpenBlob === 'function') {
166
- (navigator as any).msSaveOrOpenBlob(blob, options.filename);
167
- } else {
168
- // this trick will generate a temp <a /> tag
169
- // the code will then trigger a hidden click for it to start downloading
170
- const link = document.createElement('a');
171
- const csvUrl = URL.createObjectURL(blob);
172
-
173
- link.textContent = 'download';
174
- link.href = csvUrl;
175
- link.setAttribute('download', options.filename);
176
-
177
- // set the visibility to hidden so there is no effect on your web-layout
178
- link.style.visibility = 'hidden';
179
-
180
- // this part will append the anchor tag, trigger a click (for download to start) and finally remove the tag once completed
181
- document.body.appendChild(link);
182
- link.click();
183
- document.body.removeChild(link);
184
- }
185
- }
186
-
187
- // -----------------------
188
- // protected functions
189
- // -----------------------
190
-
191
- protected getDataOutput(): string {
192
- const columns = this._grid.getColumns() || [];
193
-
194
- // Group By text, it could be set in the export options or from translation or if nothing is found then use the English constant text
195
- let groupByColumnHeader = this._exportOptions.groupingColumnHeaderTitle;
196
- if (!groupByColumnHeader && this._gridOptions.enableTranslate && this._translaterService?.translate && this._translaterService?.getCurrentLanguage?.()) {
197
- groupByColumnHeader = this._translaterService.translate(`${getTranslationPrefix(this._gridOptions)}GROUP_BY`);
198
- } else if (!groupByColumnHeader) {
199
- groupByColumnHeader = this._locales && this._locales.TEXT_GROUP_BY;
200
- }
201
-
202
- // a CSV needs double quotes wrapper, the other types do not need any wrapper
203
- this._exportQuoteWrapper = (this._fileFormat === FileType.csv) ? '"' : '';
204
-
205
- // data variable which will hold all the fields data of a row
206
- let outputDataString = '';
207
-
208
- // get grouped column titles and if found, we will add a "Group by" column at the first column index
209
- // if it's a CSV format, we'll escape the text in double quotes
210
- const grouping = this._dataView.getGrouping();
211
- if (grouping && Array.isArray(grouping) && grouping.length > 0) {
212
- this._hasGroupedItems = true;
213
- outputDataString += (this._fileFormat === FileType.csv) ? `"${groupByColumnHeader}"${this._delimiter}` : `${groupByColumnHeader}${this._delimiter}`;
214
- } else {
215
- this._hasGroupedItems = false;
216
- }
217
-
218
- // get all Grouped Column Header Titles when defined (from pre-header row)
219
- if (this._gridOptions.createPreHeaderPanel && this._gridOptions.showPreHeaderPanel && !this._gridOptions.enableDraggableGrouping) {
220
- this._groupedColumnHeaders = this.getColumnGroupedHeaderTitles(columns) || [];
221
- if (this._groupedColumnHeaders && Array.isArray(this._groupedColumnHeaders) && this._groupedColumnHeaders.length > 0) {
222
- // add the header row + add a new line at the end of the row
223
- const outputGroupedHeaderTitles = this._groupedColumnHeaders.map((header) => `${this._exportQuoteWrapper}${header.title}${this._exportQuoteWrapper}`);
224
- outputDataString += (outputGroupedHeaderTitles.join(this._delimiter) + this._lineCarriageReturn);
225
- }
226
- }
227
-
228
- // get all Column Header Titles
229
- this._columnHeaders = this.getColumnHeaders(columns) || [];
230
- if (this._columnHeaders && Array.isArray(this._columnHeaders) && this._columnHeaders.length > 0) {
231
- // add the header row + add a new line at the end of the row
232
- const outputHeaderTitles = this._columnHeaders.map((header) => stripTags(`${this._exportQuoteWrapper}${header.title}${this._exportQuoteWrapper}`));
233
- outputDataString += (outputHeaderTitles.join(this._delimiter) + this._lineCarriageReturn);
234
- }
235
-
236
- // Populate the rest of the Grid Data
237
- outputDataString += this.getAllGridRowData(columns, this._lineCarriageReturn);
238
-
239
- return outputDataString;
240
- }
241
-
242
- /**
243
- * Get all the grid row data and return that as an output string
244
- */
245
- protected getAllGridRowData(columns: Column[], lineCarriageReturn: string): string {
246
- const outputDataStrings = [];
247
- const lineCount = this._dataView.getLength();
248
-
249
- // loop through all the grid rows of data
250
- for (let rowNumber = 0; rowNumber < lineCount; rowNumber++) {
251
- const itemObj = this._dataView.getItem(rowNumber);
252
-
253
- // make sure we have a filled object AND that the item doesn't include the "getItem" method
254
- // this happen could happen with an opened Row Detail as it seems to include an empty Slick DataView (we'll just skip those lines)
255
- if (itemObj && !itemObj.hasOwnProperty('getItem')) {
256
- // Normal row (not grouped by anything) would have an ID which was predefined in the Grid Columns definition
257
- if (itemObj[this._datasetIdPropName] !== null && itemObj[this._datasetIdPropName] !== undefined) {
258
- // get regular row item data
259
- outputDataStrings.push(this.readRegularRowData(columns, rowNumber, itemObj));
260
- } else if (this._hasGroupedItems && itemObj.__groupTotals === undefined) {
261
- // get the group row
262
- outputDataStrings.push(this.readGroupedTitleRow(itemObj));
263
- } else if (itemObj.__groupTotals) {
264
- // else if the row is a Group By and we have agreggators, then a property of '__groupTotals' would exist under that object
265
- outputDataStrings.push(this.readGroupedTotalRow(columns, itemObj));
266
- }
267
- }
268
- }
269
-
270
- return outputDataStrings.join(lineCarriageReturn);
271
- }
272
-
273
- /**
274
- * Get all Grouped Header Titles and their keys, translate the title when required.
275
- * @param {Array<object>} columns of the grid
276
- */
277
- protected getColumnGroupedHeaderTitles(columns: Column[]): Array<KeyTitlePair> {
278
- const groupedColumnHeaders: KeyTitlePair[] = [];
279
-
280
- if (columns && Array.isArray(columns)) {
281
- // Populate the Grouped Column Header, pull the columnGroup(Key) defined
282
- columns.forEach((columnDef) => {
283
- let groupedHeaderTitle = '';
284
- if (columnDef.columnGroupKey && this._gridOptions.enableTranslate && this._translaterService?.translate && this._translaterService?.getCurrentLanguage?.()) {
285
- groupedHeaderTitle = this._translaterService.translate(columnDef.columnGroupKey);
286
- } else {
287
- groupedHeaderTitle = columnDef.columnGroup || '';
288
- }
289
- const skippedField = columnDef.excludeFromExport || false;
290
-
291
- // if column width is 0px, then we consider that field as a hidden field and should not be part of the export
292
- if ((columnDef.width === undefined || columnDef.width > 0) && !skippedField) {
293
- groupedColumnHeaders.push({
294
- key: (columnDef.field || columnDef.id) as string,
295
- title: groupedHeaderTitle || ''
296
- });
297
- }
298
- });
299
- }
300
- return groupedColumnHeaders;
301
- }
302
-
303
- /**
304
- * Get all header titles and their keys, translate the title when required.
305
- * @param {Array<object>} columns of the grid
306
- */
307
- protected getColumnHeaders(columns: Column[]): Array<KeyTitlePair> {
308
- const columnHeaders: Array<KeyTitlePair> = [];
309
-
310
- if (columns && Array.isArray(columns)) {
311
- // Populate the Column Header, pull the name defined
312
- columns.forEach((columnDef) => {
313
- let headerTitle = '';
314
- if ((columnDef.nameKey || columnDef.nameKey) && this._gridOptions.enableTranslate && this._translaterService?.translate && this._translaterService?.getCurrentLanguage?.()) {
315
- headerTitle = this._translaterService.translate((columnDef.nameKey || columnDef.nameKey));
316
- } else {
317
- headerTitle = getHtmlStringOutput(columnDef.name || '', 'innerHTML') || titleCase(columnDef.field);
318
- }
319
- const skippedField = columnDef.excludeFromExport || false;
320
-
321
- // if column width is 0px, then we consider that field as a hidden field and should not be part of the export
322
- if ((columnDef.width === undefined || columnDef.width > 0) && !skippedField) {
323
- columnHeaders.push({
324
- key: (columnDef.field || columnDef.id) as string,
325
- title: headerTitle || ''
326
- });
327
- }
328
- });
329
- }
330
- return columnHeaders;
331
- }
332
-
333
- /**
334
- * Get the data of a regular row (a row without grouping)
335
- * @param {Array<Object>} columns - column definitions
336
- * @param {Number} row - row index
337
- * @param {Object} itemObj - item datacontext object
338
- */
339
- protected readRegularRowData(columns: Column[], row: number, itemObj: any) {
340
- let idx = 0;
341
- const rowOutputStrings = [];
342
- const exportQuoteWrapper = this._exportQuoteWrapper;
343
- let prevColspan: number | string = 1;
344
- const itemMetadata = this._dataView.getItemMetadata(row);
345
-
346
- for (let col = 0, ln = columns.length; col < ln; col++) {
347
- const columnDef = columns[col];
348
-
349
- // skip excluded column
350
- if (columnDef.excludeFromExport) {
351
- continue;
352
- }
353
-
354
- // if we are grouping and are on 1st column index, we need to skip this column since it will be used later by the grouping text:: Group by [columnX]
355
- if (this._hasGroupedItems && idx === 0) {
356
- const emptyValue = this._fileFormat === FileType.csv ? `""` : '';
357
- rowOutputStrings.push(emptyValue);
358
- }
359
-
360
- let colspanColumnId;
361
- if (itemMetadata?.columns) {
362
- const metadata = itemMetadata?.columns;
363
- const columnData = metadata[columnDef.id] || metadata[col];
364
- if (!((!isNaN(prevColspan as number) && +prevColspan > 1) || (prevColspan === '*' && col > 0))) {
365
- prevColspan = columnData?.colspan ?? 1;
366
- }
367
- if (prevColspan !== '*') {
368
- if (columnDef.id in metadata) {
369
- colspanColumnId = columnDef.id;
370
- }
371
- }
372
- }
373
-
374
- if ((prevColspan === '*' && col > 0) || ((!isNaN(prevColspan as number) && +prevColspan > 1) && columnDef.id !== colspanColumnId)) {
375
- rowOutputStrings.push('');
376
- if ((!isNaN(prevColspan as number) && +prevColspan > 1)) {
377
- (prevColspan as number)--;
378
- }
379
- } else {
380
- // get the output by analyzing if we'll pull the value from the cell or from a formatter
381
- let itemData = exportWithFormatterWhenDefined(row, col, columnDef, itemObj, this._grid, this._exportOptions);
382
-
383
- // does the user want to sanitize the output data (remove HTML tags)?
384
- if (columnDef.sanitizeDataExport || this._exportOptions.sanitizeDataExport) {
385
- itemData = stripTags(itemData);
386
- }
387
-
388
- // when CSV we also need to escape double quotes twice, so " becomes ""
389
- if (this._fileFormat === FileType.csv && itemData) {
390
- itemData = itemData.toString().replace(/"/gi, `""`);
391
- }
392
-
393
- // do we have a wrapper to keep as a string? in certain cases like "1E06", we don't want excel to transform it into exponential (1.0E06)
394
- // to cancel that effect we can had = in front, ex: ="1E06"
395
- const keepAsStringWrapper = columnDef?.exportCsvForceToKeepAsString ? '=' : '';
396
-
397
- rowOutputStrings.push(keepAsStringWrapper + exportQuoteWrapper + itemData + exportQuoteWrapper);
398
- }
399
-
400
- idx++;
401
- }
402
-
403
- return rowOutputStrings.join(this._delimiter);
404
- }
405
-
406
- /**
407
- * Get the grouped title(s) and its group title formatter, for example if we grouped by salesRep, the returned result would be:: 'Sales Rep: John Dow (2 items)'
408
- * @param itemObj
409
- */
410
- protected readGroupedTitleRow(itemObj: any) {
411
- let groupName = stripTags(itemObj.title);
412
- const exportQuoteWrapper = this._exportQuoteWrapper;
413
-
414
- groupName = addWhiteSpaces(5 * itemObj.level) + groupName;
415
-
416
- if (this._fileFormat === FileType.csv) {
417
- // when CSV we also need to escape double quotes twice, so " becomes ""
418
- groupName = groupName.toString().replace(/"/gi, `""`);
419
- }
420
-
421
- return exportQuoteWrapper + groupName + exportQuoteWrapper;
422
- }
423
-
424
- /**
425
- * Get the grouped totals (below the regular rows), these are set by Slick Aggregators.
426
- * For example if we grouped by "salesRep" and we have a Sum Aggregator on "sales", then the returned output would be:: ["Sum 123$"]
427
- * @param itemObj
428
- */
429
- protected readGroupedTotalRow(columns: Column[], itemObj: any) {
430
- const delimiter = this._exportOptions.delimiter;
431
- const format = this._exportOptions.format;
432
- const groupingAggregatorRowText = this._exportOptions.groupingAggregatorRowText || '';
433
- const exportQuoteWrapper = this._exportQuoteWrapper;
434
- const outputStrings = [`${exportQuoteWrapper}${groupingAggregatorRowText}${exportQuoteWrapper}`];
435
-
436
- columns.forEach((columnDef) => {
437
- let itemData = '';
438
- const skippedField = columnDef.excludeFromExport || false;
439
-
440
- // if there's a groupTotalsFormatter, we will re-run it to get the exact same output as what is shown in UI
441
- if (columnDef.groupTotalsFormatter) {
442
- const totalResult = columnDef.groupTotalsFormatter(itemObj, columnDef, this._grid);
443
- itemData = totalResult instanceof HTMLElement ? totalResult.textContent || '' : totalResult;
444
- }
445
-
446
- // does the user want to sanitize the output data (remove HTML tags)?
447
- if (columnDef.sanitizeDataExport || this._exportOptions.sanitizeDataExport) {
448
- itemData = stripTags(itemData);
449
- }
450
-
451
- if (format === FileType.csv) {
452
- // when CSV we also need to escape double quotes twice, so a double quote " becomes 2x double quotes ""
453
- itemData = itemData.toString().replace(/"/gi, `""`);
454
- }
455
- // add the column (unless user wants to skip it)
456
- if ((columnDef.width === undefined || columnDef.width > 0) && !skippedField) {
457
- outputStrings.push(exportQuoteWrapper + itemData + exportQuoteWrapper);
458
- }
459
- });
460
-
461
- return outputStrings.join(delimiter);
462
- }
463
- }
1
+ import { TextEncoder } from 'text-encoding-utf-8';
2
+ import type {
3
+ Column,
4
+ ContainerService,
5
+ ExternalResource,
6
+ GridOption,
7
+ KeyTitlePair,
8
+ Locale,
9
+ PubSubService,
10
+ SlickDataView,
11
+ SlickGrid,
12
+ TextExportOption,
13
+ TextExportService as BaseTextExportService,
14
+ TranslaterService,
15
+ } from '@slickgrid-universal/common';
16
+ import {
17
+ Constants,
18
+ DelimiterType,
19
+ FileType,
20
+ // utility functions
21
+ exportWithFormatterWhenDefined,
22
+ getTranslationPrefix,
23
+ htmlEntityDecode,
24
+ } from '@slickgrid-universal/common';
25
+ import { addWhiteSpaces, deepCopy, getHtmlStringOutput, stripTags, titleCase } from '@slickgrid-universal/utils';
26
+
27
+ const DEFAULT_EXPORT_OPTIONS: TextExportOption = {
28
+ delimiter: DelimiterType.comma,
29
+ filename: 'export',
30
+ format: FileType.csv,
31
+ useUtf8WithBom: true,
32
+ };
33
+
34
+ type ExportTextDownloadOption = {
35
+ filename: string;
36
+ content: string;
37
+ format: FileType | string;
38
+ mimeType: string;
39
+ useUtf8WithBom?: boolean;
40
+ };
41
+
42
+ export class TextExportService implements ExternalResource, BaseTextExportService {
43
+ protected _delimiter = ',';
44
+ protected _exportQuoteWrapper = '';
45
+ protected _exportOptions!: TextExportOption;
46
+ protected _fileFormat = FileType.csv;
47
+ protected _lineCarriageReturn = '\n';
48
+ protected _grid!: SlickGrid;
49
+ protected _groupedColumnHeaders?: Array<KeyTitlePair>;
50
+ protected _columnHeaders: Array<KeyTitlePair> = [];
51
+ protected _hasGroupedItems = false;
52
+ protected _locales!: Locale;
53
+ protected _pubSubService!: PubSubService | null;
54
+ protected _translaterService: TranslaterService | undefined;
55
+
56
+ /** ExcelExportService class name which is use to find service instance in the external registered services */
57
+ readonly className = 'TextExportService';
58
+
59
+ constructor() { }
60
+
61
+ protected get _datasetIdPropName(): string {
62
+ return this._gridOptions && this._gridOptions.datasetIdPropertyName || 'id';
63
+ }
64
+
65
+ /** Getter of SlickGrid DataView object */
66
+ get _dataView(): SlickDataView {
67
+ return this._grid?.getData<SlickDataView>();
68
+ }
69
+
70
+ /** Getter for the Grid Options pulled through the Grid Object */
71
+ protected get _gridOptions(): GridOption {
72
+ return this._grid?.getOptions() ?? {} as GridOption;
73
+ }
74
+
75
+ dispose() {
76
+ this._pubSubService?.unsubscribeAll();
77
+ }
78
+
79
+ /**
80
+ * Initialize the Service
81
+ * @param grid
82
+ * @param containerService
83
+ */
84
+ init(grid: SlickGrid, containerService: ContainerService): void {
85
+ this._grid = grid;
86
+ this._pubSubService = containerService.get<PubSubService>('PubSubService');
87
+
88
+ // get locales provided by user in main file or else use default English locales via the Constants
89
+ this._locales = this._gridOptions && this._gridOptions.locales || Constants.locales;
90
+ this._translaterService = this._gridOptions?.translater;
91
+
92
+ if (this._gridOptions.enableTranslate && (!this._translaterService || !this._translaterService.translate)) {
93
+ throw new Error('[Slickgrid-Universal] requires a Translate Service to be passed in the "translater" Grid Options when "enableTranslate" is enabled. (example: this.gridOptions = { enableTranslate: true, translater: this.translaterService })');
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Function to export the Grid result to an Excel CSV format using javascript for it to produce the CSV file.
99
+ * This is a WYSIWYG export to file output (What You See is What You Get)
100
+ *
101
+ * NOTES: The column position needs to match perfectly the JSON Object position because of the way we are pulling the data,
102
+ * which means that if any column(s) got moved in the UI, it has to be reflected in the JSON array output as well
103
+ *
104
+ * Example: exportToFile({ format: FileType.csv, delimiter: DelimiterType.comma })
105
+ */
106
+ exportToFile(options?: TextExportOption): Promise<boolean> {
107
+ if (!this._grid || !this._dataView || !this._pubSubService) {
108
+ throw new Error('[Slickgrid-Universal] it seems that the SlickGrid & DataView objects and/or PubSubService are not initialized did you forget to enable the grid option flag "enableTextExport"?');
109
+ }
110
+
111
+ return new Promise(resolve => {
112
+ this._pubSubService?.publish(`onBeforeExportToTextFile`, true);
113
+ this._exportOptions = deepCopy({ ...DEFAULT_EXPORT_OPTIONS, ...this._gridOptions.textExportOptions, ...options });
114
+ this._delimiter = this._exportOptions.delimiterOverride || this._exportOptions.delimiter || '';
115
+ this._fileFormat = this._exportOptions.format || FileType.csv;
116
+
117
+ // get the CSV output from the grid data
118
+ const dataOutput = this.getDataOutput();
119
+
120
+ // trigger a download file
121
+ // wrap it into a setTimeout so that the EventAggregator has enough time to start a pre-process like showing a spinner
122
+ setTimeout(() => {
123
+ const downloadOptions = {
124
+ filename: `${this._exportOptions.filename}.${this._fileFormat}`,
125
+ format: this._fileFormat || FileType.csv,
126
+ mimeType: this._exportOptions.mimeType || 'text/plain',
127
+ useUtf8WithBom: (this._exportOptions && this._exportOptions.hasOwnProperty('useUtf8WithBom')) ? this._exportOptions.useUtf8WithBom : true
128
+ };
129
+
130
+ // start downloading but add the content property only on the start download not on the event itself
131
+ this.startDownloadFile({ ...downloadOptions, content: dataOutput } as ExportTextDownloadOption); // add content property
132
+ this._pubSubService?.publish(`onAfterExportToTextFile`, downloadOptions as ExportTextDownloadOption);
133
+ resolve(true);
134
+ }, 0);
135
+ });
136
+ }
137
+
138
+ /**
139
+ * Triggers download file with file format.
140
+ * IE(6-10) are not supported
141
+ * All other browsers will use plain javascript on client side to produce a file download.
142
+ * @param options
143
+ */
144
+ startDownloadFile(options: ExportTextDownloadOption): void {
145
+ // make sure no html entities exist in the data
146
+ const csvContent = htmlEntityDecode(options.content);
147
+
148
+ // dealing with Excel CSV export and UTF-8 is a little tricky.. We will use Option #2 to cover older Excel versions
149
+ // Option #1: we need to make Excel knowing that it's dealing with an UTF-8, A correctly formatted UTF8 file can have a Byte Order Mark as its first three octets
150
+ // reference: http://stackoverflow.com/questions/155097/microsoft-excel-mangles-diacritics-in-csv-files
151
+ // Option#2: use a 3rd party extension to javascript encode into UTF-16
152
+ let outputData: Uint8Array | string;
153
+ if (options.format === FileType.csv) {
154
+ outputData = new TextEncoder('utf-8').encode(csvContent);
155
+ } else {
156
+ outputData = csvContent;
157
+ }
158
+
159
+ // create a Blob object for the download
160
+ const blob = new Blob([options.useUtf8WithBom ? '\uFEFF' : '', outputData], {
161
+ type: options.mimeType
162
+ });
163
+
164
+ // when using IE/Edge, then use different download call
165
+ if (typeof (navigator as any).msSaveOrOpenBlob === 'function') {
166
+ (navigator as any).msSaveOrOpenBlob(blob, options.filename);
167
+ } else {
168
+ // this trick will generate a temp <a /> tag
169
+ // the code will then trigger a hidden click for it to start downloading
170
+ const link = document.createElement('a');
171
+ const csvUrl = URL.createObjectURL(blob);
172
+
173
+ link.textContent = 'download';
174
+ link.href = csvUrl;
175
+ link.setAttribute('download', options.filename);
176
+
177
+ // set the visibility to hidden so there is no effect on your web-layout
178
+ link.style.visibility = 'hidden';
179
+
180
+ // this part will append the anchor tag, trigger a click (for download to start) and finally remove the tag once completed
181
+ document.body.appendChild(link);
182
+ link.click();
183
+ document.body.removeChild(link);
184
+ }
185
+ }
186
+
187
+ // -----------------------
188
+ // protected functions
189
+ // -----------------------
190
+
191
+ protected getDataOutput(): string {
192
+ const columns = this._grid.getColumns() || [];
193
+
194
+ // Group By text, it could be set in the export options or from translation or if nothing is found then use the English constant text
195
+ let groupByColumnHeader = this._exportOptions.groupingColumnHeaderTitle;
196
+ if (!groupByColumnHeader && this._gridOptions.enableTranslate && this._translaterService?.translate && this._translaterService?.getCurrentLanguage?.()) {
197
+ groupByColumnHeader = this._translaterService.translate(`${getTranslationPrefix(this._gridOptions)}GROUP_BY`);
198
+ } else if (!groupByColumnHeader) {
199
+ groupByColumnHeader = this._locales && this._locales.TEXT_GROUP_BY;
200
+ }
201
+
202
+ // a CSV needs double quotes wrapper, the other types do not need any wrapper
203
+ this._exportQuoteWrapper = (this._fileFormat === FileType.csv) ? '"' : '';
204
+
205
+ // data variable which will hold all the fields data of a row
206
+ let outputDataString = '';
207
+
208
+ // get grouped column titles and if found, we will add a "Group by" column at the first column index
209
+ // if it's a CSV format, we'll escape the text in double quotes
210
+ const grouping = this._dataView.getGrouping();
211
+ if (grouping && Array.isArray(grouping) && grouping.length > 0) {
212
+ this._hasGroupedItems = true;
213
+ outputDataString += (this._fileFormat === FileType.csv) ? `"${groupByColumnHeader}"${this._delimiter}` : `${groupByColumnHeader}${this._delimiter}`;
214
+ } else {
215
+ this._hasGroupedItems = false;
216
+ }
217
+
218
+ // get all Grouped Column Header Titles when defined (from pre-header row)
219
+ if (this._gridOptions.createPreHeaderPanel && this._gridOptions.showPreHeaderPanel && !this._gridOptions.enableDraggableGrouping) {
220
+ this._groupedColumnHeaders = this.getColumnGroupedHeaderTitles(columns) || [];
221
+ if (this._groupedColumnHeaders && Array.isArray(this._groupedColumnHeaders) && this._groupedColumnHeaders.length > 0) {
222
+ // add the header row + add a new line at the end of the row
223
+ const outputGroupedHeaderTitles = this._groupedColumnHeaders.map((header) => `${this._exportQuoteWrapper}${header.title}${this._exportQuoteWrapper}`);
224
+ outputDataString += (outputGroupedHeaderTitles.join(this._delimiter) + this._lineCarriageReturn);
225
+ }
226
+ }
227
+
228
+ // get all Column Header Titles
229
+ this._columnHeaders = this.getColumnHeaders(columns) || [];
230
+ if (this._columnHeaders && Array.isArray(this._columnHeaders) && this._columnHeaders.length > 0) {
231
+ // add the header row + add a new line at the end of the row
232
+ const outputHeaderTitles = this._columnHeaders.map((header) => stripTags(`${this._exportQuoteWrapper}${header.title}${this._exportQuoteWrapper}`));
233
+ outputDataString += (outputHeaderTitles.join(this._delimiter) + this._lineCarriageReturn);
234
+ }
235
+
236
+ // Populate the rest of the Grid Data
237
+ outputDataString += this.getAllGridRowData(columns, this._lineCarriageReturn);
238
+
239
+ return outputDataString;
240
+ }
241
+
242
+ /**
243
+ * Get all the grid row data and return that as an output string
244
+ */
245
+ protected getAllGridRowData(columns: Column[], lineCarriageReturn: string): string {
246
+ const outputDataStrings = [];
247
+ const lineCount = this._dataView.getLength();
248
+
249
+ // loop through all the grid rows of data
250
+ for (let rowNumber = 0; rowNumber < lineCount; rowNumber++) {
251
+ const itemObj = this._dataView.getItem(rowNumber);
252
+
253
+ // make sure we have a filled object AND that the item doesn't include the "getItem" method
254
+ // this happen could happen with an opened Row Detail as it seems to include an empty Slick DataView (we'll just skip those lines)
255
+ if (itemObj && !itemObj.hasOwnProperty('getItem')) {
256
+ // Normal row (not grouped by anything) would have an ID which was predefined in the Grid Columns definition
257
+ if (itemObj[this._datasetIdPropName] !== null && itemObj[this._datasetIdPropName] !== undefined) {
258
+ // get regular row item data
259
+ outputDataStrings.push(this.readRegularRowData(columns, rowNumber, itemObj));
260
+ } else if (this._hasGroupedItems && itemObj.__groupTotals === undefined) {
261
+ // get the group row
262
+ outputDataStrings.push(this.readGroupedTitleRow(itemObj));
263
+ } else if (itemObj.__groupTotals) {
264
+ // else if the row is a Group By and we have agreggators, then a property of '__groupTotals' would exist under that object
265
+ outputDataStrings.push(this.readGroupedTotalRow(columns, itemObj));
266
+ }
267
+ }
268
+ }
269
+
270
+ return outputDataStrings.join(lineCarriageReturn);
271
+ }
272
+
273
+ /**
274
+ * Get all Grouped Header Titles and their keys, translate the title when required.
275
+ * @param {Array<object>} columns of the grid
276
+ */
277
+ protected getColumnGroupedHeaderTitles(columns: Column[]): Array<KeyTitlePair> {
278
+ const groupedColumnHeaders: KeyTitlePair[] = [];
279
+
280
+ if (columns && Array.isArray(columns)) {
281
+ // Populate the Grouped Column Header, pull the columnGroup(Key) defined
282
+ columns.forEach((columnDef) => {
283
+ let groupedHeaderTitle = '';
284
+ if (columnDef.columnGroupKey && this._gridOptions.enableTranslate && this._translaterService?.translate && this._translaterService?.getCurrentLanguage?.()) {
285
+ groupedHeaderTitle = this._translaterService.translate(columnDef.columnGroupKey);
286
+ } else {
287
+ groupedHeaderTitle = columnDef.columnGroup || '';
288
+ }
289
+ const skippedField = columnDef.excludeFromExport || false;
290
+
291
+ // if column width is 0px, then we consider that field as a hidden field and should not be part of the export
292
+ if ((columnDef.width === undefined || columnDef.width > 0) && !skippedField) {
293
+ groupedColumnHeaders.push({
294
+ key: (columnDef.field || columnDef.id) as string,
295
+ title: groupedHeaderTitle || ''
296
+ });
297
+ }
298
+ });
299
+ }
300
+ return groupedColumnHeaders;
301
+ }
302
+
303
+ /**
304
+ * Get all header titles and their keys, translate the title when required.
305
+ * @param {Array<object>} columns of the grid
306
+ */
307
+ protected getColumnHeaders(columns: Column[]): Array<KeyTitlePair> {
308
+ const columnHeaders: Array<KeyTitlePair> = [];
309
+
310
+ if (columns && Array.isArray(columns)) {
311
+ // Populate the Column Header, pull the name defined
312
+ columns.forEach((columnDef) => {
313
+ let headerTitle = '';
314
+ if ((columnDef.nameKey || columnDef.nameKey) && this._gridOptions.enableTranslate && this._translaterService?.translate && this._translaterService?.getCurrentLanguage?.()) {
315
+ headerTitle = this._translaterService.translate((columnDef.nameKey || columnDef.nameKey));
316
+ } else {
317
+ headerTitle = getHtmlStringOutput(columnDef.name || '', 'innerHTML') || titleCase(columnDef.field);
318
+ }
319
+ const skippedField = columnDef.excludeFromExport || false;
320
+
321
+ // if column width is 0px, then we consider that field as a hidden field and should not be part of the export
322
+ if ((columnDef.width === undefined || columnDef.width > 0) && !skippedField) {
323
+ columnHeaders.push({
324
+ key: (columnDef.field || columnDef.id) as string,
325
+ title: headerTitle || ''
326
+ });
327
+ }
328
+ });
329
+ }
330
+ return columnHeaders;
331
+ }
332
+
333
+ /**
334
+ * Get the data of a regular row (a row without grouping)
335
+ * @param {Array<Object>} columns - column definitions
336
+ * @param {Number} row - row index
337
+ * @param {Object} itemObj - item datacontext object
338
+ */
339
+ protected readRegularRowData(columns: Column[], row: number, itemObj: any) {
340
+ let idx = 0;
341
+ const rowOutputStrings = [];
342
+ const exportQuoteWrapper = this._exportQuoteWrapper;
343
+ let prevColspan: number | string = 1;
344
+ const itemMetadata = this._dataView.getItemMetadata(row);
345
+
346
+ for (let col = 0, ln = columns.length; col < ln; col++) {
347
+ const columnDef = columns[col];
348
+
349
+ // skip excluded column
350
+ if (columnDef.excludeFromExport) {
351
+ continue;
352
+ }
353
+
354
+ // if we are grouping and are on 1st column index, we need to skip this column since it will be used later by the grouping text:: Group by [columnX]
355
+ if (this._hasGroupedItems && idx === 0) {
356
+ const emptyValue = this._fileFormat === FileType.csv ? `""` : '';
357
+ rowOutputStrings.push(emptyValue);
358
+ }
359
+
360
+ let colspanColumnId;
361
+ if (itemMetadata?.columns) {
362
+ const metadata = itemMetadata?.columns;
363
+ const columnData = metadata[columnDef.id] || metadata[col];
364
+ if (!((!isNaN(prevColspan as number) && +prevColspan > 1) || (prevColspan === '*' && col > 0))) {
365
+ prevColspan = columnData?.colspan ?? 1;
366
+ }
367
+ if (prevColspan !== '*') {
368
+ if (columnDef.id in metadata) {
369
+ colspanColumnId = columnDef.id;
370
+ }
371
+ }
372
+ }
373
+
374
+ if ((prevColspan === '*' && col > 0) || ((!isNaN(prevColspan as number) && +prevColspan > 1) && columnDef.id !== colspanColumnId)) {
375
+ rowOutputStrings.push('');
376
+ if ((!isNaN(prevColspan as number) && +prevColspan > 1)) {
377
+ (prevColspan as number)--;
378
+ }
379
+ } else {
380
+ // get the output by analyzing if we'll pull the value from the cell or from a formatter
381
+ let itemData = exportWithFormatterWhenDefined(row, col, columnDef, itemObj, this._grid, this._exportOptions);
382
+
383
+ // does the user want to sanitize the output data (remove HTML tags)?
384
+ if (columnDef.sanitizeDataExport || this._exportOptions.sanitizeDataExport) {
385
+ itemData = stripTags(itemData);
386
+ }
387
+
388
+ // when CSV we also need to escape double quotes twice, so " becomes ""
389
+ if (this._fileFormat === FileType.csv && itemData) {
390
+ itemData = itemData.toString().replace(/"/gi, `""`);
391
+ }
392
+
393
+ // do we have a wrapper to keep as a string? in certain cases like "1E06", we don't want excel to transform it into exponential (1.0E06)
394
+ // to cancel that effect we can had = in front, ex: ="1E06"
395
+ const keepAsStringWrapper = columnDef?.exportCsvForceToKeepAsString ? '=' : '';
396
+
397
+ rowOutputStrings.push(keepAsStringWrapper + exportQuoteWrapper + itemData + exportQuoteWrapper);
398
+ }
399
+
400
+ idx++;
401
+ }
402
+
403
+ return rowOutputStrings.join(this._delimiter);
404
+ }
405
+
406
+ /**
407
+ * Get the grouped title(s) and its group title formatter, for example if we grouped by salesRep, the returned result would be:: 'Sales Rep: John Dow (2 items)'
408
+ * @param itemObj
409
+ */
410
+ protected readGroupedTitleRow(itemObj: any) {
411
+ let groupName = stripTags(itemObj.title);
412
+ const exportQuoteWrapper = this._exportQuoteWrapper;
413
+
414
+ groupName = addWhiteSpaces(5 * itemObj.level) + groupName;
415
+
416
+ if (this._fileFormat === FileType.csv) {
417
+ // when CSV we also need to escape double quotes twice, so " becomes ""
418
+ groupName = groupName.toString().replace(/"/gi, `""`);
419
+ }
420
+
421
+ return exportQuoteWrapper + groupName + exportQuoteWrapper;
422
+ }
423
+
424
+ /**
425
+ * Get the grouped totals (below the regular rows), these are set by Slick Aggregators.
426
+ * For example if we grouped by "salesRep" and we have a Sum Aggregator on "sales", then the returned output would be:: ["Sum 123$"]
427
+ * @param itemObj
428
+ */
429
+ protected readGroupedTotalRow(columns: Column[], itemObj: any) {
430
+ const delimiter = this._exportOptions.delimiter;
431
+ const format = this._exportOptions.format;
432
+ const groupingAggregatorRowText = this._exportOptions.groupingAggregatorRowText || '';
433
+ const exportQuoteWrapper = this._exportQuoteWrapper;
434
+ const outputStrings = [`${exportQuoteWrapper}${groupingAggregatorRowText}${exportQuoteWrapper}`];
435
+
436
+ columns.forEach((columnDef) => {
437
+ let itemData = '';
438
+ const skippedField = columnDef.excludeFromExport || false;
439
+
440
+ // if there's a groupTotalsFormatter, we will re-run it to get the exact same output as what is shown in UI
441
+ if (columnDef.groupTotalsFormatter) {
442
+ const totalResult = columnDef.groupTotalsFormatter(itemObj, columnDef, this._grid);
443
+ itemData = totalResult instanceof HTMLElement ? totalResult.textContent || '' : totalResult;
444
+ }
445
+
446
+ // does the user want to sanitize the output data (remove HTML tags)?
447
+ if (columnDef.sanitizeDataExport || this._exportOptions.sanitizeDataExport) {
448
+ itemData = stripTags(itemData);
449
+ }
450
+
451
+ if (format === FileType.csv) {
452
+ // when CSV we also need to escape double quotes twice, so a double quote " becomes 2x double quotes ""
453
+ itemData = itemData.toString().replace(/"/gi, `""`);
454
+ }
455
+ // add the column (unless user wants to skip it)
456
+ if ((columnDef.width === undefined || columnDef.width > 0) && !skippedField) {
457
+ outputStrings.push(exportQuoteWrapper + itemData + exportQuoteWrapper);
458
+ }
459
+ });
460
+
461
+ return outputStrings.join(delimiter);
462
+ }
463
+ }