gbs-add-block 1.2.7 → 1.2.8

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 (41) hide show
  1. package/README.md +3 -3
  2. package/index.cjs +9 -9
  3. package/package.json +1 -1
  4. package/source/components/datagridbeta/README.md +232 -0
  5. package/source/components/datagridbeta/__tests__/core.test.ts +341 -0
  6. package/source/components/datagridbeta/__tests__/export.test.ts +70 -0
  7. package/source/components/datagridbeta/core/columnHelper.ts +21 -0
  8. package/source/components/datagridbeta/core/columns.ts +288 -0
  9. package/source/components/datagridbeta/core/filtering.ts +237 -0
  10. package/source/components/datagridbeta/core/grid.ts +616 -0
  11. package/source/components/datagridbeta/core/index.ts +14 -0
  12. package/source/components/datagridbeta/core/rows.ts +64 -0
  13. package/source/components/datagridbeta/core/sorting.ts +111 -0
  14. package/source/components/datagridbeta/core/state.ts +82 -0
  15. package/source/components/datagridbeta/core/store.ts +52 -0
  16. package/source/components/datagridbeta/core/types.ts +266 -0
  17. package/source/components/datagridbeta/core/values.ts +98 -0
  18. package/source/components/datagridbeta/core/virtual.ts +85 -0
  19. package/source/components/datagridbeta/export/csv.ts +32 -0
  20. package/source/components/datagridbeta/export/download.ts +12 -0
  21. package/source/components/datagridbeta/export/pdf.ts +61 -0
  22. package/source/components/datagridbeta/export/table.ts +73 -0
  23. package/source/components/datagridbeta/export/xlsx.ts +145 -0
  24. package/source/components/datagridbeta/export/zip.ts +87 -0
  25. package/source/components/datagridbeta/index.ts +7 -0
  26. package/source/components/datagridbeta/react/Cell.tsx +282 -0
  27. package/source/components/datagridbeta/react/ColumnMenu.tsx +135 -0
  28. package/source/components/datagridbeta/react/DataGrid.tsx +296 -0
  29. package/source/components/datagridbeta/react/FilterForm.tsx +142 -0
  30. package/source/components/datagridbeta/react/HeaderRow.tsx +306 -0
  31. package/source/components/datagridbeta/react/Pagination.tsx +107 -0
  32. package/source/components/datagridbeta/react/Popover.tsx +79 -0
  33. package/source/components/datagridbeta/react/Row.tsx +85 -0
  34. package/source/components/datagridbeta/react/Toolbar.tsx +266 -0
  35. package/source/components/datagridbeta/react/Viewport.tsx +167 -0
  36. package/source/components/datagridbeta/react/context.ts +57 -0
  37. package/source/components/datagridbeta/react/hooks.ts +80 -0
  38. package/source/components/datagridbeta/react/icons.tsx +72 -0
  39. package/source/components/datagridbeta/react/keyboard.ts +110 -0
  40. package/source/components/datagridbeta/react/locale.ts +128 -0
  41. package/source/components/datagridbeta/styles.css +717 -0
