@voila.dev/ui-spreadsheet 1.1.9
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/package.json +58 -0
- package/src/components/nested-table-input.tsx +85 -0
- package/src/components/spreadsheet.tsx +1288 -0
- package/src/hooks/use-spreadsheet-column-sizing.ts +120 -0
- package/src/hooks/use-spreadsheet-drag.ts +617 -0
- package/src/hooks/use-spreadsheet-grid.ts +743 -0
- package/src/hooks/use-spreadsheet-image-drop.ts +114 -0
- package/src/styles.css +10 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
/** Widths outside this range are dropped as corrupt when read back. */
|
|
4
|
+
const MINIMUM_WIDTH = 32;
|
|
5
|
+
const MAXIMUM_WIDTH = 1200;
|
|
6
|
+
|
|
7
|
+
/** A width worth restoring: finite and inside the range a column can live at. */
|
|
8
|
+
function isPlausibleWidth(width: unknown): width is number {
|
|
9
|
+
return (
|
|
10
|
+
typeof width === "number" &&
|
|
11
|
+
Number.isFinite(width) &&
|
|
12
|
+
width >= MINIMUM_WIDTH &&
|
|
13
|
+
width <= MAXIMUM_WIDTH
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** The stored entry as a plain object, or null when it is absent or corrupt. */
|
|
18
|
+
function readStoredEntry(raw: string | null): Record<string, unknown> | null {
|
|
19
|
+
if (raw === null) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
const parsed: unknown = JSON.parse(raw);
|
|
24
|
+
const isObject =
|
|
25
|
+
typeof parsed === "object" && parsed !== null && !Array.isArray(parsed);
|
|
26
|
+
return isObject ? (parsed as Record<string, unknown>) : null;
|
|
27
|
+
} catch {
|
|
28
|
+
return null; // Corrupt entry: fall back to the defaults rather than throw.
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Parses a persisted sizing map, keeping only entries that are still plausible:
|
|
34
|
+
* a finite width in range, under a column id the table still declares. Stored
|
|
35
|
+
* widths outlive the code that wrote them - a renamed or dropped column, a
|
|
36
|
+
* hand-edited entry, or a half-written value must not be able to wedge a table
|
|
37
|
+
* at 4px wide with no way back but clearing site data.
|
|
38
|
+
*/
|
|
39
|
+
function parseStoredSizing(
|
|
40
|
+
raw: string | null,
|
|
41
|
+
columnIds: ReadonlySet<string>,
|
|
42
|
+
): Record<string, number> {
|
|
43
|
+
const entry = readStoredEntry(raw);
|
|
44
|
+
if (entry === null) {
|
|
45
|
+
return {};
|
|
46
|
+
}
|
|
47
|
+
const sizing: Record<string, number> = {};
|
|
48
|
+
for (const [columnId, width] of Object.entries(entry)) {
|
|
49
|
+
if (columnIds.has(columnId) && isPlausibleWidth(width)) {
|
|
50
|
+
sizing[columnId] = Math.round(width);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return sizing;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Column widths for an `Spreadsheet`, persisted per user in `localStorage`.
|
|
58
|
+
* Returns the `columnSizing` / `onColumnSizingChange` pair to spread on the
|
|
59
|
+
* table, plus `reset` for a "restore default widths" action.
|
|
60
|
+
*
|
|
61
|
+
* `columnIds` fences what may be read back (see `parseStoredSizing`), so pass
|
|
62
|
+
* the table's declared ids. Only resized columns are stored - a column the user
|
|
63
|
+
* never touched keeps following the `width` its `SpreadsheetColumn` declares,
|
|
64
|
+
* which is what lets a later default-width change reach existing users.
|
|
65
|
+
*
|
|
66
|
+
* Read lazily on mount rather than during render: the app hydrates on the
|
|
67
|
+
* server too, where `localStorage` does not exist, and a first paint at the
|
|
68
|
+
* default widths beats one at zero.
|
|
69
|
+
*/
|
|
70
|
+
function useSpreadsheetColumnSizing({
|
|
71
|
+
storageKey,
|
|
72
|
+
columnIds,
|
|
73
|
+
}: {
|
|
74
|
+
/** Stable, namespaced, e.g. "acme.dev.shop-variants.column-sizing". */
|
|
75
|
+
storageKey: string;
|
|
76
|
+
columnIds: readonly string[];
|
|
77
|
+
}): {
|
|
78
|
+
columnSizing: Record<string, number>;
|
|
79
|
+
onColumnSizingChange: (sizing: Record<string, number>) => void;
|
|
80
|
+
reset: () => void;
|
|
81
|
+
} {
|
|
82
|
+
const [columnSizing, setColumnSizing] = React.useState<
|
|
83
|
+
Record<string, number>
|
|
84
|
+
>({});
|
|
85
|
+
// The ids are declared inline by the consumer, so a fresh array arrives on
|
|
86
|
+
// every render; the joined key is what actually changes.
|
|
87
|
+
const columnIdsKey = columnIds.join("|");
|
|
88
|
+
|
|
89
|
+
React.useEffect(() => {
|
|
90
|
+
if (typeof window === "undefined") {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
setColumnSizing(
|
|
94
|
+
parseStoredSizing(
|
|
95
|
+
window.localStorage.getItem(storageKey),
|
|
96
|
+
new Set(columnIdsKey.split("|")),
|
|
97
|
+
),
|
|
98
|
+
);
|
|
99
|
+
}, [storageKey, columnIdsKey]);
|
|
100
|
+
|
|
101
|
+
const write = (sizing: Record<string, number>) => {
|
|
102
|
+
setColumnSizing(sizing);
|
|
103
|
+
if (typeof window === "undefined") {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
window.localStorage.setItem(storageKey, JSON.stringify(sizing));
|
|
108
|
+
} catch {
|
|
109
|
+
// Private mode or a full quota: the widths still apply this session.
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
columnSizing,
|
|
115
|
+
onColumnSizingChange: write,
|
|
116
|
+
reset: () => write({}),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export { useSpreadsheetColumnSizing };
|