@opencorestack/opengridx 0.1.5 → 0.1.7

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.
Files changed (130) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +9 -0
  3. package/dist/index.d.ts +6 -0
  4. package/dist/opengridx.css +1 -1
  5. package/dist/opengridx.es.js +319 -319
  6. package/dist/opengridx.es.js.map +1 -1
  7. package/dist/opengridx.umd.js +7 -7
  8. package/dist/opengridx.umd.js.map +1 -1
  9. package/docs/API_REFERENCE.md +177 -0
  10. package/docs/README.md +60 -0
  11. package/docs/components/cell.md +44 -0
  12. package/docs/components/column-group-header.md +57 -0
  13. package/docs/components/column-resize.md +63 -0
  14. package/docs/components/column-visibility.md +61 -0
  15. package/docs/components/datagrid.md +70 -0
  16. package/docs/components/filter-panel.md +26 -0
  17. package/docs/components/header.md +39 -0
  18. package/docs/components/pagination.md +34 -0
  19. package/docs/components/row.md +31 -0
  20. package/docs/components/toolbar.md +40 -0
  21. package/docs/components/tooltip.md +68 -0
  22. package/docs/customization/slots-api.md +227 -0
  23. package/docs/customization/theming.md +293 -0
  24. package/docs/features/aggregation-pivot.md +79 -0
  25. package/docs/features/cell-spanning.md +520 -0
  26. package/docs/features/clipboard.md +76 -0
  27. package/docs/features/custom-pagination.md +143 -0
  28. package/docs/features/data-source.md +72 -0
  29. package/docs/features/editing-reordering.md +99 -0
  30. package/docs/features/export-guide.md +434 -0
  31. package/docs/features/filtering.md +83 -0
  32. package/docs/features/infinite-scroll.md +87 -0
  33. package/docs/features/list-view.md +65 -0
  34. package/docs/features/loading-states.md +68 -0
  35. package/docs/features/master-detail.md +46 -0
  36. package/docs/features/pinning.md +67 -0
  37. package/docs/features/selection.md +70 -0
  38. package/docs/features/sorting-pagination.md +66 -0
  39. package/docs/features/state-persistence.md +67 -0
  40. package/docs/features/tree-data-grouping.md +67 -0
  41. package/docs/features/virtualization.md +188 -0
  42. package/docs/roadmap.md +71 -0
  43. package/lib/components/Cell/Cell.css +119 -0
  44. package/lib/components/Cell/Cell.tsx +264 -0
  45. package/lib/components/Cell/GridEditInputCell.css +62 -0
  46. package/lib/components/Cell/GridEditInputCell.tsx +152 -0
  47. package/lib/components/ColumnGroupHeader/ColumnGroupHeader.css +67 -0
  48. package/lib/components/ColumnGroupHeader/ColumnGroupHeader.tsx +220 -0
  49. package/lib/components/ColumnResizeHandle/ColumnResizeHandle.css +31 -0
  50. package/lib/components/ColumnResizeHandle/ColumnResizeHandle.tsx +85 -0
  51. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.css +137 -0
  52. package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx +190 -0
  53. package/lib/components/DataGrid/DataGrid.css +224 -0
  54. package/lib/components/DataGrid/DataGrid.tsx +2253 -0
  55. package/lib/components/DetailPanel/DetailPanel.css +36 -0
  56. package/lib/components/DetailPanel/DetailPanel.tsx +34 -0
  57. package/lib/components/FilterPanel/FilterPanel.css +196 -0
  58. package/lib/components/FilterPanel/FilterPanel.tsx +183 -0
  59. package/lib/components/FilterPanel/index.ts +1 -0
  60. package/lib/components/Header/ColumnMenu.css +68 -0
  61. package/lib/components/Header/ColumnMenu.tsx +249 -0
  62. package/lib/components/Header/Header.css +224 -0
  63. package/lib/components/Header/Header.tsx +516 -0
  64. package/lib/components/ListView/ListView.css +112 -0
  65. package/lib/components/ListView/ListViewRow.tsx +63 -0
  66. package/lib/components/Pagination/Pagination.css +91 -0
  67. package/lib/components/Pagination/Pagination.tsx +151 -0
  68. package/lib/components/QuickFilter/QuickFilter.css +23 -0
  69. package/lib/components/QuickFilter/QuickFilter.tsx +79 -0
  70. package/lib/components/Row/Row.css +126 -0
  71. package/lib/components/Row/Row.tsx +377 -0
  72. package/lib/components/Skeleton/Skeleton.css +55 -0
  73. package/lib/components/Skeleton/Skeleton.tsx +22 -0
  74. package/lib/components/SkeletonRow/SkeletonRow.css +32 -0
  75. package/lib/components/SkeletonRow/SkeletonRow.tsx +89 -0
  76. package/lib/components/SkeletonRow/index.ts +1 -0
  77. package/lib/components/Toolbar/GlobalSearch.css +113 -0
  78. package/lib/components/Toolbar/GlobalSearch.tsx +120 -0
  79. package/lib/components/Toolbar/GridToolbar.css +204 -0
  80. package/lib/components/Toolbar/GridToolbar.tsx +647 -0
  81. package/lib/components/Toolbar/PivotPanel.css +212 -0
  82. package/lib/components/Toolbar/PivotPanel.tsx +239 -0
  83. package/lib/components/Tooltip/Tooltip.css +59 -0
  84. package/lib/components/Tooltip/Tooltip.tsx +106 -0
  85. package/lib/components/ui/Button.css +89 -0
  86. package/lib/components/ui/Button.tsx +39 -0
  87. package/lib/components/ui/Checkbox.css +71 -0
  88. package/lib/components/ui/Checkbox.tsx +66 -0
  89. package/lib/components/ui/DragHandleIcon.css +19 -0
  90. package/lib/components/ui/DragHandleIcon.tsx +18 -0
  91. package/lib/components/ui/ExpandIcon.css +38 -0
  92. package/lib/components/ui/ExpandIcon.tsx +75 -0
  93. package/lib/components/ui/Input.css +59 -0
  94. package/lib/components/ui/Input.tsx +45 -0
  95. package/lib/components/ui/index.ts +15 -0
  96. package/lib/hooks/core/useDataGrid.ts +392 -0
  97. package/lib/hooks/core/useGridApiRef.ts +10 -0
  98. package/lib/hooks/features/useAggregation.ts +143 -0
  99. package/lib/hooks/features/useGridClipboard.ts +133 -0
  100. package/lib/hooks/features/useGridDataSource.ts +190 -0
  101. package/lib/hooks/features/useGridEditing.ts +100 -0
  102. package/lib/hooks/features/useGridSpanning.ts +349 -0
  103. package/lib/hooks/features/usePivot.ts +149 -0
  104. package/lib/hooks/useColumnReorder.ts +114 -0
  105. package/lib/hooks/useRowGrouping.ts +302 -0
  106. package/lib/hooks/useRowReorder.ts +83 -0
  107. package/lib/hooks/useTreeData.ts +323 -0
  108. package/lib/index.ts +106 -0
  109. package/lib/state/index.ts +17 -0
  110. package/lib/state/types.ts +48 -0
  111. package/lib/state/useGridStateStorage.ts +113 -0
  112. package/lib/styles/base/reset.css +21 -0
  113. package/lib/styles/base/variables.css +161 -0
  114. package/lib/styles/opengridx.css +27 -0
  115. package/lib/styles/opengridx.css.d.ts +4 -0
  116. package/lib/theme/DataGridThemeProvider.tsx +276 -0
  117. package/lib/theme/defaultStyles.ts +0 -0
  118. package/lib/theme/index.ts +21 -0
  119. package/lib/theme/presets.ts +148 -0
  120. package/lib/theme/types.ts +155 -0
  121. package/lib/types/index.ts +764 -0
  122. package/lib/utils/export/exportToExcelAdvanced.ts +516 -0
  123. package/lib/utils/export/index.ts +550 -0
  124. package/lib/utils/filtering/index.ts +240 -0
  125. package/lib/utils/pinning/index.ts +282 -0
  126. package/lib/utils/sorting/index.ts +55 -0
  127. package/lib/utils/virtualization/index.ts +106 -0
  128. package/lib/vite-env.d.ts +1 -0
  129. package/llms.txt +12 -2
  130. package/package.json +13 -4