@@ -0,0 +1,32 @@
1
+ import type { ExportTable } from "./table";
2
+
3
+ /** Spreadsheet apps execute cells starting with these characters as formulas. */
4
+ const FORMULA_PREFIX = /^[=+\-@\t\r]/;
5
+
6
+ function escapeField(text: string, isText: boolean, delimiter: string): string {
7
+ const safe = isText && FORMULA_PREFIX.test(text) ? `'${text}` : text;
8
+ const needsQuotes =
9
+ safe.includes(delimiter) || safe.includes('"') || safe.includes("\n") || safe.includes("\r");
10
+ return needsQuotes ? `"${safe.replace(/"/g, '""')}"` : safe;
11
+ }
12
+
13
+ function serialize(table: ExportTable, delimiter: string): string {
14
+ const lines = [table.columns.map((c) => escapeField(c.header, true, delimiter)).join(delimiter)];
15
+ table.text.forEach((row, r) => {
16
+ lines.push(
17
+ row
18
+ .map((text, c) => escapeField(text, typeof table.values[r][c] === "string", delimiter))
19
+ .join(delimiter),
20
+ );
21
+ });
22
+ return lines.join("\r\n");
23
+ }
24
+
25
+ export const toCsv = (table: ExportTable) => serialize(table, ",");
26
+
27
+ export const toTsv = (table: ExportTable) => serialize(table, "\t");
28
+
29
+ export function createCsvBlob(table: ExportTable): Blob {
30
+ // The BOM makes Excel read the file as UTF-8.
31
+ return new Blob(["", toCsv(table)], { type: "text/csv;charset=utf-8" });
32
+ }
@@ -0,0 +1,12 @@
1
+ export function downloadBlob(blob: Blob, fileName: string): void {
2
+ const url = URL.createObjectURL(blob);
3
+ const link = document.createElement("a");
4
+ link.href = url;
5
+ link.download = fileName;
6
+ link.rel = "noopener";
7
+ link.style.display = "none";
8
+ document.body.append(link);
9
+ link.click();
10
+ link.remove();
11
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
12
+ }
@@ -0,0 +1,61 @@
1
+ import type { ExportTable } from "./table";
2
+
3
+ export interface PrintOptions {
4
+ title: string;
5
+ orientation: "portrait" | "landscape";
6
+ paperSize: "A3" | "A4" | "A5" | "letter" | "legal";
7
+ }
8
+
9
+ const escapeHtml = (text: string) =>
10
+ text.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
11
+
12
+ export function buildPrintHtml(table: ExportTable, options: PrintOptions): string {
13
+ const head = table.columns.map((c) => `<th>${escapeHtml(c.header)}</th>`).join("");
14
+ const numeric = table.columns.map((c) => c.type === "number");
15
+ const body = table.text
16
+ .map(
17
+ (row) =>
18
+ `<tr>${row
19
+ .map((text, i) => `<td${numeric[i] ? ' class="num"' : ""}>${escapeHtml(text)}</td>`)
20
+ .join("")}</tr>`,
21
+ )
22
+ .join("");
23
+
24
+ return `<!doctype html><html><head><meta charset="utf-8"><title>${escapeHtml(options.title)}</title>
25
+ <style>
26
+ @page { size: ${options.paperSize} ${options.orientation}; margin: 12mm; }
27
+ body { font: 9pt/1.35 system-ui, -apple-system, "Segoe UI", sans-serif; color: #111; margin: 0; }
28
+ h1 { font-size: 13pt; margin: 0 0 8px; }
29
+ table { border-collapse: collapse; width: 100%; }
30
+ th, td { border: 1px solid #c8c8c8; padding: 3px 6px; text-align: start; vertical-align: top; }
31
+ th { background: #f0f0f0; font-weight: 600; }
32
+ thead { display: table-header-group; }
33
+ tr { break-inside: avoid; }
34
+ td.num { text-align: end; font-variant-numeric: tabular-nums; }
35
+ </style></head><body><h1>${escapeHtml(options.title)}</h1>
36
+ <table><thead><tr>${head}</tr></thead><tbody>${body}</tbody></table></body></html>`;
37
+ }
38
+
39
+ /**
40
+ * Opens the browser's print dialog with the table laid out for paper, where
41
+ * the user picks "Save as PDF". No PDF library is bundled.
42
+ */
43
+ export function printTable(table: ExportTable, options: PrintOptions): void {
44
+ const iframe = document.createElement("iframe");
45
+ iframe.setAttribute("aria-hidden", "true");
46
+ iframe.style.cssText = "position:fixed;right:0;bottom:0;width:0;height:0;border:0;";
47
+
48
+ const cleanup = () => iframe.remove();
49
+ iframe.addEventListener("load", () => {
50
+ const win = iframe.contentWindow;
51
+ if (!win) return cleanup();
52
+ win.addEventListener("afterprint", cleanup, { once: true });
53
+ win.focus();
54
+ win.print();
55
+ // Fallback for browsers that never fire afterprint.
56
+ setTimeout(cleanup, 60_000);
57
+ }, { once: true });
58
+
59
+ iframe.srcdoc = buildPrintHtml(table, options);
60
+ document.body.append(iframe);
61
+ }
@@ -0,0 +1,73 @@
1
+ import type { ColumnType, ExportCellValue, ResolvedColumn } from "../core/types";
2
+ import { formatCellValue, toTime, type Formatters } from "../core/values";
3
+
4
+ export interface ExportColumn {
5
+ header: string;
6
+ width: number;
7
+ type: ColumnType;
8
+ }
9
+
10
+ export interface ExportTable {
11
+ columns: ExportColumn[];
12
+ /** Typed values, for formats that keep types (XLSX). */
13
+ values: ExportCellValue[][];
14
+ /** Display text, for text formats (CSV, PDF, clipboard). */
15
+ text: string[][];
16
+ }
17
+
18
+ function typedValue<T>(column: ResolvedColumn<T>, raw: unknown, text: string): ExportCellValue {
19
+ if (raw === null || raw === undefined) return null;
20
+ if (column.def.options) return text;
21
+ if (typeof raw === "number" || typeof raw === "boolean") return raw;
22
+ if (raw instanceof Date) return raw;
23
+ if (column.type === "date") {
24
+ const time = toTime(raw);
25
+ return time === null ? text : new Date(time);
26
+ }
27
+ return text;
28
+ }
29
+
30
+ export function buildExportTable<T>(
31
+ rows: readonly T[],
32
+ items: readonly { column: ResolvedColumn<T>; width: number }[],
33
+ formatters: Formatters,
34
+ ): ExportTable {
35
+ const values: ExportCellValue[][] = [];
36
+ const text: string[][] = [];
37
+
38
+ for (const row of rows) {
39
+ const rowValues: ExportCellValue[] = [];
40
+ const rowText: string[] = [];
41
+ for (const { column } of items) {
42
+ const { exportValue } = column.def;
43
+ if (exportValue) {
44
+ const value = exportValue(row);
45
+ rowValues.push(value);
46
+ rowText.push(
47
+ value === null || value === undefined
48
+ ? ""
49
+ : value instanceof Date
50
+ ? formatters.date.format(value)
51
+ : String(value),
52
+ );
53
+ } else {
54
+ const raw = column.getValue(row);
55
+ const display = formatCellValue(column, raw, row, formatters);
56
+ rowValues.push(typedValue(column, raw, display));
57
+ rowText.push(display);
58
+ }
59
+ }
60
+ values.push(rowValues);
61
+ text.push(rowText);
62
+ }
63
+
64
+ return {
65
+ columns: items.map(({ column, width }) => ({
66
+ header: column.header,
67
+ width,
68
+ type: column.type,
69
+ })),
70
+ values,
71
+ text,
72
+ };
73
+ }
@@ -0,0 +1,145 @@
1
+ import type { ExportCellValue } from "../core/types";
2
+ import type { ExportTable } from "./table";
3
+ import { createZip } from "./zip";
4
+
5
+ const XML_HEADER = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>';
6
+ const MAIN_NS = "http://schemas.openxmlformats.org/spreadsheetml/2006/main";
7
+ const REL_NS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
8
+ const PKG_REL_NS = "http://schemas.openxmlformats.org/package/2006/relationships";
9
+
10
+ /** Characters XML 1.0 does not allow at all. */
11
+ const INVALID_XML_CHARS = /[^\t\n\r -퟿-�\u{10000}-\u{10FFFF}]/gu;
12
+
13
+ const XML_ENTITIES: Record<string, string> = {
14
+ "&": "&amp;",
15
+ "<": "&lt;",
16
+ ">": "&gt;",
17
+ '"': "&quot;",
18
+ };
19
+
20
+ export const escapeXml = (text: string) =>
21
+ text.replace(INVALID_XML_CHARS, "").replace(/[&<>"]/g, (c) => XML_ENTITIES[c]);
22
+
23
+ export function columnName(index: number): string {
24
+ let n = index + 1;
25
+ let name = "";
26
+ while (n > 0) {
27
+ const remainder = (n - 1) % 26;
28
+ name = String.fromCharCode(65 + remainder) + name;
29
+ n = Math.floor((n - 1) / 26);
30
+ }
31
+ return name;
32
+ }
33
+
34
+ /** Excel serial date in local time. */
35
+ function toSerialDate(date: Date): number {
36
+ return (date.getTime() - date.getTimezoneOffset() * 60_000) / 86_400_000 + 25_569;
37
+ }
38
+
39
+ // Style indexes into cellXfs below.
40
+ const STYLE_HEADER = 1;
41
+ const STYLE_DATE = 2;
42
+ const STYLE_DATETIME = 3;
43
+
44
+ function cell(ref: string, value: ExportCellValue, style = 0): string {
45
+ const s = style ? ` s="${style}"` : "";
46
+ if (value === null || value === undefined || value === "") return "";
47
+ if (typeof value === "number") {
48
+ return Number.isFinite(value) ? `<c r="${ref}"${s}><v>${value}</v></c>` : "";
49
+ }
50
+ if (typeof value === "boolean") return `<c r="${ref}" t="b"${s}><v>${value ? 1 : 0}</v></c>`;
51
+ if (value instanceof Date) {
52
+ if (Number.isNaN(value.getTime())) return "";
53
+ const serial = toSerialDate(value);
54
+ const dateStyle = Number.isInteger(serial) ? STYLE_DATE : STYLE_DATETIME;
55
+ return `<c r="${ref}" s="${dateStyle}"><v>${serial}</v></c>`;
56
+ }
57
+ return `<c r="${ref}" t="inlineStr"${s}><is><t xml:space="preserve">${escapeXml(value)}</t></is></c>`;
58
+ }
59
+
60
+ const STYLES = `${XML_HEADER}<styleSheet xmlns="${MAIN_NS}">\
61
+ <numFmts count="2"><numFmt numFmtId="164" formatCode="yyyy-mm-dd"/><numFmt numFmtId="165" formatCode="yyyy-mm-dd hh:mm"/></numFmts>\
62
+ <fonts count="2"><font><sz val="11"/><name val="Calibri"/></font><font><b/><sz val="11"/><name val="Calibri"/></font></fonts>\
63
+ <fills count="2"><fill><patternFill patternType="none"/></fill><fill><patternFill patternType="gray125"/></fill></fills>\
64
+ <borders count="1"><border><left/><right/><top/><bottom/><diagonal/></border></borders>\
65
+ <cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>\
66
+ <cellXfs count="4"><xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/>\
67
+ <xf numFmtId="0" fontId="1" fillId="0" borderId="0" xfId="0" applyFont="1"/>\
68
+ <xf numFmtId="164" fontId="0" fillId="0" borderId="0" xfId="0" applyNumberFormat="1"/>\
69
+ <xf numFmtId="165" fontId="0" fillId="0" borderId="0" xfId="0" applyNumberFormat="1"/></cellXfs>\
70
+ <cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles></styleSheet>`;
71
+
72
+ export function createXlsxBlob(table: ExportTable, sheetName = "Sheet1"): Blob {
73
+ const encoder = new TextEncoder();
74
+ const name = sheetName.replace(/[[\]:*?/\\]/g, " ").slice(0, 31) || "Sheet1";
75
+ const columnCount = Math.max(1, table.columns.length);
76
+ const lastRef = `${columnName(columnCount - 1)}${table.values.length + 1}`;
77
+
78
+ const header =
79
+ `${XML_HEADER}<worksheet xmlns="${MAIN_NS}">` +
80
+ `<sheetViews><sheetView workbookViewId="0"><pane ySplit="1" topLeftCell="A2" activePane="bottomLeft" state="frozen"/></sheetView></sheetViews>` +
81
+ `<cols>${table.columns
82
+ .map((c, i) => `<col min="${i + 1}" max="${i + 1}" width="${Math.max(8, Math.round(c.width / 7))}" customWidth="1"/>`)
83
+ .join("")}</cols>` +
84
+ `<sheetData><row r="1">${table.columns
85
+ .map((c, i) => cell(`${columnName(i)}1`, c.header, STYLE_HEADER))
86
+ .join("")}</row>`;
87
+
88
+ // Encode in chunks to keep peak memory low on large exports.
89
+ const chunks: Uint8Array[] = [encoder.encode(header)];
90
+ const names = table.columns.map((_, i) => columnName(i));
91
+ const CHUNK_ROWS = 2000;
92
+ for (let start = 0; start < table.values.length; start += CHUNK_ROWS) {
93
+ let xml = "";
94
+ const end = Math.min(table.values.length, start + CHUNK_ROWS);
95
+ for (let r = start; r < end; r++) {
96
+ const rowNumber = r + 2;
97
+ xml += `<row r="${rowNumber}">`;
98
+ table.values[r].forEach((value, c) => {
99
+ xml += cell(`${names[c]}${rowNumber}`, value);
100
+ });
101
+ xml += "</row>";
102
+ }
103
+ chunks.push(encoder.encode(xml));
104
+ }
105
+ chunks.push(encoder.encode(`</sheetData><autoFilter ref="A1:${lastRef}"/></worksheet>`));
106
+
107
+ const escapedName = escapeXml(name).replace(/'/g, "''");
108
+ const workbook =
109
+ `${XML_HEADER}<workbook xmlns="${MAIN_NS}" xmlns:r="${REL_NS}">` +
110
+ `<sheets><sheet name="${escapeXml(name)}" sheetId="1" r:id="rId1"/></sheets>` +
111
+ `<definedNames><definedName name="_xlnm._FilterDatabase" localSheetId="0" hidden="1">'${escapedName}'!$A$1:$${columnName(columnCount - 1)}$${table.values.length + 1}</definedName></definedNames>` +
112
+ `</workbook>`;
113
+
114
+ return createZip(
115
+ [
116
+ {
117
+ name: "[Content_Types].xml",
118
+ content:
119
+ `${XML_HEADER}<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">` +
120
+ `<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>` +
121
+ `<Default Extension="xml" ContentType="application/xml"/>` +
122
+ `<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>` +
123
+ `<Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>` +
124
+ `<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>` +
125
+ `</Types>`,
126
+ },
127
+ {
128
+ name: "_rels/.rels",
129
+ content: `${XML_HEADER}<Relationships xmlns="${PKG_REL_NS}"><Relationship Id="rId1" Type="${REL_NS}/officeDocument" Target="xl/workbook.xml"/></Relationships>`,
130
+ },
131
+ { name: "xl/workbook.xml", content: workbook },
132
+ {
133
+ name: "xl/_rels/workbook.xml.rels",
134
+ content:
135
+ `${XML_HEADER}<Relationships xmlns="${PKG_REL_NS}">` +
136
+ `<Relationship Id="rId1" Type="${REL_NS}/worksheet" Target="worksheets/sheet1.xml"/>` +
137
+ `<Relationship Id="rId2" Type="${REL_NS}/styles" Target="styles.xml"/>` +
138
+ `</Relationships>`,
139
+ },
140
+ { name: "xl/styles.xml", content: STYLES },
141
+ { name: "xl/worksheets/sheet1.xml", content: chunks },
142
+ ],
143
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
144
+ );
145
+ }
@@ -0,0 +1,87 @@
1
+ /** Minimal ZIP writer (stored, uncompressed), enough for XLSX packages. */
2
+
3
+ const CRC_TABLE = (() => {
4
+ const table = new Uint32Array(256);
5
+ for (let n = 0; n < 256; n++) {
6
+ let c = n;
7
+ for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
8
+ table[n] = c >>> 0;
9
+ }
10
+ return table;
11
+ })();
12
+
13
+ export function crc32(chunks: readonly Uint8Array[]): number {
14
+ let crc = 0xffffffff;
15
+ for (const chunk of chunks) {
16
+ for (let i = 0; i < chunk.length; i++) {
17
+ crc = CRC_TABLE[(crc ^ chunk[i]) & 0xff] ^ (crc >>> 8);
18
+ }
19
+ }
20
+ return (crc ^ 0xffffffff) >>> 0;
21
+ }
22
+
23
+ export interface ZipEntry {
24
+ name: string;
25
+ /** Content as text, or pre-encoded chunks for large files. */
26
+ content: string | Uint8Array[];
27
+ }
28
+
29
+ export function createZip(entries: readonly ZipEntry[], type = "application/zip"): Blob {
30
+ const encoder = new TextEncoder();
31
+ const now = new Date();
32
+ const dosTime = (now.getHours() << 11) | (now.getMinutes() << 5) | (now.getSeconds() >> 1);
33
+ const dosDate =
34
+ ((now.getFullYear() - 1980) << 9) | ((now.getMonth() + 1) << 5) | now.getDate();
35
+
36
+ const parts: Uint8Array[] = [];
37
+ const central: Uint8Array[] = [];
38
+ let offset = 0;
39
+
40
+ for (const entry of entries) {
41
+ const name = encoder.encode(entry.name);
42
+ const chunks = typeof entry.content === "string" ? [encoder.encode(entry.content)] : entry.content;
43
+ const size = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
44
+ const crc = crc32(chunks);
45
+
46
+ const local = new DataView(new ArrayBuffer(30));
47
+ local.setUint32(0, 0x04034b50, true);
48
+ local.setUint16(4, 20, true);
49
+ local.setUint16(6, 0x0800, true); // UTF-8 names
50
+ local.setUint16(8, 0, true); // stored
51
+ local.setUint16(10, dosTime, true);
52
+ local.setUint16(12, dosDate, true);
53
+ local.setUint32(14, crc, true);
54
+ local.setUint32(18, size, true);
55
+ local.setUint32(22, size, true);
56
+ local.setUint16(26, name.length, true);
57
+ local.setUint16(28, 0, true);
58
+ parts.push(new Uint8Array(local.buffer), name, ...chunks);
59
+
60
+ const header = new DataView(new ArrayBuffer(46));
61
+ header.setUint32(0, 0x02014b50, true);
62
+ header.setUint16(4, 20, true);
63
+ header.setUint16(6, 20, true);
64
+ header.setUint16(8, 0x0800, true);
65
+ header.setUint16(10, 0, true);
66
+ header.setUint16(12, dosTime, true);
67
+ header.setUint16(14, dosDate, true);
68
+ header.setUint32(16, crc, true);
69
+ header.setUint32(20, size, true);
70
+ header.setUint32(24, size, true);
71
+ header.setUint16(28, name.length, true);
72
+ header.setUint32(42, offset, true);
73
+ central.push(new Uint8Array(header.buffer), name);
74
+
75
+ offset += 30 + name.length + size;
76
+ }
77
+
78
+ const centralSize = central.reduce((sum, part) => sum + part.length, 0);
79
+ const end = new DataView(new ArrayBuffer(22));
80
+ end.setUint32(0, 0x06054b50, true);
81
+ end.setUint16(8, entries.length, true);
82
+ end.setUint16(10, entries.length, true);
83
+ end.setUint32(12, centralSize, true);
84
+ end.setUint32(16, offset, true);
85
+
86
+ return new Blob([...parts, ...central, new Uint8Array(end.buffer)] as BlobPart[], { type });
87
+ }
@@ -0,0 +1,7 @@
1
+ export { DataGrid } from "./react/DataGrid";
2
+ export type { DataGridProps } from "./react/DataGrid";
3
+ export type { GridSlot } from "./react/context";
4
+ export type { ToolbarOptions } from "./react/Toolbar";
5
+ export { defaultLocaleText } from "./react/locale";
6
+ export type { LocaleText } from "./react/locale";
7
+ export * from "./core";