@lnsy/data-table 0.1.0

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/src/file-io.js ADDED
@@ -0,0 +1,203 @@
1
+ /**
2
+ * File I/O Module
3
+ *
4
+ * Load, save, import, and export for <data-table>.
5
+ * Formats: .lss (native), CSV, JSON, XLSX/XLS/ODS (import only).
6
+ *
7
+ * Single-sheet: all formats operate on one rectangular data grid.
8
+ */
9
+
10
+ import * as XLSX from 'xlsx';
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // Helpers
14
+ // ---------------------------------------------------------------------------
15
+
16
+ function triggerDownload(blob, filename) {
17
+ const url = URL.createObjectURL(blob);
18
+ const a = document.createElement('a');
19
+ a.href = url;
20
+ a.download = filename;
21
+ document.body.appendChild(a);
22
+ a.click();
23
+ document.body.removeChild(a);
24
+ URL.revokeObjectURL(url);
25
+ }
26
+
27
+ function readFileAsText(file) {
28
+ return new Promise((resolve, reject) => {
29
+ const reader = new FileReader();
30
+ reader.onload = () => resolve(reader.result);
31
+ reader.onerror = () => reject(reader.error);
32
+ reader.readAsText(file);
33
+ });
34
+ }
35
+
36
+ function readFileAsArrayBuffer(file) {
37
+ return new Promise((resolve, reject) => {
38
+ const reader = new FileReader();
39
+ reader.onload = () => resolve(reader.result);
40
+ reader.onerror = () => reject(reader.error);
41
+ reader.readAsArrayBuffer(file);
42
+ });
43
+ }
44
+
45
+ export function createHiddenFileInput(accept, callback) {
46
+ const input = document.createElement('input');
47
+ input.type = 'file';
48
+ input.accept = accept;
49
+ input.style.display = 'none';
50
+ input.onchange = (e) => {
51
+ const file = e.target.files[0];
52
+ if (file) callback(file);
53
+ document.body.removeChild(input);
54
+ };
55
+ document.body.appendChild(input);
56
+ input.click();
57
+ }
58
+
59
+ /** Normalize imported rows into an array-of-arrays of strings. */
60
+ function normalizeData(data) {
61
+ if (!Array.isArray(data)) return [];
62
+ return data
63
+ .filter((row) => Array.isArray(row))
64
+ .map((row) =>
65
+ row.map((cell) => (cell === null || cell === undefined ? '' : String(cell)))
66
+ );
67
+ }
68
+
69
+ /**
70
+ * Drop trailing all-empty rows and columns so exports don't carry the
71
+ * spreadsheet's padding grid.
72
+ */
73
+ function trimTrailingEmpties(data) {
74
+ const rows = data.filter((row) => row.some((cell) => cell !== ''));
75
+ if (rows.length === 0) return [];
76
+ let lastCol = 0;
77
+ for (const row of rows) {
78
+ for (let c = row.length - 1; c >= 0; c--) {
79
+ if (row[c] !== '') {
80
+ lastCol = Math.max(lastCol, c);
81
+ break;
82
+ }
83
+ }
84
+ }
85
+ return rows.map((row) => row.slice(0, lastCol + 1));
86
+ }
87
+
88
+ function applyToComponent(component, data) {
89
+ component.setData(normalizeData(data));
90
+ }
91
+
92
+ // ---------------------------------------------------------------------------
93
+ // .lss — LNSY Spread Sheet (native format)
94
+ // ---------------------------------------------------------------------------
95
+
96
+ export function saveLSS(component, filename) {
97
+ const payload = {
98
+ version: '3.0',
99
+ format: 'lnsy-spreadsheet',
100
+ createdAt: new Date().toISOString(),
101
+ // Single sheet. Formulas are stored as raw "=…" strings.
102
+ sheet: {
103
+ name: 'Sheet1',
104
+ data: trimTrailingEmpties(component.getData()),
105
+ },
106
+ };
107
+
108
+ const blob = new Blob([JSON.stringify(payload, null, 2)], {
109
+ type: 'application/json',
110
+ });
111
+ triggerDownload(blob, filename || 'table.lss');
112
+ }
113
+
114
+ export async function loadLSS(component, file) {
115
+ const text = await readFileAsText(file);
116
+ let payload;
117
+ try {
118
+ payload = JSON.parse(text);
119
+ } catch {
120
+ throw new Error('Invalid .lss file: not valid JSON');
121
+ }
122
+
123
+ if (payload.format !== 'lnsy-spreadsheet') {
124
+ throw new Error('Invalid .lss file: unrecognized format');
125
+ }
126
+
127
+ // v1.0/v2.0 stored `data` (+ optional sheets array); v3.0 stores `sheet`.
128
+ let data;
129
+ if (payload.sheet?.data) {
130
+ data = payload.sheet.data;
131
+ } else if (Array.isArray(payload.sheets) && payload.sheets.length > 0) {
132
+ data = payload.sheets[0].data; // single sheet now: take the first
133
+ } else if (Array.isArray(payload.data)) {
134
+ data = payload.data;
135
+ } else {
136
+ return;
137
+ }
138
+
139
+ applyToComponent(component, data);
140
+ }
141
+
142
+ // ---------------------------------------------------------------------------
143
+ // CSV
144
+ // ---------------------------------------------------------------------------
145
+
146
+ export function exportCSV(component, filename) {
147
+ // Like spreadsheet apps, CSV exchange uses computed values.
148
+ // (.lss keeps raw formulas; see saveLSS.)
149
+ const data = trimTrailingEmpties(component.getData()).map((row, r) =>
150
+ row.map((cell, c) =>
151
+ typeof cell === 'string' && cell.startsWith('=')
152
+ ? String(component.getComputedValue(r, c))
153
+ : cell
154
+ )
155
+ );
156
+ const ws = XLSX.utils.aoa_to_sheet(data);
157
+ const csv = XLSX.utils.sheet_to_csv(ws);
158
+ triggerDownload(new Blob([csv], { type: 'text/csv' }), filename || 'table.csv');
159
+ }
160
+
161
+ export async function importCSV(component, file) {
162
+ const buffer = await readFileAsArrayBuffer(file);
163
+ const workbook = XLSX.read(buffer, { type: 'array' });
164
+ const sheet = workbook.Sheets[workbook.SheetNames[0]];
165
+ applyToComponent(component, XLSX.utils.sheet_to_json(sheet, { header: 1 }));
166
+ }
167
+
168
+ // ---------------------------------------------------------------------------
169
+ // JSON
170
+ // ---------------------------------------------------------------------------
171
+
172
+ export function exportJSON(component, filename) {
173
+ const data = trimTrailingEmpties(component.getData());
174
+ triggerDownload(
175
+ new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }),
176
+ filename || 'table.json'
177
+ );
178
+ }
179
+
180
+ export async function importJSON(component, file) {
181
+ const text = await readFileAsText(file);
182
+ let data;
183
+ try {
184
+ data = JSON.parse(text);
185
+ } catch {
186
+ throw new Error('Invalid JSON file');
187
+ }
188
+ if (!Array.isArray(data)) {
189
+ throw new Error('Invalid JSON file: expected a two-dimensional array');
190
+ }
191
+ applyToComponent(component, data);
192
+ }
193
+
194
+ // ---------------------------------------------------------------------------
195
+ // XLSX / XLS / ODS (import)
196
+ // ---------------------------------------------------------------------------
197
+
198
+ export async function importSpreadsheetFile(component, file) {
199
+ const buffer = await readFileAsArrayBuffer(file);
200
+ const workbook = XLSX.read(buffer, { type: 'array' });
201
+ const sheet = workbook.Sheets[workbook.SheetNames[0]];
202
+ applyToComponent(component, XLSX.utils.sheet_to_json(sheet, { header: 1 }));
203
+ }