@@ -0,0 +1,550 @@
1
+
2
+ import type { GridColDef, GridRowModel, GridAggregationModel } from '../../types';
3
+ import { formatAggregationValue } from '../../hooks/features/useAggregation';
4
+
5
+ export interface CsvExportOptions {
6
+ fileName?: string;
7
+ includeHeaders?: boolean;
8
+ delimiter?: string;
9
+ selectedRows?: (string | number)[];
10
+ aggregationResult?: Record<string, any> | null;
11
+ aggregationModel?: GridAggregationModel | null;
12
+ }
13
+
14
+ export function exportToCsv<R extends GridRowModel>(
15
+ rows: R[],
16
+ columns: GridColDef<R>[],
17
+ options: CsvExportOptions = {}
18
+ ): void {
19
+ const {
20
+ fileName = 'export.csv',
21
+ includeHeaders = true,
22
+ delimiter = ',',
23
+ selectedRows
24
+ } = options;
25
+
26
+ const rowsToExport = selectedRows && selectedRows.length > 0
27
+ ? rows.filter(r => selectedRows.includes(r.id))
28
+ : rows;
29
+
30
+ if (rowsToExport.length === 0) {
31
+ console.warn('No rows to export');
32
+ return;
33
+ }
34
+
35
+ const exportColumns = columns.filter(col => {
36
+ if (col.exportable === false) return false;
37
+ if (col.field === '__check__' || col.field === '__actions__') return false;
38
+ return true;
39
+ });
40
+
41
+ let csvContent = '';
42
+
43
+ // Add headers
44
+ if (includeHeaders) {
45
+ const headers = exportColumns.map(col => escapeCSV(col.headerName || col.field));
46
+ csvContent += headers.join(delimiter) + '\n';
47
+ }
48
+
49
+ rowsToExport.forEach(row => {
50
+ const values = exportColumns.map(col => {
51
+ let value: any = row[col.field as keyof R];
52
+
53
+ if (col.valueGetter) {
54
+ value = col.valueGetter({ row, field: col.field, value });
55
+ }
56
+
57
+ if (col.valueFormatter && value !== undefined && value !== null) {
58
+ value = col.valueFormatter({ value, row, field: col.field });
59
+ }
60
+
61
+ return escapeCSV(String(value ?? ''));
62
+ });
63
+ csvContent += values.join(delimiter) + '\n';
64
+ });
65
+
66
+ // Add Aggregation
67
+ if (options.aggregationResult) {
68
+ const aggResult = options.aggregationResult;
69
+ const aggModel = options.aggregationModel || {};
70
+
71
+ // Print Aggregation Function Labels
72
+ const aggLabels = exportColumns.map((col) => {
73
+ if (aggModel[col.field]) {
74
+ return escapeCSV(aggModel[col.field].toUpperCase());
75
+ }
76
+ return escapeCSV('');
77
+ });
78
+ csvContent += aggLabels.join(delimiter) + '\n';
79
+
80
+ // Print Aggregation Values
81
+ const aggValues = exportColumns.map((col) => {
82
+ const aggVal = aggResult[col.field];
83
+ if (aggVal !== undefined && aggVal !== null) {
84
+ let formatted = aggVal;
85
+ if (col.valueFormatter) {
86
+ formatted = col.valueFormatter({ value: aggVal, row: {} as R, field: col.field });
87
+ } else if (aggModel[col.field]) {
88
+ formatted = formatAggregationValue(aggVal, aggModel[col.field]);
89
+ }
90
+ return escapeCSV(String(formatted));
91
+ }
92
+ return escapeCSV('');
93
+ });
94
+ csvContent += aggValues.join(delimiter) + '\n';
95
+ }
96
+
97
+ downloadFile(csvContent, fileName, 'text/csv;charset=utf-8;');
98
+ }
99
+
100
+ function escapeCSV(value: string): string {
101
+ if (value.includes(',') || value.includes('"') || value.includes('\n')) {
102
+ return `"${value.replace(/"/g, '""')}"`;
103
+ }
104
+ return value;
105
+ }
106
+
107
+ export interface ExcelExportOptions {
108
+ fileName?: string;
109
+ sheetName?: string;
110
+ includeHeaders?: boolean;
111
+ selectedRows?: (string | number)[];
112
+ aggregationResult?: Record<string, any> | null;
113
+ aggregationModel?: GridAggregationModel | null;
114
+ }
115
+
116
+ export function exportToExcel<R extends GridRowModel>(
117
+ rows: R[],
118
+ columns: GridColDef<R>[],
119
+ options: ExcelExportOptions = {}
120
+ ): void {
121
+ const {
122
+ fileName = 'export.xlsx',
123
+ sheetName = 'Sheet1',
124
+ includeHeaders = true,
125
+ selectedRows
126
+ } = options;
127
+
128
+ const rowsToExport = selectedRows && selectedRows.length > 0
129
+ ? rows.filter(r => selectedRows.includes(r.id))
130
+ : rows;
131
+
132
+ if (rowsToExport.length === 0) {
133
+ console.warn('No rows to export');
134
+ return;
135
+ }
136
+
137
+ const exportColumns = columns.filter(col => {
138
+ if (col.exportable === false) return false;
139
+ if (col.field === '__check__' || col.field === '__actions__') return false;
140
+ return true;
141
+ });
142
+
143
+ let html = '<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel" xmlns="http://www.w3.org/TR/REC-html40">';
144
+ html += '<head><!--[if gte mso 9]><xml><x:ExcelWorkbook><x:ExcelWorksheets><x:ExcelWorksheet>';
145
+ html += `<x:Name>${sheetName}</x:Name>`;
146
+ html += '<x:WorksheetOptions><x:DisplayGridlines/></x:WorksheetOptions></x:ExcelWorksheet></x:ExcelWorksheets></x:ExcelWorkbook></xml><![endif]-->';
147
+ html += '<meta charset="UTF-8"></head><body>';
148
+ html += '<table border="1">';
149
+
150
+ if (includeHeaders) {
151
+ html += '<thead><tr>';
152
+ exportColumns.forEach(col => {
153
+ html += `<th>${escapeHTML(col.headerName || col.field)}</th>`;
154
+ });
155
+ html += '</tr></thead>';
156
+ }
157
+
158
+ html += '<tbody>';
159
+ rowsToExport.forEach(row => {
160
+ html += '<tr>';
161
+ exportColumns.forEach(col => {
162
+ let value: any = row[col.field as keyof R];
163
+
164
+ if (col.valueGetter) {
165
+ value = col.valueGetter({ row, field: col.field, value });
166
+ }
167
+
168
+ if (col.valueFormatter && value !== undefined && value !== null) {
169
+ value = col.valueFormatter({ value, row, field: col.field });
170
+ }
171
+
172
+ html += `<td>${escapeHTML(String(value ?? ''))}</td>`;
173
+ });
174
+ });
175
+
176
+ if (options.aggregationResult) {
177
+ const aggResult = options.aggregationResult;
178
+ const aggModel = options.aggregationModel || {};
179
+
180
+ html += '<tr style="font-size: 11px; color: #666; background-color: #f5f5f5;">';
181
+ exportColumns.forEach((col) => {
182
+ if (aggModel[col.field]) {
183
+ html += `<td style="text-align: right; font-weight: bold;">${escapeHTML(aggModel[col.field].toUpperCase())}</td>`;
184
+ } else {
185
+ html += `<td></td>`;
186
+ }
187
+ });
188
+ html += '</tr>';
189
+
190
+ html += '<tr style="font-weight: bold; background-color: #f5f5f5;">';
191
+ exportColumns.forEach((col) => {
192
+ const aggVal = aggResult[col.field];
193
+ if (aggVal !== undefined && aggVal !== null) {
194
+ let formatted = aggVal;
195
+ if (col.valueFormatter) {
196
+ formatted = col.valueFormatter({ value: aggVal, row: {} as R, field: col.field });
197
+ } else if (aggModel[col.field]) {
198
+ formatted = formatAggregationValue(aggVal, aggModel[col.field]);
199
+ }
200
+ html += `<td style="text-align: right;">${escapeHTML(String(formatted))}</td>`;
201
+ } else {
202
+ html += `<td></td>`;
203
+ }
204
+ });
205
+ html += '</tr>';
206
+ }
207
+
208
+ html += '</tbody></table></body></html>';
209
+
210
+ downloadFile(html, fileName, 'application/vnd.ms-excel');
211
+ }
212
+
213
+ function escapeHTML(value: string): string {
214
+ return value
215
+ .replace(/&/g, '&amp;')
216
+ .replace(/</g, '&lt;')
217
+ .replace(/>/g, '&gt;')
218
+ .replace(/"/g, '&quot;')
219
+ .replace(/'/g, '&#039;');
220
+ }
221
+
222
+ // ============================================================================
223
+ // JSON Export
224
+ // ============================================================================
225
+
226
+ export interface JsonExportOptions {
227
+ fileName?: string;
228
+ pretty?: boolean;
229
+ selectedRows?: (string | number)[];
230
+ aggregationResult?: Record<string, any> | null;
231
+ aggregationModel?: GridAggregationModel | null;
232
+ }
233
+
234
+ /**
235
+ * Export data to JSON format
236
+ */
237
+ export function exportToJson<R extends GridRowModel>(
238
+ rows: R[],
239
+ columns: GridColDef<R>[],
240
+ options: JsonExportOptions = {}
241
+ ): void {
242
+ const {
243
+ fileName = 'export.json',
244
+ pretty = true,
245
+ selectedRows
246
+ } = options;
247
+
248
+ // Filter rows if selection is active
249
+ const rowsToExport = selectedRows && selectedRows.length > 0
250
+ ? rows.filter(r => selectedRows.includes(r.id))
251
+ : rows;
252
+
253
+ if (rowsToExport.length === 0) {
254
+ console.warn('No rows to export');
255
+ return;
256
+ }
257
+
258
+ // Filter columns
259
+ const exportColumns = columns.filter(col => {
260
+ if (col.exportable === false) return false;
261
+ if (col.field === '__check__' || col.field === '__actions__') return false;
262
+ return true;
263
+ });
264
+
265
+ // Build JSON data
266
+ const data = rowsToExport.map(row => {
267
+ const obj: any = {};
268
+ exportColumns.forEach(col => {
269
+ let value: any = row[col.field as keyof R];
270
+
271
+ // Use valueGetter if available
272
+ if (col.valueGetter) {
273
+ value = col.valueGetter({ row, field: col.field, value });
274
+ }
275
+
276
+ obj[col.field] = value;
277
+ });
278
+ return obj;
279
+ });
280
+
281
+ let outData: any = data;
282
+
283
+ if (options.aggregationResult) {
284
+ const aggResult = options.aggregationResult;
285
+ const aggModel = options.aggregationModel || {};
286
+
287
+ const summaryLabelObj: any = {};
288
+ exportColumns.forEach((col) => {
289
+ if (aggModel[col.field]) {
290
+ summaryLabelObj[col.field] = aggModel[col.field].toUpperCase();
291
+ }
292
+ });
293
+
294
+ const summaryDataObj: any = {};
295
+ exportColumns.forEach((col) => {
296
+ if (aggResult[col.field] !== undefined && aggResult[col.field] !== null) {
297
+ let aggVal = aggResult[col.field];
298
+ if (col.valueFormatter) {
299
+ aggVal = col.valueFormatter({ value: aggVal, row: {} as R, field: col.field });
300
+ } else if (aggModel[col.field]) {
301
+ aggVal = formatAggregationValue(aggVal, aggModel[col.field]);
302
+ }
303
+ summaryDataObj[col.field] = aggVal;
304
+ }
305
+ });
306
+
307
+ outData = {
308
+ data: data,
309
+ aggregation: {
310
+ labels: summaryLabelObj,
311
+ values: summaryDataObj
312
+ }
313
+ };
314
+ }
315
+
316
+ const jsonContent = pretty ? JSON.stringify(outData, null, 2) : JSON.stringify(outData);
317
+
318
+ // Download file
319
+ downloadFile(jsonContent, fileName, 'application/json;charset=utf-8;');
320
+ }
321
+
322
+ // ============================================================================
323
+ // Print
324
+ // ============================================================================
325
+
326
+ export interface PrintOptions {
327
+ title?: string;
328
+ selectedRows?: (string | number)[];
329
+ aggregationResult?: Record<string, any> | null;
330
+ aggregationModel?: GridAggregationModel | null;
331
+ }
332
+
333
+ /**
334
+ * Open print dialog with formatted table
335
+ */
336
+ export async function printGrid<R extends GridRowModel>(
337
+ rows: R[],
338
+ columns: GridColDef<R>[],
339
+ titleOrOptions?: string | PrintOptions
340
+ ): Promise<void> {
341
+ // 1. Open window immediately to resolve user activation constraints and provide feedback
342
+ const printWindow = window.open('', '_blank');
343
+
344
+ if (!printWindow) {
345
+ console.error('Failed to open print window. Please check your popup blocker settings.');
346
+ return;
347
+ }
348
+
349
+ // 2. Show loading state
350
+ printWindow.document.write(`
351
+ <!DOCTYPE html>
352
+ <html>
353
+ <head>
354
+ <title>Preparing Print...</title>
355
+ <style>
356
+ body { font-family: sans-serif; height: 100vh; display: flex; align-items: center; justify-content: center; margin: 0; background: #f5f5f5; }
357
+ .loader { text-align: center; }
358
+ .spinner { border: 4px solid #f3f3f3; border-top: 4px solid #3498db; border-radius: 50%; width: 40px; height: 40px; animation: spin 1s linear infinite; margin: 0 auto 20px; }
359
+ @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
360
+ </style>
361
+ </head>
362
+ <body>
363
+ <div class="loader">
364
+ <div class="spinner"></div>
365
+ <h2>Generating Print Preview...</h2>
366
+ <p>Processing data, please wait.</p>
367
+ </div>
368
+ </body>
369
+ </html>
370
+ `);
371
+ printWindow.document.close();
372
+
373
+ // 3. Yield to main thread to allow the new window to render its loading state
374
+ await new Promise(resolve => setTimeout(resolve, 100));
375
+
376
+ try {
377
+ let title = 'Print';
378
+ let selectedRows: (string | number)[] | undefined;
379
+ let aggregationResult: Record<string, any> | null = null;
380
+ let aggregationModel: GridAggregationModel | null = null;
381
+
382
+ if (typeof titleOrOptions === 'string') {
383
+ title = titleOrOptions;
384
+ } else if (typeof titleOrOptions === 'object') {
385
+ title = titleOrOptions.title || 'Print';
386
+ selectedRows = titleOrOptions.selectedRows;
387
+ aggregationResult = titleOrOptions.aggregationResult || null;
388
+ aggregationModel = titleOrOptions.aggregationModel || null;
389
+ }
390
+
391
+ // Filter rows if selection is active
392
+ const rowsToExport = selectedRows && selectedRows.length > 0
393
+ ? rows.filter(r => selectedRows.includes(r.id))
394
+ : rows;
395
+
396
+ if (rowsToExport.length === 0) {
397
+ printWindow.document.body.innerHTML = '<h3>No rows to export</h3>';
398
+ return;
399
+ }
400
+
401
+ // Filter columns
402
+ const exportColumns = columns.filter(col => {
403
+ if (col.exportable === false) return false;
404
+ if (col.field === '__check__' || col.field === '__actions__') return false;
405
+ return true;
406
+ });
407
+
408
+ // Build HTML
409
+ let html = `
410
+ <!DOCTYPE html>
411
+ <html>
412
+ <head>
413
+ <title>${title || 'Print'}</title>
414
+ <style>
415
+ body { font-family: Arial, sans-serif; margin: 20px; }
416
+ h1 { font-size: 24px; margin-bottom: 20px; }
417
+ table { border-collapse: collapse; width: 100%; }
418
+ th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
419
+ th { background-color: #f5f5f5; font-weight: bold; }
420
+ tr:nth-child(even) { background-color: #f9f9f9; }
421
+ @media print {
422
+ body { margin: 0; }
423
+ @page { margin: 1cm; }
424
+ }
425
+ </style>
426
+ </head>
427
+ <body>
428
+ `;
429
+
430
+ if (title) {
431
+ html += `<h1>${escapeHTML(title)}</h1>`;
432
+ }
433
+
434
+ html += '<table>';
435
+
436
+ // Headers
437
+ html += '<thead><tr>';
438
+ exportColumns.forEach(col => {
439
+ html += `<th>${escapeHTML(col.headerName || col.field)}</th>`;
440
+ });
441
+ html += '</tr></thead>';
442
+
443
+ // Rows
444
+ html += '<tbody>';
445
+
446
+ // Process rows (potential bottleneck logic here)
447
+ rowsToExport.forEach(row => {
448
+ html += '<tr>';
449
+ exportColumns.forEach(col => {
450
+ let value: any = row[col.field as keyof R];
451
+
452
+ if (col.valueGetter) {
453
+ value = col.valueGetter({ row, field: col.field, value });
454
+ }
455
+
456
+ if (col.valueFormatter && value !== undefined && value !== null) {
457
+ value = col.valueFormatter({ value, row, field: col.field });
458
+ }
459
+
460
+ if (col.type === 'image' && value) {
461
+ html += `<td><div style="display:flex;justify-content:center;"><img src="${value}" alt="${col.headerName}" style="max-height: 40px; border-radius: 4px;"></div></td>`;
462
+ } else {
463
+ html += `<td>${escapeHTML(String(value ?? ''))}</td>`;
464
+ }
465
+ });
466
+ html += '</tr>';
467
+ });
468
+
469
+ if (aggregationResult) {
470
+ const aggModel = aggregationModel || {};
471
+ html += '<tfoot><tr style="font-size: 11px; color: #666; background-color: #f5f5f5;">';
472
+ exportColumns.forEach((col) => {
473
+ if (aggModel[col.field]) {
474
+ html += `<td style="text-align: right; font-weight: bold;">${escapeHTML(aggModel[col.field].toUpperCase())}</td>`;
475
+ } else {
476
+ html += `<td></td>`;
477
+ }
478
+ });
479
+ html += '</tr><tr style="font-weight: bold; background-color: #f5f5f5;">';
480
+ exportColumns.forEach((col) => {
481
+ const aggVal = aggregationResult![col.field];
482
+ if (aggVal !== undefined && aggVal !== null) {
483
+ let formatted = aggVal;
484
+ if (col.valueFormatter) {
485
+ formatted = col.valueFormatter({ value: aggVal, row: {} as R, field: col.field });
486
+ } else if (aggModel[col.field]) {
487
+ formatted = formatAggregationValue(aggVal, aggModel[col.field]);
488
+ }
489
+ html += `<td style="text-align: right;">${escapeHTML(String(formatted))}</td>`;
490
+ } else {
491
+ html += `<td></td>`;
492
+ }
493
+ });
494
+ html += '</tr></tfoot>';
495
+ }
496
+
497
+ html += '</tbody></table></body></html>';
498
+
499
+ // Write the HTML content
500
+ printWindow.document.open();
501
+ printWindow.document.write(html);
502
+ printWindow.document.close();
503
+
504
+ // Set the document title explicitly (fixes about:blank issue)
505
+ printWindow.document.title = title || 'Print Preview';
506
+
507
+ printWindow.focus();
508
+
509
+ let hasPrinted = false;
510
+
511
+ printWindow.onload = () => {
512
+ if (!hasPrinted) {
513
+ hasPrinted = true;
514
+ printWindow.print();
515
+
516
+ setTimeout(() => {
517
+ printWindow.close();
518
+ }, 100);
519
+ }
520
+ };
521
+
522
+ setTimeout(() => {
523
+ if (!hasPrinted && printWindow && !printWindow.closed) {
524
+ hasPrinted = true;
525
+ printWindow.print();
526
+ setTimeout(() => {
527
+ if (!printWindow.closed) printWindow.close();
528
+ }, 100);
529
+ }
530
+ }, 500);
531
+
532
+ } catch (e: any) {
533
+ console.error('Print generation failed:', e);
534
+ if (printWindow && !printWindow.closed) {
535
+ printWindow.document.body.innerHTML = `<div style="color: red; padding: 20px;"><h3>Error Generating Print Preview</h3><p>${e.message}</p></div>`;
536
+ }
537
+ }
538
+ }
539
+
540
+ function downloadFile(content: string, fileName: string, mimeType: string): void {
541
+ const blob = new Blob([content], { type: mimeType });
542
+ const url = URL.createObjectURL(blob);
543
+ const link = document.createElement('a');
544
+ link.href = url;
545
+ link.download = fileName;
546
+ document.body.appendChild(link);
547
+ link.click();
548
+ document.body.removeChild(link);
549
+ URL.revokeObjectURL(url);
550
+ }