@svgrid/enterprise 3.1.26 → 3.1.27
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/dist/cdn/svgrid-enterprise.svelte-external.js +20195 -19732
- package/dist/designer/assets/{GridFooter-DKMfZpL4.js → GridFooter-EyLg77jG.js} +1 -1
- package/dist/designer/assets/{GridMenus-C8WqB-To.js → GridMenus-BAgfpDd-.js} +2 -2
- package/dist/designer/assets/SvGrid.helpers-DHkH3mHW.js +1 -0
- package/dist/designer/assets/{SvGridCellEditor-CC9UMdVq.js → SvGridCellEditor-BHxH4b5h.js} +1 -1
- package/dist/designer/assets/column-types-Bl5R-97G.js +1 -0
- package/dist/designer/assets/{index-PEFkd-5C.js → index-Cim1_diz.js} +324 -324
- package/dist/designer/assets/src-sFRH_up7.js +60 -0
- package/dist/designer/index.html +8 -7
- package/dist/export-conditional.d.ts +2 -0
- package/dist/export-conditional.js +2 -0
- package/dist/export-pdf.d.ts +142 -7
- package/dist/export-pdf.js +368 -44
- package/dist/export.d.ts +22 -4
- package/dist/export.js +150 -53
- package/dist/import.js +4 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/node/studio.js +1 -1
- package/dist/pdf-worker.d.ts +92 -0
- package/dist/pdf-worker.js +147 -0
- package/dist/sheet/ods-document.js +3 -1
- package/dist/sheet/open-file.js +4 -2
- package/dist/sheet/xlsx-document.d.ts +2 -2
- package/dist/sheet/xlsx-document.js +4 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/wc/sv-sheet-element.js +1476 -1439
- package/dist/xlsx-limits.d.ts +34 -0
- package/dist/xlsx-limits.js +68 -0
- package/package.json +5 -1
- package/dist/designer/assets/SvGrid.helpers-BF2QRcNt.js +0 -1
- package/dist/designer/assets/src-DC5nN4Q7.js +0 -58
package/dist/export.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { applyGroupAggregate } from '@svgrid/grid/core';
|
|
1
2
|
// Import the pure formatters from the grid's Svelte-free `/format` subpath so
|
|
2
3
|
// this module (and the enterprise unit tests) don't drag in SvGrid.svelte.
|
|
3
4
|
import { formatValueForExport, toExcelNumFmt, valueForExcel } from '@svgrid/grid/format';
|
|
@@ -6,7 +7,10 @@ import { installSmartShim } from './smart-shim.js';
|
|
|
6
7
|
import { downloadBlobFile, downloadTextFile, serializeDelimited, serializeHtml, serializeJson, serializeMarkdown, serializeXml, } from './export-serialize.js';
|
|
7
8
|
import { serializeSpreadsheetML } from './export-xls.js';
|
|
8
9
|
import { buildXlsxParts, packageXlsx } from './export-ooxml.js';
|
|
9
|
-
import { buildPdfDocDefinition, pdfBlob, readGridPdfTheme, registerPdfFonts, resolvePdfCharts } from './export-pdf.js';
|
|
10
|
+
import { buildPdfDocData, buildPdfDocDefinition, pdfBlob, PDF_ROBOTO_MISSING, readGridPdfTheme, registerPdfFont, registerPdfFonts, resolvePdfCharts } from './export-pdf.js';
|
|
11
|
+
import { renderPdfInWorker } from './pdf-worker.js';
|
|
12
|
+
/** Rows past which a PDF rendered on the page earns a warning: about a second of blocked page per thousand. */
|
|
13
|
+
const LARGE_PDF_ROWS = 2000;
|
|
10
14
|
import { buildConditionalResolver } from './export-conditional.js';
|
|
11
15
|
/** Formats served by the native, dependency-free, streaming serializers. */
|
|
12
16
|
const NATIVE_TEXT_FORMATS = [
|
|
@@ -102,6 +106,7 @@ export function resolveColumns(api, opts, sourceRows) {
|
|
|
102
106
|
header: c.header,
|
|
103
107
|
format: c.format,
|
|
104
108
|
align: c.align,
|
|
109
|
+
aggregate: c.aggregate,
|
|
105
110
|
}));
|
|
106
111
|
}
|
|
107
112
|
if (sourceRows.length > 0) {
|
|
@@ -550,27 +555,58 @@ function isSummableColumn(col) {
|
|
|
550
555
|
}
|
|
551
556
|
/**
|
|
552
557
|
* Build a grouped PDF body: a bold group header per cluster (nested for
|
|
553
|
-
* multi-level grouping), the cluster's data rows,
|
|
554
|
-
* the
|
|
558
|
+
* multi-level grouping), the cluster's data rows, a subtotal row, and a grand
|
|
559
|
+
* total at the end. A column totals with its own `aggregate` (avg, min, max,
|
|
560
|
+
* count, ...), as the grid's group rows do; without one, number and currency
|
|
561
|
+
* columns sum. Values and group names use the same formatting as the grid.
|
|
562
|
+
* Each data row carries its index in `rows`, so conditional formatting and
|
|
563
|
+
* links can find their row.
|
|
555
564
|
*/
|
|
556
|
-
export function buildGroupedPdfBody(cols, rows, groupBy) {
|
|
565
|
+
export function buildGroupedPdfBody(cols, rows, groupBy, opts = {}) {
|
|
557
566
|
const out = [];
|
|
558
|
-
const
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
567
|
+
const colOf = (field) => cols.find((c) => c.field === field);
|
|
568
|
+
const aggOf = (c) => c.aggregate ?? (isSummableColumn(c) ? 'sum' : undefined);
|
|
569
|
+
const hasTotals = cols.some((c) => aggOf(c) !== undefined);
|
|
570
|
+
// The label goes in the first column that has no total of its own; if
|
|
571
|
+
// every column has one, -1 means "don't overwrite a total with the label".
|
|
572
|
+
const labelColIdx = cols.findIndex((c) => aggOf(c) === undefined);
|
|
573
|
+
const index = new Map();
|
|
574
|
+
rows.forEach((r, i) => index.set(r, i));
|
|
575
|
+
const field = (r, f) => r[f];
|
|
576
|
+
const total = (c, subset) => {
|
|
577
|
+
const agg = aggOf(c);
|
|
578
|
+
if (agg === undefined)
|
|
579
|
+
return '';
|
|
580
|
+
// The grid's own aggregator, over the same values the column shows.
|
|
581
|
+
const v = applyGroupAggregate(agg, c.field, subset.map((r) => ({ original: r, getCellValueByColumnId: () => field(r, c.field) })));
|
|
582
|
+
if (v == null || (typeof v === 'number' && !Number.isFinite(v)))
|
|
583
|
+
return '';
|
|
584
|
+
// A count is a number of rows, not an amount: no currency or percent.
|
|
585
|
+
if (agg === 'count' || agg === 'countDistinct')
|
|
586
|
+
return String(v);
|
|
587
|
+
if (Array.isArray(v))
|
|
588
|
+
return v.map((x) => formatValueForExport(x, c.format)).join(' - ');
|
|
589
|
+
return formatValueForExport(v, c.format);
|
|
590
|
+
};
|
|
591
|
+
const totalRow = (subset, label) => {
|
|
592
|
+
const cells = cols.map((c) => total(c, subset));
|
|
593
|
+
if (labelColIdx >= 0)
|
|
594
|
+
cells[labelColIdx] = label;
|
|
595
|
+
return { kind: 'subtotal', cells };
|
|
596
|
+
};
|
|
562
597
|
const recurse = (subset, level) => {
|
|
563
598
|
if (level >= groupBy.length) {
|
|
564
599
|
for (const r of subset) {
|
|
565
|
-
out.push({ kind: 'data', cells: cols.map((c) => String(cellValueFor(c, r, false) ?? '')) });
|
|
600
|
+
out.push({ kind: 'data', cells: cols.map((c) => String(cellValueFor(c, r, false) ?? '')), source: index.get(r) });
|
|
566
601
|
}
|
|
567
602
|
return;
|
|
568
603
|
}
|
|
569
|
-
const
|
|
604
|
+
const groupField = groupBy[level];
|
|
605
|
+
const groupCol = colOf(groupField);
|
|
570
606
|
// Preserve first-seen order of group keys.
|
|
571
607
|
const buckets = new Map();
|
|
572
608
|
for (const r of subset) {
|
|
573
|
-
const key = String(r
|
|
609
|
+
const key = String(field(r, groupField) ?? '');
|
|
574
610
|
const bucket = buckets.get(key);
|
|
575
611
|
if (bucket)
|
|
576
612
|
bucket.push(r);
|
|
@@ -578,26 +614,31 @@ export function buildGroupedPdfBody(cols, rows, groupBy) {
|
|
|
578
614
|
buckets.set(key, [r]);
|
|
579
615
|
}
|
|
580
616
|
for (const [key, groupRows] of buckets) {
|
|
581
|
-
|
|
617
|
+
// The name as the grid shows it: a date group reads "Oct 3, 2026", not
|
|
618
|
+
// the timestamp it is keyed by.
|
|
619
|
+
const shown = groupCol ? String(cellValueFor(groupCol, groupRows[0], false) ?? key) : key;
|
|
620
|
+
out.push({ kind: 'group', label: `${groupCol?.header ?? groupField}: ${shown} (${groupRows.length})`, level });
|
|
582
621
|
recurse(groupRows, level + 1);
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
if (hasSummable) {
|
|
586
|
-
const cells = cols.map((c) => {
|
|
587
|
-
if (!isSummableColumn(c))
|
|
588
|
-
return '';
|
|
589
|
-
const sum = groupRows.reduce((a, r) => a + (Number(r[c.field]) || 0), 0);
|
|
590
|
-
return formatValueForExport(sum, c.format);
|
|
591
|
-
});
|
|
592
|
-
if (labelColIdx >= 0)
|
|
593
|
-
cells[labelColIdx] = `Subtotal (${key})`;
|
|
594
|
-
out.push({ kind: 'subtotal', cells });
|
|
595
|
-
}
|
|
622
|
+
if (hasTotals)
|
|
623
|
+
out.push(totalRow(groupRows, opts.labels?.subtotal?.(shown) ?? `Subtotal (${shown})`));
|
|
596
624
|
}
|
|
597
625
|
};
|
|
598
626
|
recurse(rows, 0);
|
|
627
|
+
if (hasTotals && opts.grandTotal !== false && rows.length)
|
|
628
|
+
out.push(totalRow(rows, opts.labels?.total ?? 'Total'));
|
|
599
629
|
return out;
|
|
600
630
|
}
|
|
631
|
+
/**
|
|
632
|
+
* Say so when the PDF holds text Roboto cannot draw. Without a `pdf.font` it
|
|
633
|
+
* prints as empty boxes, and nothing else would tell the developer why.
|
|
634
|
+
*/
|
|
635
|
+
function warnMissingGlyphs(texts) {
|
|
636
|
+
const hit = texts.find((t) => PDF_ROBOTO_MISSING.test(t));
|
|
637
|
+
if (hit === undefined)
|
|
638
|
+
return;
|
|
639
|
+
console.warn(`@svgrid/enterprise: the PDF has text its default font (Roboto) cannot draw, such as "${hit.slice(0, 40)}"; ` +
|
|
640
|
+
'it prints as empty boxes. Pass pdf.font with a font that covers it (Noto Sans CJK, Noto Sans Arabic, ...).');
|
|
641
|
+
}
|
|
601
642
|
/**
|
|
602
643
|
* PDF export via our own pdfmake docDefinition (page setup, repeated header,
|
|
603
644
|
* page numbers, per-column alignment/widths, title/logo, theme colors, and
|
|
@@ -605,18 +646,17 @@ export function buildGroupedPdfBody(cols, rows, groupBy) {
|
|
|
605
646
|
* formatted display strings, so the PDF matches the grid.
|
|
606
647
|
*/
|
|
607
648
|
async function exportPdf(api, opts) {
|
|
608
|
-
await ensureGlobals('pdf');
|
|
609
|
-
const pdfMake = globalThis.pdfMake;
|
|
610
|
-
if (!pdfMake) {
|
|
611
|
-
throw new Error('@svgrid/enterprise: pdf export requires the "pdfmake" peer dependency.');
|
|
612
|
-
}
|
|
613
649
|
const sourceRows = resolveRowSource(api, opts.rows);
|
|
614
650
|
if (sourceRows.length === 0) {
|
|
615
651
|
throw new Error('@svgrid/enterprise: nothing to export - the grid has no rows');
|
|
616
652
|
}
|
|
617
653
|
const cols = resolveColumns(api, opts, sourceRows);
|
|
618
654
|
const fields = cols.map((c) => c.field);
|
|
619
|
-
|
|
655
|
+
// The grid's columns arrive with the alignment the screen uses; columns an
|
|
656
|
+
// app passes itself may not say, so a number or date format right-aligns,
|
|
657
|
+
// as it does on screen.
|
|
658
|
+
const alignOf = (c) => c.align ?? (c.format && ['number', 'currency', 'percent', 'date', 'datetime', 'time'].includes(String(c.format.type)) ? 'right' : undefined);
|
|
659
|
+
const columns = cols.map((c) => ({ header: c.header ?? c.field, align: alignOf(c) }));
|
|
620
660
|
// Carry the grid's active grouping into group + subtotal rows (unless the
|
|
621
661
|
// caller opted out or passed hierarchical), matching the xlsx outline path.
|
|
622
662
|
const autoGrouping = opts.autoGroup !== false && !opts.groupBy?.length && !opts.hierarchical
|
|
@@ -648,33 +688,67 @@ async function exportPdf(api, opts) {
|
|
|
648
688
|
return chartToPngBlob(el, { scale: 2 });
|
|
649
689
|
});
|
|
650
690
|
const now = new Date();
|
|
651
|
-
let
|
|
691
|
+
let params;
|
|
692
|
+
// A boolean prints as the grid's checkbox does: a tick for true and nothing
|
|
693
|
+
// for false, rather than the words. Only plain fields: a column with a
|
|
694
|
+
// format or an `exportValue` prints what it asks for.
|
|
695
|
+
const boolAt = (rowIdx, colIdx) => {
|
|
696
|
+
const col = cols[colIdx];
|
|
697
|
+
if (!col || col.exportValue || col.format)
|
|
698
|
+
return undefined;
|
|
699
|
+
const v = sourceRows[rowIdx]?.[col.field];
|
|
700
|
+
return typeof v === 'boolean' ? v : undefined;
|
|
701
|
+
};
|
|
702
|
+
const withTick = (v, b) => {
|
|
703
|
+
const icon = v?.icon ?? (b ? '✓' : undefined);
|
|
704
|
+
return v || icon ? { fill: v?.fill, color: v?.color, bold: v?.bold, icon, iconOnly: v?.iconOnly } : undefined;
|
|
705
|
+
};
|
|
652
706
|
if (effGroupBy?.length) {
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
707
|
+
const grouped = buildGroupedPdfBody(cols, sourceRows, effGroupBy, { grandTotal: pdfOpts.grandTotal, labels: pdfOpts.labels });
|
|
708
|
+
const body = grouped.map((row) => row.kind === 'data' && row.source !== undefined
|
|
709
|
+
? { ...row, cells: row.cells.map((text, c) => (boolAt(row.source, c) === undefined ? text : '')) }
|
|
710
|
+
: row);
|
|
711
|
+
if (!pdfOpts.font)
|
|
712
|
+
warnMissingGlyphs(body.flatMap((r) => (r.kind === 'group' ? [r.label] : r.cells)));
|
|
713
|
+
// Data rows come out in group order; conditional formatting and links are
|
|
714
|
+
// looked up by each row's place in the exported rows.
|
|
715
|
+
const sources = body.flatMap((r) => (r.kind === 'data' ? [r.source ?? -1] : []));
|
|
716
|
+
const visual = buildConditionalResolver(cols, sourceRows, opts.conditionalFormats);
|
|
717
|
+
const link = buildLinkResolver(cols, sourceRows);
|
|
718
|
+
params = {
|
|
719
|
+
columns,
|
|
720
|
+
body,
|
|
721
|
+
dataCellStyle: (r, c) => {
|
|
722
|
+
const at = sources[r] ?? -1;
|
|
723
|
+
return at >= 0 ? withTick(visual(at, c), boolAt(at, c)) : undefined;
|
|
724
|
+
},
|
|
725
|
+
dataCellLink: link
|
|
726
|
+
? (r, c) => {
|
|
727
|
+
const at = sources[r] ?? -1;
|
|
728
|
+
return at >= 0 ? link(at, c) : undefined;
|
|
729
|
+
}
|
|
730
|
+
: undefined,
|
|
731
|
+
opts: pdfOpts,
|
|
732
|
+
styles: opts.styles,
|
|
733
|
+
theme,
|
|
734
|
+
...lines,
|
|
735
|
+
charts,
|
|
736
|
+
now,
|
|
737
|
+
};
|
|
657
738
|
}
|
|
658
739
|
else {
|
|
659
740
|
const projected = await projectRows(sourceRows, cols, opts);
|
|
660
|
-
const dataRows = projected.slice(1).map((r) => fields.map((f) => String(r[f] ?? '')));
|
|
661
|
-
// Conditional formatting -> per-cell fill / color / bold
|
|
741
|
+
const dataRows = projected.slice(1).map((r, ri) => fields.map((f, c) => (boolAt(ri, c) === undefined ? String(r[f] ?? '') : '')));
|
|
742
|
+
// Conditional formatting -> per-cell fill / color / bold, and the icon,
|
|
743
|
+
// which the builder draws (the icon sets' glyphs are not in the font).
|
|
662
744
|
const visual = buildConditionalResolver(cols, sourceRows, opts.conditionalFormats);
|
|
663
|
-
for (let r = 0; r < dataRows.length; r++) {
|
|
664
|
-
for (let c = 0; c < cols.length; c++) {
|
|
665
|
-
const icon = visual(r, c)?.icon;
|
|
666
|
-
if (icon)
|
|
667
|
-
dataRows[r][c] = `${icon} ${dataRows[r][c] ?? ''}`;
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
745
|
const link = buildLinkResolver(cols, sourceRows);
|
|
671
|
-
|
|
746
|
+
if (!pdfOpts.font)
|
|
747
|
+
warnMissingGlyphs([...columns.map((c) => c.header), ...dataRows.flat()]);
|
|
748
|
+
params = {
|
|
672
749
|
columns,
|
|
673
750
|
rows: dataRows,
|
|
674
|
-
dataCellStyle: (r, c) =>
|
|
675
|
-
const v = visual(r, c);
|
|
676
|
-
return v ? { fill: v.fill, color: v.color, bold: v.bold } : undefined;
|
|
677
|
-
},
|
|
751
|
+
dataCellStyle: (r, c) => withTick(visual(r, c), boolAt(r, c)),
|
|
678
752
|
dataCellLink: link,
|
|
679
753
|
opts: pdfOpts,
|
|
680
754
|
styles: opts.styles,
|
|
@@ -683,11 +757,34 @@ async function exportPdf(api, opts) {
|
|
|
683
757
|
merges: opts.merges,
|
|
684
758
|
charts,
|
|
685
759
|
now,
|
|
686
|
-
}
|
|
760
|
+
};
|
|
687
761
|
}
|
|
688
762
|
const filename = (opts.filename ?? 'grid').trim() || 'grid';
|
|
689
|
-
const
|
|
690
|
-
|
|
763
|
+
const total = sourceRows.length;
|
|
764
|
+
let blob;
|
|
765
|
+
if (pdfOpts.worker) {
|
|
766
|
+
// The document crosses to the worker as data; pdfmake is loaded there.
|
|
767
|
+
blob = await renderPdfInWorker(pdfOpts.worker, buildPdfDocData(params), {
|
|
768
|
+
font: pdfOpts.font,
|
|
769
|
+
signal: opts.signal,
|
|
770
|
+
onProgress: (ratio) => opts.onProgress?.({ phase: 'write', ratio, total }),
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
else {
|
|
774
|
+
if (total > LARGE_PDF_ROWS) {
|
|
775
|
+
console.warn(`@svgrid/enterprise: rendering a ${total.toLocaleString('en-US')}-row PDF on the page; pdfmake blocks it until done ` +
|
|
776
|
+
'(about a second per thousand rows). Pass pdf.worker to render in a worker instead.');
|
|
777
|
+
}
|
|
778
|
+
await ensureGlobals('pdf');
|
|
779
|
+
const pdfMake = globalThis.pdfMake;
|
|
780
|
+
if (!pdfMake) {
|
|
781
|
+
throw new Error('@svgrid/enterprise: pdf export requires the "pdfmake" peer dependency.');
|
|
782
|
+
}
|
|
783
|
+
if (pdfOpts.font)
|
|
784
|
+
registerPdfFont(pdfMake, pdfOpts.font);
|
|
785
|
+
blob = await pdfBlob(pdfMake.createPdf(buildPdfDocDefinition(params)));
|
|
786
|
+
}
|
|
787
|
+
opts.onProgress?.({ phase: 'write', ratio: 1, total });
|
|
691
788
|
return deliver(blob, ensureExt(filename, 'pdf'), 'application/pdf', sourceRows.length, opts.download);
|
|
692
789
|
}
|
|
693
790
|
/**
|
package/dist/import.js
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* `print`, so unlicensed evaluation works but emits a one-time nudge.
|
|
25
25
|
*/
|
|
26
26
|
import { assertEnterpriseLicensed } from './license.js';
|
|
27
|
-
import { XLSX_MAX_COLS, XLSX_MAX_FILL, xlsxFillError, xlsxRangeError } from './xlsx-limits.js';
|
|
27
|
+
import { createUnzipBudget, readZipEntry, XLSX_MAX_COLS, XLSX_MAX_FILL, xlsxFillError, xlsxRangeError } from './xlsx-limits.js';
|
|
28
28
|
// ---------------------------------------------------------------------------
|
|
29
29
|
// Lazy peer dependency: jszip (xlsx only)
|
|
30
30
|
// ---------------------------------------------------------------------------
|
|
@@ -183,11 +183,12 @@ async function parseXlsx(file) {
|
|
|
183
183
|
const J = await getJSZip();
|
|
184
184
|
const buf = await file.arrayBuffer();
|
|
185
185
|
const zip = await J.loadAsync(buf);
|
|
186
|
+
const budget = createUnzipBudget();
|
|
186
187
|
// Read sharedStrings.xml if it exists.
|
|
187
188
|
let sharedStrings = [];
|
|
188
189
|
const ssEntry = zip.file('xl/sharedStrings.xml');
|
|
189
190
|
if (ssEntry) {
|
|
190
|
-
const ssText = await ssEntry
|
|
191
|
+
const ssText = await readZipEntry(ssEntry, 'string', budget);
|
|
191
192
|
sharedStrings = parseSharedStrings(ssText);
|
|
192
193
|
}
|
|
193
194
|
// Read the FIRST sheet. Workbook.xml -> rels mapping is involved; for the
|
|
@@ -197,7 +198,7 @@ async function parseXlsx(file) {
|
|
|
197
198
|
if (!sheetEntry) {
|
|
198
199
|
throw new Error('@svgrid/enterprise: could not locate sheet1.xml in the .xlsx archive');
|
|
199
200
|
}
|
|
200
|
-
const sheetText = await sheetEntry
|
|
201
|
+
const sheetText = await readZipEntry(sheetEntry, 'string', budget);
|
|
201
202
|
return parseSheetXml(sheetText, sharedStrings);
|
|
202
203
|
}
|
|
203
204
|
/** Pull every `<t>...</t>` out of sharedStrings.xml in document order. */
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export { setLicenseKey, clearLicenseKey, getLicenseKey, isLicenseKeySet, hasVali
|
|
|
2
2
|
export { parseLicenseExpiry } from './license-core.js';
|
|
3
3
|
export { exportGrid, copyExportToClipboard, type ExportFormat, type ExportOptions, type ExportColumn, type ExportCellStyle, type ExportStyles, type ExportHeaderFooterLine, type ExportSheet, type ExportRowSource, type ExportProgress, type ExportResult, type ClipboardFormat, type ClipboardExportOptions, } from './export.js';
|
|
4
4
|
export { default as SvExportMenu } from './SvExportMenu.svelte';
|
|
5
|
-
export { buildPdfDocDefinition, resolvePdfCharts, resolvePdfVfs, registerPdfFonts, type PdfExportOptions, type PdfPageSize, type PdfChart, type PdfKpi, type PdfChartImage, type PdfVirtualFileSystem, type PdfMakeLike } from './export-pdf.js';
|
|
5
|
+
export { buildPdfDocDefinition, buildPdfDocData, hydratePdfDoc, resolvePdfCharts, resolvePdfVfs, registerPdfFonts, registerPdfFont, type PdfExportOptions, type PdfDocData, type PdfDocParams, type PdfWorkerLike, type PdfPageSize, type PdfChart, type PdfKpi, type PdfChartImage, type PdfVirtualFileSystem, type PdfMakeLike, type PdfFont } from './export-pdf.js';
|
|
6
6
|
export { printGrid, type PrintOptions } from './print.js';
|
|
7
7
|
export { importData, readImportMatrix, mapImportMatrix, mapImportMatrixAsync, autoMapColumns, inferImportColumnTypes, type ImportFormat, type ImportFieldType, type ImportOptions, type ImportResult, type ImportProgress, type ImportColumnMap, type ImportColumnTypes, type ImportGridColumn, type ImportRowError, type ImportValidator, } from './import.js';
|
|
8
8
|
export { default as SvImportDialog } from './SvImportDialog.svelte';
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@ licenseCovers, getLicenseEdition, nudgeEnterpriseFor, editionCovers, } from './l
|
|
|
8
8
|
export { parseLicenseExpiry } from './license-core.js';
|
|
9
9
|
export { exportGrid, copyExportToClipboard, } from './export.js';
|
|
10
10
|
export { default as SvExportMenu } from './SvExportMenu.svelte';
|
|
11
|
-
export { buildPdfDocDefinition, resolvePdfCharts, resolvePdfVfs, registerPdfFonts } from './export-pdf.js';
|
|
11
|
+
export { buildPdfDocDefinition, buildPdfDocData, hydratePdfDoc, resolvePdfCharts, resolvePdfVfs, registerPdfFonts, registerPdfFont } from './export-pdf.js';
|
|
12
12
|
export { printGrid } from './print.js';
|
|
13
13
|
export { importData, readImportMatrix, mapImportMatrix, mapImportMatrixAsync, autoMapColumns, inferImportColumnTypes, } from './import.js';
|
|
14
14
|
export { default as SvImportDialog } from './SvImportDialog.svelte';
|
package/dist/node/studio.js
CHANGED
|
@@ -16514,7 +16514,7 @@ function hasFieldConditions(schema) {
|
|
|
16514
16514
|
*
|
|
16515
16515
|
* `version.test.ts` asserts this matches `package.json`, so the two cannot drift.
|
|
16516
16516
|
*/
|
|
16517
|
-
var SVGRID_VERSION = "3.1.
|
|
16517
|
+
var SVGRID_VERSION = "3.1.27";
|
|
16518
16518
|
/**
|
|
16519
16519
|
* The version of `@svgrid/grid`, which releases on its own line: enterprise is
|
|
16520
16520
|
* on 3.1.x while the grid is on 3.0.x.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render a PDF export in a worker, so a large report does not freeze the
|
|
3
|
+
* page. pdfmake lays out and writes the whole document in one synchronous
|
|
4
|
+
* run, about a millisecond per row of eight columns on a desktop machine:
|
|
5
|
+
* a 20,000-row export held the main thread for over twenty seconds.
|
|
6
|
+
*
|
|
7
|
+
* The app owns the worker file, so any bundler builds it:
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* // pdf.worker.ts
|
|
11
|
+
* import { servePdfWorker } from '@svgrid/enterprise/pdf-worker'
|
|
12
|
+
* servePdfWorker()
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* ```ts
|
|
16
|
+
* await api.exportData({
|
|
17
|
+
* format: 'pdf',
|
|
18
|
+
* pdf: { worker: () => new Worker(new URL('./pdf.worker.ts', import.meta.url), { type: 'module' }) },
|
|
19
|
+
* })
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* The main thread builds the document as plain data (`buildPdfDocData`) and
|
|
23
|
+
* posts it here with the font, if any; the worker loads pdfmake, attaches
|
|
24
|
+
* the layout and footer functions (`hydratePdfDoc`), renders, and posts the
|
|
25
|
+
* bytes back with progress along the way.
|
|
26
|
+
*/
|
|
27
|
+
import { type PdfDocData, type PdfFont, type PdfMakeLike, type PdfWorkerLike } from './export-pdf.js';
|
|
28
|
+
export type { PdfWorkerLike };
|
|
29
|
+
/** Tags this channel's messages, so a worker shared with other traffic ignores the rest. */
|
|
30
|
+
export declare const PDF_WORKER_CHANNEL = "svgrid-pdf";
|
|
31
|
+
export type PdfWorkerRequest = {
|
|
32
|
+
channel: typeof PDF_WORKER_CHANNEL;
|
|
33
|
+
id: number;
|
|
34
|
+
doc: PdfDocData;
|
|
35
|
+
font?: PdfFont;
|
|
36
|
+
};
|
|
37
|
+
export type PdfWorkerReply = {
|
|
38
|
+
channel: typeof PDF_WORKER_CHANNEL;
|
|
39
|
+
id: number;
|
|
40
|
+
progress: number;
|
|
41
|
+
} | {
|
|
42
|
+
channel: typeof PDF_WORKER_CHANNEL;
|
|
43
|
+
id: number;
|
|
44
|
+
pdf: ArrayBuffer;
|
|
45
|
+
} | {
|
|
46
|
+
channel: typeof PDF_WORKER_CHANNEL;
|
|
47
|
+
id: number;
|
|
48
|
+
error: string;
|
|
49
|
+
};
|
|
50
|
+
/** The pdfmake calls rendering needs, in either the 0.2 or the 0.3 shape. */
|
|
51
|
+
export type PdfMakeRenderer = PdfMakeLike & {
|
|
52
|
+
fonts?: Record<string, unknown>;
|
|
53
|
+
addFonts?: (fonts: Record<string, unknown>) => void;
|
|
54
|
+
createPdf(def: unknown): {
|
|
55
|
+
getBuffer(cb?: (bytes: Uint8Array) => void, options?: {
|
|
56
|
+
progressCallback?: (progress: number) => void;
|
|
57
|
+
}): unknown;
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
/** The part of a worker's global scope (or a `MessagePort`) this needs. */
|
|
61
|
+
export type PdfWorkerScope = {
|
|
62
|
+
postMessage(message: unknown, transfer?: Transferable[]): void;
|
|
63
|
+
addEventListener(type: 'message', listener: (event: MessageEvent) => void): void;
|
|
64
|
+
removeEventListener(type: 'message', listener: (event: MessageEvent) => void): void;
|
|
65
|
+
};
|
|
66
|
+
/**
|
|
67
|
+
* Render a document from `buildPdfDocData` with the pdfmake given. The
|
|
68
|
+
* worker's half of the export, exported so it can run anywhere pdfmake does.
|
|
69
|
+
*/
|
|
70
|
+
export declare function renderPdfDoc(doc: PdfDocData, pdfMake: PdfMakeRenderer, opts?: {
|
|
71
|
+
font?: PdfFont;
|
|
72
|
+
onProgress?: (progress: number) => void;
|
|
73
|
+
}): Promise<Uint8Array>;
|
|
74
|
+
export type ServePdfWorkerOptions = {
|
|
75
|
+
/** Where to listen. Default: the worker's global scope. */
|
|
76
|
+
scope?: PdfWorkerScope;
|
|
77
|
+
/** How to get pdfmake. Default: import the installed `pdfmake` peer. */
|
|
78
|
+
loadPdfMake?: () => Promise<PdfMakeRenderer>;
|
|
79
|
+
};
|
|
80
|
+
/** Answer PDF export requests from the main thread. Call it once, in the worker. */
|
|
81
|
+
export declare function servePdfWorker(options?: ServePdfWorkerOptions): {
|
|
82
|
+
dispose(): void;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* The main thread's half: post the document to a fresh worker, report its
|
|
86
|
+
* progress, and resolve with the PDF. An abort terminates the worker.
|
|
87
|
+
*/
|
|
88
|
+
export declare function renderPdfInWorker(makeWorker: () => PdfWorkerLike, doc: PdfDocData, opts?: {
|
|
89
|
+
font?: PdfFont;
|
|
90
|
+
signal?: AbortSignal;
|
|
91
|
+
onProgress?: (progress: number) => void;
|
|
92
|
+
}): Promise<Blob>;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/// <reference path="./pdfmake-shims.d.ts" />
|
|
2
|
+
/**
|
|
3
|
+
* Render a PDF export in a worker, so a large report does not freeze the
|
|
4
|
+
* page. pdfmake lays out and writes the whole document in one synchronous
|
|
5
|
+
* run, about a millisecond per row of eight columns on a desktop machine:
|
|
6
|
+
* a 20,000-row export held the main thread for over twenty seconds.
|
|
7
|
+
*
|
|
8
|
+
* The app owns the worker file, so any bundler builds it:
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* // pdf.worker.ts
|
|
12
|
+
* import { servePdfWorker } from '@svgrid/enterprise/pdf-worker'
|
|
13
|
+
* servePdfWorker()
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* ```ts
|
|
17
|
+
* await api.exportData({
|
|
18
|
+
* format: 'pdf',
|
|
19
|
+
* pdf: { worker: () => new Worker(new URL('./pdf.worker.ts', import.meta.url), { type: 'module' }) },
|
|
20
|
+
* })
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* The main thread builds the document as plain data (`buildPdfDocData`) and
|
|
24
|
+
* posts it here with the font, if any; the worker loads pdfmake, attaches
|
|
25
|
+
* the layout and footer functions (`hydratePdfDoc`), renders, and posts the
|
|
26
|
+
* bytes back with progress along the way.
|
|
27
|
+
*/
|
|
28
|
+
import { hydratePdfDoc, registerPdfFont, registerPdfFonts } from './export-pdf.js';
|
|
29
|
+
/** Tags this channel's messages, so a worker shared with other traffic ignores the rest. */
|
|
30
|
+
export const PDF_WORKER_CHANNEL = 'svgrid-pdf';
|
|
31
|
+
/**
|
|
32
|
+
* Render a document from `buildPdfDocData` with the pdfmake given. The
|
|
33
|
+
* worker's half of the export, exported so it can run anywhere pdfmake does.
|
|
34
|
+
*/
|
|
35
|
+
export function renderPdfDoc(doc, pdfMake, opts = {}) {
|
|
36
|
+
if (opts.font)
|
|
37
|
+
registerPdfFont(pdfMake, opts.font);
|
|
38
|
+
const def = hydratePdfDoc(doc);
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
const done = (bytes) => resolve(bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes));
|
|
41
|
+
try {
|
|
42
|
+
// 0.2 calls back; 0.3 returns a promise and ignores the callback.
|
|
43
|
+
const returned = pdfMake.createPdf(def).getBuffer(done, { progressCallback: opts.onProgress });
|
|
44
|
+
if (returned && typeof returned.then === 'function') {
|
|
45
|
+
;
|
|
46
|
+
returned.then(done, reject);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
reject(err);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
async function loadPdfMake() {
|
|
55
|
+
// @ts-ignore - "pdfmake" is an optional peerDependency
|
|
56
|
+
const mod = (await import('pdfmake/build/pdfmake'));
|
|
57
|
+
// @ts-ignore - "pdfmake" is an optional peerDependency
|
|
58
|
+
const vfs = await import('pdfmake/build/vfs_fonts');
|
|
59
|
+
const pdfMake = (mod.default ?? mod);
|
|
60
|
+
if (!registerPdfFonts(pdfMake, vfs)) {
|
|
61
|
+
throw new Error('"pdfmake/build/vfs_fonts" did not contain a font map; the installed pdfmake is not one this export knows.');
|
|
62
|
+
}
|
|
63
|
+
// createPdf reads `pdfMake.tableLayouts` off the global, and a bundled
|
|
64
|
+
// import never sets that global, so without this every render failed with
|
|
65
|
+
// "Cannot read properties of undefined (reading 'tableLayouts')". The page
|
|
66
|
+
// side does the same in ensureGlobals.
|
|
67
|
+
;
|
|
68
|
+
globalThis.pdfMake = pdfMake;
|
|
69
|
+
return pdfMake;
|
|
70
|
+
}
|
|
71
|
+
/** Answer PDF export requests from the main thread. Call it once, in the worker. */
|
|
72
|
+
export function servePdfWorker(options = {}) {
|
|
73
|
+
const scope = options.scope ?? globalThis;
|
|
74
|
+
const load = options.loadPdfMake ?? loadPdfMake;
|
|
75
|
+
let pdfMake = null;
|
|
76
|
+
const reply = (message, transfer) => scope.postMessage(message, transfer);
|
|
77
|
+
const listener = async (event) => {
|
|
78
|
+
const msg = event.data;
|
|
79
|
+
if (!msg || msg.channel !== PDF_WORKER_CHANNEL || typeof msg.id !== 'number' || !msg.doc)
|
|
80
|
+
return;
|
|
81
|
+
const { id, doc, font } = msg;
|
|
82
|
+
try {
|
|
83
|
+
pdfMake ?? (pdfMake = load());
|
|
84
|
+
// pdfmake reports progress per item drawn: a 5,000-row table sent
|
|
85
|
+
// 79,129 messages. One per percent is enough for a progress bar.
|
|
86
|
+
let sent = -1;
|
|
87
|
+
const bytes = await renderPdfDoc(doc, await pdfMake, {
|
|
88
|
+
font,
|
|
89
|
+
onProgress: (progress) => {
|
|
90
|
+
if (progress < 1 && progress - sent < 0.01)
|
|
91
|
+
return;
|
|
92
|
+
sent = progress;
|
|
93
|
+
reply({ channel: PDF_WORKER_CHANNEL, id, progress });
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
// A copy of exactly these bytes, handed over rather than cloned.
|
|
97
|
+
const pdf = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
98
|
+
reply({ channel: PDF_WORKER_CHANNEL, id, pdf }, [pdf]);
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
pdfMake = null;
|
|
102
|
+
reply({ channel: PDF_WORKER_CHANNEL, id, error: err instanceof Error ? err.message : String(err) });
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
scope.addEventListener('message', listener);
|
|
106
|
+
return { dispose: () => scope.removeEventListener('message', listener) };
|
|
107
|
+
}
|
|
108
|
+
let nextJob = 1;
|
|
109
|
+
/**
|
|
110
|
+
* The main thread's half: post the document to a fresh worker, report its
|
|
111
|
+
* progress, and resolve with the PDF. An abort terminates the worker.
|
|
112
|
+
*/
|
|
113
|
+
export function renderPdfInWorker(makeWorker, doc, opts = {}) {
|
|
114
|
+
const worker = makeWorker();
|
|
115
|
+
const id = nextJob++;
|
|
116
|
+
return new Promise((resolve, reject) => {
|
|
117
|
+
const finish = () => {
|
|
118
|
+
opts.signal?.removeEventListener('abort', onAbort);
|
|
119
|
+
worker.terminate();
|
|
120
|
+
};
|
|
121
|
+
const onAbort = () => {
|
|
122
|
+
finish();
|
|
123
|
+
reject(new DOMException('Export aborted', 'AbortError'));
|
|
124
|
+
};
|
|
125
|
+
if (opts.signal?.aborted)
|
|
126
|
+
return onAbort();
|
|
127
|
+
opts.signal?.addEventListener('abort', onAbort, { once: true });
|
|
128
|
+
worker.addEventListener('message', (event) => {
|
|
129
|
+
const msg = event.data;
|
|
130
|
+
if (!msg || msg.channel !== PDF_WORKER_CHANNEL || msg.id !== id)
|
|
131
|
+
return;
|
|
132
|
+
if ('progress' in msg)
|
|
133
|
+
return opts.onProgress?.(msg.progress);
|
|
134
|
+
finish();
|
|
135
|
+
if ('pdf' in msg)
|
|
136
|
+
resolve(new Blob([msg.pdf], { type: 'application/pdf' }));
|
|
137
|
+
else
|
|
138
|
+
reject(new Error(`@svgrid/enterprise: the PDF worker failed: ${msg.error}`));
|
|
139
|
+
});
|
|
140
|
+
worker.addEventListener('error', (event) => {
|
|
141
|
+
finish();
|
|
142
|
+
reject(new Error(`@svgrid/enterprise: the PDF worker failed to start or crashed: ${event.message || 'no message'}. Check that it calls servePdfWorker().`));
|
|
143
|
+
});
|
|
144
|
+
const request = { channel: PDF_WORKER_CHANNEL, id, doc, ...(opts.font ? { font: opts.font } : {}) };
|
|
145
|
+
worker.postMessage(request);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
@@ -17,6 +17,7 @@ import { colToLetters, lettersToCol } from './address.js';
|
|
|
17
17
|
import { tokenize } from './tokenize.js';
|
|
18
18
|
import { formatKeyAt } from './format-store.js';
|
|
19
19
|
import { linkAt } from './links.js';
|
|
20
|
+
import { createUnzipBudget, readZipEntry } from '../xlsx-limits.js';
|
|
20
21
|
import { threadAt } from './comments.js';
|
|
21
22
|
import { loadZip } from './xlsx-document.js';
|
|
22
23
|
import { accountingParts, accountingPattern } from './number-format.js';
|
|
@@ -747,6 +748,7 @@ function durationSeconds(text) {
|
|
|
747
748
|
export async function documentFromOds(file, JSZip) {
|
|
748
749
|
const Zip = await loadZip(JSZip);
|
|
749
750
|
const zip = await Zip.loadAsync(file);
|
|
751
|
+
const budget = createUnzipBudget();
|
|
750
752
|
const parts = {};
|
|
751
753
|
const pending = [];
|
|
752
754
|
zip.forEach((path, entry) => {
|
|
@@ -755,7 +757,7 @@ export async function documentFromOds(file, JSZip) {
|
|
|
755
757
|
if (!/\.(xml)$/i.test(path))
|
|
756
758
|
return;
|
|
757
759
|
const clean = path.replace(/^\//, '');
|
|
758
|
-
pending.push(entry
|
|
760
|
+
pending.push(readZipEntry(entry, 'string', budget).then((text) => { parts[clean] = text; }));
|
|
759
761
|
});
|
|
760
762
|
await Promise.all(pending);
|
|
761
763
|
return sheetStateFromOds(parts);
|
package/dist/sheet/open-file.js
CHANGED
|
@@ -14,6 +14,7 @@ import { documentFromXlsxParts, loadZip } from './xlsx-document.js';
|
|
|
14
14
|
import { sheetStateFromOds } from './ods-document.js';
|
|
15
15
|
import { sheetStateFromCsv } from './csv.js';
|
|
16
16
|
import { isXlsFile, sheetStateFromXls } from './xls-document.js';
|
|
17
|
+
import { createUnzipBudget, readZipEntry } from '../xlsx-limits.js';
|
|
17
18
|
/** A picture travels as the data URL the document holds, as in the xlsx reader. */
|
|
18
19
|
const MEDIA_PREFIX = 'xl/media/';
|
|
19
20
|
const mediaTypeOf = (path) => {
|
|
@@ -31,6 +32,7 @@ export async function readSpreadsheetFile(file, JSZip) {
|
|
|
31
32
|
}
|
|
32
33
|
const Zip = await loadZip(JSZip);
|
|
33
34
|
const zip = await Zip.loadAsync(file);
|
|
35
|
+
const budget = createUnzipBudget();
|
|
34
36
|
const parts = {};
|
|
35
37
|
const pending = [];
|
|
36
38
|
zip.forEach((path, entry) => {
|
|
@@ -38,12 +40,12 @@ export async function readSpreadsheetFile(file, JSZip) {
|
|
|
38
40
|
return;
|
|
39
41
|
const clean = path.replace(/^\//, '');
|
|
40
42
|
if (clean.startsWith(MEDIA_PREFIX)) {
|
|
41
|
-
pending.push(entry
|
|
43
|
+
pending.push(readZipEntry(entry, 'base64', budget).then((base64) => { parts[clean] = `data:${mediaTypeOf(clean)};base64,${base64}`; }));
|
|
42
44
|
return;
|
|
43
45
|
}
|
|
44
46
|
if (!/\.(xml|rels|vml)$/i.test(clean))
|
|
45
47
|
return;
|
|
46
|
-
pending.push(entry
|
|
48
|
+
pending.push(readZipEntry(entry, 'string', budget).then((text) => { parts[clean] = text; }));
|
|
47
49
|
});
|
|
48
50
|
await Promise.all(pending);
|
|
49
51
|
if (parts['content.xml'] && !parts['xl/workbook.xml'])
|