@opencorestack/opengridx 3.1.0 → 3.2.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.
- package/CHANGELOG.md +21 -0
- package/README.md +17 -1
- package/dist/index.d.ts +28 -0
- package/dist/opengridx.es.js +3564 -3509
- package/dist/opengridx.es.js.map +1 -1
- package/dist/opengridx.umd.js +13 -13
- package/dist/opengridx.umd.js.map +1 -1
- package/docs/API_REFERENCE.md +14 -0
- package/docs/features/pdf-export.md +38 -2
- package/lib/components/Cell/GridEditInputCell.tsx +3 -0
- package/lib/index.ts +1 -0
- package/lib/types/index.ts +31 -0
- package/lib/utils/export/exportToPdf.ts +167 -21
- package/lib/utils/export/index.ts +1 -1
- package/llms.txt +4 -1
- package/package.json +4 -2
package/docs/API_REFERENCE.md
CHANGED
|
@@ -1446,6 +1446,15 @@ interface PdfExportOptions {
|
|
|
1446
1446
|
headerTextColor?: string;
|
|
1447
1447
|
fontSize?: number;
|
|
1448
1448
|
font?: { name: string; data: string; boldData?: string }; // v3.0+
|
|
1449
|
+
onProgress?: (p: PdfExportProgress) => void; // v3.2+
|
|
1450
|
+
signal?: AbortSignal; // v3.2+
|
|
1451
|
+
maxRows?: number; // v3.2+, default 20000
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
interface PdfExportProgress {
|
|
1455
|
+
phase: 'prepare' | 'render' | 'save';
|
|
1456
|
+
done: number;
|
|
1457
|
+
total: number;
|
|
1449
1458
|
}
|
|
1450
1459
|
```
|
|
1451
1460
|
|
|
@@ -1466,3 +1475,8 @@ interface PdfExportOptions {
|
|
|
1466
1475
|
| `headerTextColor` | `string` | `'#ffffff'` | Column header cell text color (`#rrggbb` / `#rgb`) |
|
|
1467
1476
|
| `fontSize` | `number` | `9` | Body cell font size in points |
|
|
1468
1477
|
| `font` | `{ name, data, boldData? }` | — | Base64 TrueType font for text outside Latin-1 (₹, CJK, Cyrillic…). Without it, such characters are replaced with `?` (v3.0+) |
|
|
1478
|
+
| `onProgress` | `(p: PdfExportProgress) => void` | — | Called per slice with `{ phase, done, total }`; phases `prepare` → `render` → `save`, `done` monotonic per phase and ending at `total` (v3.2+) |
|
|
1479
|
+
| `signal` | `AbortSignal` | — | Abort → the promise rejects with `DOMException` `'AbortError'`, no file saved. The final jsPDF save step cannot be interrupted (v3.2+) |
|
|
1480
|
+
| `maxRows` | `number` | `20000` | Above this many table rows a dev-mode `console.warn` recommends `exportToCsv` / `exportToExcelAdvanced`. Never throws; `Infinity` silences it (v3.2+) |
|
|
1481
|
+
|
|
1482
|
+
The export yields to the event loop between ~30 ms slices while formatting and drawing (v3.2+), so the page stays responsive; only jsPDF's final save is one synchronous step. See [PDF Export › Large exports](./features/pdf-export.md#large-exports).
|
|
@@ -16,7 +16,7 @@ npm install jspdf jspdf-autotable
|
|
|
16
16
|
> await exportToPdf(rows, columns, { fileName: 'invoices', font: { name: 'NotoSans', data: notoSansBase64 } });
|
|
17
17
|
> ```
|
|
18
18
|
|
|
19
|
-
> **Large datasets:**
|
|
19
|
+
> **Large datasets:** time and file size grow with every row (50,000 grouped rows: several seconds and a file of 75–120 MB). Since v3.2 the export is sliced so the page stays responsive, and it reports progress and can be cancelled, but for more than a few thousand rows CSV (`exportToCsv`) or `.xlsx` (`exportToExcelAdvanced`) is still the better format. See [Large exports](#large-exports).
|
|
20
20
|
|
|
21
21
|
## Basic Usage
|
|
22
22
|
|
|
@@ -97,6 +97,42 @@ await exportToPdf(
|
|
|
97
97
|
| `headerTextColor` | `string` | `'#ffffff'` | Column header cell text color (`#rrggbb` or `#rgb`). Invalid values fall back to the default |
|
|
98
98
|
| `fontSize` | `number` | `9` | Body cell font size in points |
|
|
99
99
|
| `font` | `{ name: string; data: string; boldData?: string }` | — | A TrueType font (base64 `.ttf`) for text outside Latin-1 (v3.0+). See [Non-Latin text](#non-latin-text-and-unicode-fonts) |
|
|
100
|
+
| `onProgress` | `(p: PdfExportProgress) => void` | — | Progress: `{ phase: 'prepare' \| 'render' \| 'save', done, total }` (v3.2+). See [Large exports](#large-exports) |
|
|
101
|
+
| `signal` | `AbortSignal` | — | Cancels the export: the promise rejects with a `DOMException` named `'AbortError'` and nothing is saved (v3.2+) |
|
|
102
|
+
| `maxRows` | `number` | `20000` | Table-row count above which a development-mode warning recommends CSV / `.xlsx`. The export still runs; `Infinity` silences it (v3.2+) |
|
|
103
|
+
|
|
104
|
+
## Large exports
|
|
105
|
+
|
|
106
|
+
`exportToPdf` returns a `Promise<void>`. Since v3.2 it works in slices of about 30 ms and yields to the event loop between them, both while formatting the rows and while drawing the table, so the page keeps painting and handling input. Pass `onProgress` for a progress bar and `signal` for a Cancel button:
|
|
107
|
+
|
|
108
|
+
```tsx
|
|
109
|
+
const controllerRef = useRef<AbortController | null>(null);
|
|
110
|
+
const [progress, setProgress] = useState<PdfExportProgress | null>(null);
|
|
111
|
+
|
|
112
|
+
const onExport = async () => {
|
|
113
|
+
const controller = new AbortController();
|
|
114
|
+
controllerRef.current = controller;
|
|
115
|
+
try {
|
|
116
|
+
await exportToPdf(rows, columns, {
|
|
117
|
+
groupedRows: apiRef.current.getGroupedExportRows() ?? undefined,
|
|
118
|
+
signal: controller.signal,
|
|
119
|
+
onProgress: setProgress,
|
|
120
|
+
});
|
|
121
|
+
} catch (error) {
|
|
122
|
+
if (!(error instanceof DOMException && error.name === 'AbortError')) throw error;
|
|
123
|
+
} finally {
|
|
124
|
+
setProgress(null);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
// <button onClick={() => controllerRef.current?.abort()}>Cancel</button>
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
- **Phases.** `prepare` formats the rows (`total` = rows, or grouped entries with `groupedRows`); `render` draws the table (`total` = table body rows); `save` is reported as `0/1` before the file is written and `1/1` after. Within a phase `done` never decreases and ends at `total`.
|
|
131
|
+
- **Cancel.** When `signal` aborts, the promise rejects with `DOMException` `'AbortError'` at the next slice boundary and no file is saved. An already aborted signal rejects before jsPDF is loaded.
|
|
132
|
+
- **The last step still blocks.** jsPDF builds the whole file in one synchronous call when saving; that step cannot be split or cancelled. It is short next to the rest (about 0.4–0.6 s for 50,000 grouped rows in Chromium), but it grows with the file.
|
|
133
|
+
- **Same output.** The table is drawn as a series of `jspdf-autotable` calls that continue on the same page, with fixed column widths, the header repeated on every page and the footer only at the end. Pages, row positions, shading and page breaks are the same as a single call; the file is marginally larger (repeated style operators).
|
|
134
|
+
- **`maxRows` (default 20,000).** Above it a console warning in development recommends `exportToCsv` or `exportToExcelAdvanced`; nothing is thrown, so existing exports keep working. 20,000 rows is roughly where a PDF passes 30 MB and a few seconds to build, and where viewers start to struggle to open it. Pass a larger value (or `Infinity`) to silence the warning when a large PDF is intended.
|
|
135
|
+
- **Total time.** Slicing makes an export slightly slower overall, not faster. Formatting is a small part of the work; drawing the table dominates.
|
|
100
136
|
|
|
101
137
|
## Totals and filter summary
|
|
102
138
|
|
|
@@ -148,7 +184,7 @@ Pass the `font` option with a Unicode TrueType font. See [Non-Latin text](#non-l
|
|
|
148
184
|
|
|
149
185
|
**Export is slow or the file is huge**
|
|
150
186
|
|
|
151
|
-
PDF is not suited to tens of thousands of rows (see
|
|
187
|
+
PDF is not suited to tens of thousands of rows. Show progress and a Cancel button (see [Large exports](#large-exports)), or use CSV or `.xlsx` for large exports.
|
|
152
188
|
|
|
153
189
|
**Error: "exportToPdf requires 'jspdf' and 'jspdf-autotable'"**
|
|
154
190
|
|
|
@@ -91,6 +91,9 @@ function BooleanEditor({ value, colDef, onValueChange, onCommit, onCancel }: Gri
|
|
|
91
91
|
aria-label={editorLabel(colDef)}
|
|
92
92
|
checked={!!value}
|
|
93
93
|
onChange={handleChange}
|
|
94
|
+
// Safari does not focus checkboxes on click: mousedown moves focus to the cell, the
|
|
95
|
+
// blur commits the unchanged value and the editor unmounts before the click toggles it.
|
|
96
|
+
onMouseDown={(e) => e.preventDefault()}
|
|
94
97
|
onBlur={onCommit}
|
|
95
98
|
onKeyDown={makeKeyDownHandler(onCommit, onCancel, false)}
|
|
96
99
|
/>
|
package/lib/index.ts
CHANGED
package/lib/types/index.ts
CHANGED
|
@@ -527,6 +527,37 @@ export interface PdfExportOptions<R extends GridValidRowModel = GridRowModel> {
|
|
|
527
527
|
* Has no effect when the grid has no active `rowGroupingModel`, or when `selectedRows` is non-empty.
|
|
528
528
|
*/
|
|
529
529
|
groupedRows?: GridGroupedExportRow[];
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Called as the export advances: `prepare` (formatting rows; `total` = source rows or grouped
|
|
533
|
+
* entries), `render` (drawing table rows; `total` = table body rows) and `save` (`0/1` before the
|
|
534
|
+
* file is written, `1/1` after). `done` never decreases within a phase and ends at `total`.
|
|
535
|
+
* @since v3.2
|
|
536
|
+
*/
|
|
537
|
+
onProgress?: (progress: PdfExportProgress) => void;
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Cancels the export. When aborted, the returned promise rejects with a `DOMException` named
|
|
541
|
+
* `'AbortError'` and no file is saved. Checked between slices of work, so cancellation takes
|
|
542
|
+
* effect within one slice; the final save step cannot be interrupted once started.
|
|
543
|
+
* @since v3.2
|
|
544
|
+
*/
|
|
545
|
+
signal?: AbortSignal;
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Table-row count above which a development-mode warning recommends `exportToCsv` or
|
|
549
|
+
* `exportToExcelAdvanced`. The export still runs (nothing is thrown). Pass `Infinity` to silence it.
|
|
550
|
+
* Default: 20000
|
|
551
|
+
* @since v3.2
|
|
552
|
+
*/
|
|
553
|
+
maxRows?: number;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/** Progress reported by `PdfExportOptions.onProgress`. @since v3.2 */
|
|
557
|
+
export interface PdfExportProgress {
|
|
558
|
+
phase: 'prepare' | 'render' | 'save';
|
|
559
|
+
done: number;
|
|
560
|
+
total: number;
|
|
530
561
|
}
|
|
531
562
|
|
|
532
563
|
/**
|
|
@@ -57,8 +57,49 @@ interface AutoTableOptions {
|
|
|
57
57
|
showFoot?: string;
|
|
58
58
|
showHead?: string;
|
|
59
59
|
margin?: { top: number; left: number; right: number; bottom: number };
|
|
60
|
+
willDrawPage?: (data: AutoTableHookData) => void;
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
/** The part of jspdf-autotable's hook data used here; `settings` and `cursor` are the table's live objects. */
|
|
64
|
+
interface AutoTableHookData {
|
|
65
|
+
settings: { showHead: string };
|
|
66
|
+
cursor: { y: number } | null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
type AutoTableFn = (doc: JsPDFDoc, opts: AutoTableOptions) => void;
|
|
70
|
+
|
|
71
|
+
/** How the work is sliced. Output does not depend on these values; tests shrink them. */
|
|
72
|
+
export interface PdfExportTuning {
|
|
73
|
+
/** Longest stretch of synchronous work before yielding to the event loop, in ms. */
|
|
74
|
+
sliceMs: number;
|
|
75
|
+
/** Table rows drawn by the first autoTable call; later calls adapt to `sliceMs`. Rounded to even. */
|
|
76
|
+
renderChunkRows: number;
|
|
77
|
+
/** Resize later chunks to fit `sliceMs` (default behaviour); false keeps every chunk at `renderChunkRows`. */
|
|
78
|
+
adaptive: boolean;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const DEFAULT_TUNING: PdfExportTuning = { sliceMs: 30, renderChunkRows: 40, adaptive: true };
|
|
82
|
+
|
|
83
|
+
/** Default `maxRows`: above this a PDF is tens of MB and takes seconds to build and open. */
|
|
84
|
+
export const PDF_MAX_ROWS_DEFAULT = 20_000;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Let the browser paint and run other tasks. A macrotask on purpose: `scheduler.yield()` resumes
|
|
88
|
+
* ahead of queued tasks, which starves timers and React's scheduler (a progress bar would not
|
|
89
|
+
* update until the export finished).
|
|
90
|
+
*/
|
|
91
|
+
function yieldToEventLoop(): Promise<void> {
|
|
92
|
+
return new Promise(resolve => setTimeout(resolve, 0));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function abortError(): DOMException {
|
|
96
|
+
return new DOMException('PDF export was aborted', 'AbortError');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const now = (): number => (typeof performance !== 'undefined' ? performance.now() : Date.now());
|
|
100
|
+
|
|
101
|
+
const evenAtLeast2 = (n: number): number => Math.max(2, Math.floor(n / 2) * 2);
|
|
102
|
+
|
|
62
103
|
type Rgb = [number, number, number];
|
|
63
104
|
|
|
64
105
|
const DEFAULT_HEADER_BACKGROUND: Rgb = [79, 70, 229];
|
|
@@ -144,21 +185,37 @@ const LINE_HEIGHT_MM_PER_PT = 1.15 * 0.3528;
|
|
|
144
185
|
|
|
145
186
|
export function exportToPdf<R extends GridValidRowModel = GridRowModel>(rows: R[], columns: GridColDef<R>[], options?: PdfExportOptions<R>): Promise<void>;
|
|
146
187
|
export function exportToPdf<R extends GridValidRowModel = GridRowModel>(rows: R[], columns: GridColDef[], options?: PdfExportOptions<R>): Promise<void>;
|
|
147
|
-
export
|
|
188
|
+
export function exportToPdf<R extends GridRowModel>(
|
|
148
189
|
rows: R[],
|
|
149
190
|
columns: GridColDef<R>[],
|
|
150
191
|
options: PdfExportOptions<R> = {}
|
|
151
192
|
): Promise<void> {
|
|
193
|
+
return runPdfExport(rows, columns, options, DEFAULT_TUNING);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** `exportToPdf` with explicit slicing. Internal: not part of the public API. */
|
|
197
|
+
export async function runPdfExport<R extends GridRowModel>(
|
|
198
|
+
rows: R[],
|
|
199
|
+
columns: GridColDef<R>[],
|
|
200
|
+
options: PdfExportOptions<R>,
|
|
201
|
+
tuning: PdfExportTuning,
|
|
202
|
+
): Promise<void> {
|
|
203
|
+
const { signal, onProgress } = options;
|
|
204
|
+
const checkAbort = (): void => {
|
|
205
|
+
if (signal?.aborted) throw abortError();
|
|
206
|
+
};
|
|
207
|
+
checkAbort();
|
|
208
|
+
|
|
152
209
|
// Lazy-load peer deps — provides a clear error if not installed
|
|
153
210
|
let JsPDF: new (opts: Record<string, unknown>) => JsPDFDoc;
|
|
154
|
-
let autoTable:
|
|
211
|
+
let autoTable: AutoTableFn;
|
|
155
212
|
try {
|
|
156
213
|
const [jspdfMod, autoTableMod] = await Promise.all([
|
|
157
214
|
import('jspdf'),
|
|
158
215
|
import('jspdf-autotable'),
|
|
159
216
|
]);
|
|
160
217
|
JsPDF = jspdfMod.default as unknown as new (opts: Record<string, unknown>) => JsPDFDoc;
|
|
161
|
-
autoTable = autoTableMod.default as unknown as
|
|
218
|
+
autoTable = autoTableMod.default as unknown as AutoTableFn;
|
|
162
219
|
} catch {
|
|
163
220
|
throw new Error(
|
|
164
221
|
"exportToPdf requires 'jspdf' and 'jspdf-autotable'. Run: npm install jspdf jspdf-autotable"
|
|
@@ -181,7 +238,21 @@ export async function exportToPdf<R extends GridRowModel>(
|
|
|
181
238
|
fontSize = 9,
|
|
182
239
|
groupedRows,
|
|
183
240
|
font,
|
|
241
|
+
maxRows = PDF_MAX_ROWS_DEFAULT,
|
|
184
242
|
} = options;
|
|
243
|
+
checkAbort();
|
|
244
|
+
|
|
245
|
+
// Time-sliced work: `maybeYield` yields once the current slice has run for `sliceMs`.
|
|
246
|
+
let sliceStart = now();
|
|
247
|
+
const maybeYield = async (): Promise<void> => {
|
|
248
|
+
if (now() - sliceStart < tuning.sliceMs) return;
|
|
249
|
+
await yieldToEventLoop();
|
|
250
|
+
checkAbort();
|
|
251
|
+
sliceStart = now();
|
|
252
|
+
};
|
|
253
|
+
const report = (phase: 'prepare' | 'render' | 'save', done: number, total: number): void => {
|
|
254
|
+
onProgress?.({ phase, done, total });
|
|
255
|
+
};
|
|
185
256
|
|
|
186
257
|
const doc = new JsPDF({ orientation, unit: 'mm', format: 'a4' });
|
|
187
258
|
const pageWidth = doc.internal.pageSize.getWidth();
|
|
@@ -274,30 +345,48 @@ export async function exportToPdf<R extends GridRowModel>(
|
|
|
274
345
|
const aggregateTexts = (values: Record<string, unknown>): string[] =>
|
|
275
346
|
exportColumns.map(col => formatExportAggregate(col, aggMod[col.field], values[col.field], values));
|
|
276
347
|
|
|
277
|
-
|
|
348
|
+
const body: string[][] = [];
|
|
278
349
|
let foot: string[][] | undefined;
|
|
350
|
+
const pushRow = (cells: string[]): void => {
|
|
351
|
+
body.push(cells.map(pdfText));
|
|
352
|
+
};
|
|
279
353
|
|
|
280
354
|
if (useGrouped) {
|
|
281
355
|
// Grouped export: flatten the ordered list into body rows with indentation
|
|
282
|
-
|
|
283
|
-
|
|
356
|
+
const total = groupedRows.length;
|
|
357
|
+
report('prepare', 0, total);
|
|
358
|
+
for (let index = 0; index < total; index++) {
|
|
359
|
+
const entry: GridGroupedExportRow = groupedRows[index];
|
|
284
360
|
const indent = ' '.repeat(entry.depth * 2);
|
|
285
361
|
if (entry.type === 'group-header') {
|
|
286
362
|
const label = `${indent}${groupHeaderLabel(entry, columns)}`;
|
|
287
|
-
|
|
363
|
+
pushRow(exportColumns.map((_, i) => i === 0 ? label : ''));
|
|
288
364
|
} else if (entry.type === 'leaf' && entry.row) {
|
|
289
365
|
const row = entry.row as R;
|
|
290
|
-
|
|
366
|
+
pushRow(exportColumns.map((col, i) => (i === 0 ? indent : '') + resolveValue(row, col)));
|
|
291
367
|
} else if (entry.type === 'group-subtotal' && entry.aggregatedValues) {
|
|
292
368
|
const texts = aggregateTexts(entry.aggregatedValues);
|
|
293
|
-
|
|
369
|
+
pushRow(texts.map((text, i) => (i === 0 ? `${indent}${summaryLabelText('Subtotal', text)}` : text)));
|
|
294
370
|
} else if (entry.type === 'grand-total' && entry.aggregatedValues) {
|
|
295
371
|
const texts = aggregateTexts(entry.aggregatedValues);
|
|
296
|
-
foot = [texts.map((text, i) => (i === 0 ? summaryLabelText('Grand Total', text) : text))];
|
|
372
|
+
foot = [texts.map((text, i) => (i === 0 ? summaryLabelText('Grand Total', text) : text)).map(pdfText)];
|
|
297
373
|
}
|
|
298
|
-
|
|
374
|
+
if ((index & 63) === 63) {
|
|
375
|
+
report('prepare', index + 1, total);
|
|
376
|
+
await maybeYield();
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
report('prepare', total, total);
|
|
299
380
|
} else {
|
|
300
|
-
|
|
381
|
+
const total = rowsToExport.length;
|
|
382
|
+
report('prepare', 0, total);
|
|
383
|
+
for (let index = 0; index < total; index++) {
|
|
384
|
+
pushRow(exportColumns.map(col => resolveValue(rowsToExport[index], col)));
|
|
385
|
+
if ((index & 63) === 63) {
|
|
386
|
+
report('prepare', index + 1, total);
|
|
387
|
+
await maybeYield();
|
|
388
|
+
}
|
|
389
|
+
}
|
|
301
390
|
|
|
302
391
|
const totals = aggregationModel
|
|
303
392
|
? aggregationForExport(rowsToExport, hasSelection(selectedRows), columns, aggregationResult, aggregationModel)
|
|
@@ -305,12 +394,19 @@ export async function exportToPdf<R extends GridRowModel>(
|
|
|
305
394
|
if (totals && aggregationModel) {
|
|
306
395
|
const hasAgg = exportColumns.some(c => aggregationModel[c.field]);
|
|
307
396
|
const texts = aggregateTexts(totals);
|
|
308
|
-
foot = [texts.map((text, i) => (i === 0 && hasAgg ? summaryLabelText('TOTAL', text) : text))];
|
|
397
|
+
foot = [texts.map((text, i) => (i === 0 && hasAgg ? summaryLabelText('TOTAL', text) : text)).map(pdfText)];
|
|
309
398
|
}
|
|
399
|
+
report('prepare', total, total);
|
|
310
400
|
}
|
|
401
|
+
checkAbort();
|
|
311
402
|
|
|
312
|
-
body
|
|
313
|
-
|
|
403
|
+
if (body.length > maxRows && process.env.NODE_ENV !== 'production') {
|
|
404
|
+
console.warn(
|
|
405
|
+
`[exportToPdf] Exporting ${body.length} table rows (maxRows: ${maxRows}). PDFs this large are slow to build ` +
|
|
406
|
+
'and open and can reach tens of MB. Prefer exportToCsv or exportToExcelAdvanced for full-dataset exports, ' +
|
|
407
|
+
'or pass a larger `maxRows` to silence this warning.'
|
|
408
|
+
);
|
|
409
|
+
}
|
|
314
410
|
|
|
315
411
|
if (replacedCharacters) {
|
|
316
412
|
console.warn(
|
|
@@ -325,11 +421,8 @@ export async function exportToPdf<R extends GridRowModel>(
|
|
|
325
421
|
const [hr, hg, hb] = hexToRgb(headerBackgroundColor, DEFAULT_HEADER_BACKGROUND);
|
|
326
422
|
const [tr, tg, tb] = hexToRgb(headerTextColor, DEFAULT_HEADER_TEXT);
|
|
327
423
|
|
|
328
|
-
|
|
424
|
+
const baseTableOptions: Omit<AutoTableOptions, 'body' | 'startY'> = {
|
|
329
425
|
head,
|
|
330
|
-
body,
|
|
331
|
-
...(foot ? { foot } : {}),
|
|
332
|
-
startY,
|
|
333
426
|
styles: { fontSize, cellPadding: 3, font: fontName, overflow: 'linebreak' },
|
|
334
427
|
headStyles: {
|
|
335
428
|
fillColor: [hr, hg, hb],
|
|
@@ -343,10 +436,59 @@ export async function exportToPdf<R extends GridRowModel>(
|
|
|
343
436
|
? { footStyles: { fillColor: [241, 245, 249], fontStyle: 'bold', textColor: [0, 0, 0] } }
|
|
344
437
|
: {}),
|
|
345
438
|
columnStyles,
|
|
346
|
-
showFoot: foot ? 'lastPage' : 'never',
|
|
347
439
|
showHead: 'everyPage',
|
|
348
440
|
margin: { top: MARGIN, left: MARGIN, right: MARGIN, bottom: MARGIN + 6 },
|
|
349
|
-
}
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
// The body is drawn as a series of autoTable calls, each continuing where the previous one
|
|
444
|
+
// ended, so the drawing phase yields too. Column widths are fixed (columnStyles), chunks hold
|
|
445
|
+
// an even number of rows (alternate shading keeps its parity), only the last chunk carries the
|
|
446
|
+
// footer, and a continuation chunk skips the header on the page it starts on but repeats it on
|
|
447
|
+
// every page it adds: the same pages a single call would produce.
|
|
448
|
+
const totalRows = body.length;
|
|
449
|
+
report('render', 0, totalRows);
|
|
450
|
+
let drawn = 0;
|
|
451
|
+
let chunkRows = evenAtLeast2(tuning.renderChunkRows);
|
|
452
|
+
do {
|
|
453
|
+
const chunk = body.slice(drawn, drawn + chunkRows);
|
|
454
|
+
const isFirst = drawn === 0;
|
|
455
|
+
const isLast = drawn + chunk.length >= totalRows;
|
|
456
|
+
const continueY = isFirst ? startY : doc.lastAutoTable.finalY;
|
|
457
|
+
let firstPageOfChunk = true;
|
|
458
|
+
const started = now();
|
|
459
|
+
autoTable(doc, {
|
|
460
|
+
...baseTableOptions,
|
|
461
|
+
body: chunk,
|
|
462
|
+
...(isLast && foot ? { foot } : {}),
|
|
463
|
+
// A continuation starts at the top margin so autoTable's "does the table fit below
|
|
464
|
+
// startY" check never adds a page; the hook then moves the cursor to where the previous
|
|
465
|
+
// chunk ended, and each row breaks the page exactly as it would in a single table.
|
|
466
|
+
startY: isFirst ? startY : MARGIN,
|
|
467
|
+
showFoot: isLast && foot ? 'lastPage' : 'never',
|
|
468
|
+
...(isFirst ? {} : {
|
|
469
|
+
willDrawPage: (data: AutoTableHookData) => {
|
|
470
|
+
if (firstPageOfChunk) {
|
|
471
|
+
firstPageOfChunk = false;
|
|
472
|
+
// Same page as the previous chunk: its header is already drawn.
|
|
473
|
+
data.settings.showHead = 'never';
|
|
474
|
+
if (data.cursor) data.cursor.y = continueY;
|
|
475
|
+
} else {
|
|
476
|
+
data.settings.showHead = 'everyPage';
|
|
477
|
+
}
|
|
478
|
+
},
|
|
479
|
+
}),
|
|
480
|
+
});
|
|
481
|
+
drawn += chunk.length;
|
|
482
|
+
report('render', drawn, totalRows);
|
|
483
|
+
const elapsed = now() - started;
|
|
484
|
+
// Aim each call at one slice; grow at most 4x at a time.
|
|
485
|
+
if (tuning.adaptive) chunkRows = evenAtLeast2(elapsed > 0 ? Math.min(chunkRows * 4, chunkRows * (tuning.sliceMs / elapsed)) : chunkRows * 4);
|
|
486
|
+
if (!isLast) {
|
|
487
|
+
await yieldToEventLoop();
|
|
488
|
+
checkAbort();
|
|
489
|
+
sliceStart = now();
|
|
490
|
+
}
|
|
491
|
+
} while (drawn < totalRows);
|
|
350
492
|
|
|
351
493
|
// Two-pass page numbering — stamp all pages after the table is fully rendered
|
|
352
494
|
// so each stamp reads the correct final total (not the running count during draw)
|
|
@@ -361,7 +503,11 @@ export async function exportToPdf<R extends GridRowModel>(
|
|
|
361
503
|
pageHeight - 8,
|
|
362
504
|
{ align: 'right' }
|
|
363
505
|
);
|
|
506
|
+
await maybeYield();
|
|
364
507
|
}
|
|
365
508
|
|
|
509
|
+
checkAbort();
|
|
510
|
+
report('save', 0, 1);
|
|
366
511
|
doc.save(`${fileName}.pdf`);
|
|
512
|
+
report('save', 1, 1);
|
|
367
513
|
}
|
|
@@ -690,4 +690,4 @@ function downloadFile(content: string, fileName: string, mimeType: string): void
|
|
|
690
690
|
}
|
|
691
691
|
|
|
692
692
|
export { exportToPdf } from './exportToPdf';
|
|
693
|
-
export type { PdfExportOptions } from '../../types';
|
|
693
|
+
export type { PdfExportOptions, PdfExportProgress } from '../../types';
|
package/llms.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# OpenGridX — AI Agent Context
|
|
2
2
|
# Package: @opencorestack/opengridx
|
|
3
|
-
# Version: 3.1
|
|
3
|
+
# Version: 3.2.1
|
|
4
4
|
# Docs: https://opencorestack.github.io/OpenGridX/
|
|
5
5
|
# This file is machine-readable. AI developers: start here.
|
|
6
6
|
|
|
@@ -497,10 +497,13 @@ await exportToExcelAdvanced(rows, columns, {
|
|
|
497
497
|
});
|
|
498
498
|
exportToJson(rows, columns, { fileName: 'export.json' });
|
|
499
499
|
await exportToPdf(rows, columns, { fileName: 'export', title: 'My Report' });
|
|
500
|
+
// 3.2.0+: large PDFs — progress, cancel, size warning (never throws)
|
|
501
|
+
await exportToPdf(rows, columns, { onProgress: ({ phase, done, total }) => {}, signal: controller.signal, maxRows: 20000 });
|
|
500
502
|
await printGrid(rows, columns, 'My Report Title');
|
|
501
503
|
```
|
|
502
504
|
|
|
503
505
|
- Every export accepts `selectedRows`, `getRowId` (pass the grid's getRowId so ids match), `aggregationResult` + `aggregationModel` (totals row) and `groupedRows` from `apiRef.current.getGroupedExportRows()`.
|
|
506
|
+
- exportToPdf (3.2.0+) works in ~30 ms slices and yields between them (prepare and render phases), so the page stays responsive; only jsPDF's final save is one synchronous step. `onProgress({ phase: 'prepare'|'render'|'save', done, total })`; `signal` abort rejects with DOMException 'AbortError' and saves nothing; above `maxRows` (default 20000) a dev console.warn recommends exportToCsv / exportToExcelAdvanced. Output (pages, positions) is the same as before.
|
|
504
507
|
- CSV and exportToExcel neutralise formulas: text starting with = + - @ tab or CR gets a leading `'` (`escapeFormulas: false` opts out).
|
|
505
508
|
- exportToExcel writes HTML: a `.xlsx` fileName is saved as `.xls` with a console warning.
|
|
506
509
|
- Columns with `exportable: false` are left out everywhere; `valueGetter` and `valueFormatter` are applied.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opencorestack/opengridx",
|
|
3
|
-
"version": "3.1
|
|
3
|
+
"version": "3.2.1",
|
|
4
4
|
"description": "OpenGridX: High-performance React data infrastructure. Unlock advanced Row Grouping, Excel Export, and Column Pinning without the usual \"Pro\" gatekeeping. Built for speed, scale, and complete architectural freedom. Fully open, virtualization-ready, and feature-complete.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/opengridx.umd.js",
|
|
@@ -47,11 +47,12 @@
|
|
|
47
47
|
"build": "tsc --project tsconfig.build.json && vite build",
|
|
48
48
|
"build:lib": "tsc --project tsconfig.build.json && vite build --config vite.config.lib.js && cp lib/styles/opengridx.css.d.ts dist/opengridx.css.d.ts && node scripts/check-bundle.mjs",
|
|
49
49
|
"lint": "eslint .",
|
|
50
|
-
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json --noEmit",
|
|
50
|
+
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json --noEmit && tsc -p e2e/tsconfig.json",
|
|
51
51
|
"preview": "vite preview",
|
|
52
52
|
"test": "vitest run --project unit",
|
|
53
53
|
"test:browser": "vitest run --project browser",
|
|
54
54
|
"test:all": "vitest run",
|
|
55
|
+
"test:smoke": "node scripts/smoke.mjs",
|
|
55
56
|
"test:watch": "vitest --project unit",
|
|
56
57
|
"coverage": "vitest run --coverage",
|
|
57
58
|
"prepublishOnly": "npm run build:lib"
|
|
@@ -106,6 +107,7 @@
|
|
|
106
107
|
},
|
|
107
108
|
"devDependencies": {
|
|
108
109
|
"@eslint/js": "^9.39.1",
|
|
110
|
+
"@playwright/test": "^1.58.2",
|
|
109
111
|
"@testing-library/jest-dom": "^7.0.0",
|
|
110
112
|
"@testing-library/react": "^16.3.2",
|
|
111
113
|
"@testing-library/user-event": "^14.6.1",
|