@lotics/xlsx 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/package.json +30 -0
- package/src/auto_sum.test.ts +71 -0
- package/src/auto_sum.ts +83 -0
- package/src/canvas_cf.ts +135 -0
- package/src/canvas_chart.ts +687 -0
- package/src/canvas_fill.ts +156 -0
- package/src/canvas_images.ts +99 -0
- package/src/canvas_layout.test.ts +159 -0
- package/src/canvas_layout.ts +239 -0
- package/src/canvas_renderer.ts +1010 -0
- package/src/canvas_shape.ts +242 -0
- package/src/canvas_sparkline.ts +163 -0
- package/src/canvas_text.ts +454 -0
- package/src/cell_editor.ts +558 -0
- package/src/clipboard.test.ts +145 -0
- package/src/clipboard.ts +341 -0
- package/src/command_history.ts +102 -0
- package/src/csv_parser.test.ts +130 -0
- package/src/csv_parser.ts +172 -0
- package/src/data_validation.test.ts +95 -0
- package/src/data_validation.ts +114 -0
- package/src/editing_layer.test.ts +309 -0
- package/src/excel_cf_evaluate.test.ts +293 -0
- package/src/excel_cf_evaluate.ts +719 -0
- package/src/excel_cf_icons.ts +108 -0
- package/src/excel_cf_types.ts +67 -0
- package/src/excel_numfmt.test.ts +607 -0
- package/src/excel_numfmt.ts +1033 -0
- package/src/excel_parser.test.ts +1061 -0
- package/src/excel_parser.ts +393 -0
- package/src/excel_pattern_fills.ts +64 -0
- package/src/excel_table_styles.ts +160 -0
- package/src/excel_utils.test.ts +108 -0
- package/src/excel_utils.ts +162 -0
- package/src/fast-formula-parser.d.ts +53 -0
- package/src/fill_logic.ts +257 -0
- package/src/fill_patterns.test.ts +90 -0
- package/src/fill_patterns.ts +71 -0
- package/src/flash_fill.test.ts +75 -0
- package/src/flash_fill.ts +221 -0
- package/src/formula_deps.ts +189 -0
- package/src/formula_engine.test.ts +348 -0
- package/src/formula_engine.ts +401 -0
- package/src/formula_highlight.test.ts +81 -0
- package/src/formula_highlight.ts +139 -0
- package/src/formula_suggest.ts +100 -0
- package/src/hidden_sheets.test.ts +49 -0
- package/src/index.ts +149 -0
- package/src/keyboard_nav.test.ts +394 -0
- package/src/keyboard_nav.ts +891 -0
- package/src/move_logic.ts +63 -0
- package/src/numfmt_type.test.ts +158 -0
- package/src/numfmt_type.ts +231 -0
- package/src/ooxml_cell_format.ts +70 -0
- package/src/ooxml_chart.test.ts +85 -0
- package/src/ooxml_chart.ts +347 -0
- package/src/ooxml_chart_types.ts +207 -0
- package/src/ooxml_color.ts +339 -0
- package/src/ooxml_drawing.test.ts +87 -0
- package/src/ooxml_drawing.ts +287 -0
- package/src/ooxml_pivot.test.ts +195 -0
- package/src/ooxml_pivot.ts +468 -0
- package/src/ooxml_pivot_types.ts +165 -0
- package/src/ooxml_shape.ts +355 -0
- package/src/ooxml_sheet.ts +1271 -0
- package/src/ooxml_styles.ts +556 -0
- package/src/ooxml_table.test.ts +70 -0
- package/src/ooxml_table.ts +131 -0
- package/src/ooxml_workbook.test.ts +40 -0
- package/src/ooxml_workbook.ts +259 -0
- package/src/ooxml_zip.ts +33 -0
- package/src/pivot_model.ts +237 -0
- package/src/pivot_recompute.test.ts +210 -0
- package/src/pivot_recompute.ts +413 -0
- package/src/selection_model.ts +364 -0
- package/src/spreadsheet_commands.test.ts +772 -0
- package/src/spreadsheet_commands.ts +1805 -0
- package/src/structured_refs.test.ts +128 -0
- package/src/structured_refs.ts +160 -0
- package/src/style_helpers.test.ts +33 -0
- package/src/style_helpers.ts +30 -0
- package/src/template_builder.test.ts +139 -0
- package/src/template_builder.ts +36 -0
- package/src/types.ts +239 -0
- package/src/workbook_model.test.ts +536 -0
- package/src/workbook_model.ts +911 -0
- package/src/xlsx_round_trip.test.ts +279 -0
- package/src/xlsx_writer.test.ts +488 -0
- package/src/xlsx_writer.ts +1549 -0
- package/src/xml_entities.ts +23 -0
|
@@ -0,0 +1,891 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Keyboard Navigation
|
|
3
|
+
// =============================================================================
|
|
4
|
+
//
|
|
5
|
+
// Pure TypeScript keyboard event handler for the spreadsheet editor.
|
|
6
|
+
// Translates KeyboardEvent into selection model, command, and clipboard actions.
|
|
7
|
+
// No DOM dependencies beyond the KeyboardEvent interface.
|
|
8
|
+
// =============================================================================
|
|
9
|
+
|
|
10
|
+
import type { SelectionModel } from "./selection_model";
|
|
11
|
+
import { normalizeRange } from "./selection_model";
|
|
12
|
+
import type { WorkbookModel } from "./workbook_model";
|
|
13
|
+
import type { CommandHistory } from "./command_history";
|
|
14
|
+
import type { FormulaEngine } from "./formula_engine";
|
|
15
|
+
import type { ClipboardData } from "./clipboard";
|
|
16
|
+
import {
|
|
17
|
+
setRangeStyleCommand,
|
|
18
|
+
clearRangeCommand,
|
|
19
|
+
setHiddenRowsCommand,
|
|
20
|
+
setHiddenColsCommand,
|
|
21
|
+
setCellValueCommand,
|
|
22
|
+
} from "./spreadsheet_commands";
|
|
23
|
+
import { copyRange, pasteRange, clipboardToText, clipboardToHtml } from "./clipboard";
|
|
24
|
+
import { rowColToRef } from "./excel_utils";
|
|
25
|
+
|
|
26
|
+
// =============================================================================
|
|
27
|
+
// Context
|
|
28
|
+
// =============================================================================
|
|
29
|
+
|
|
30
|
+
export interface KeyboardNavContext {
|
|
31
|
+
selection: SelectionModel;
|
|
32
|
+
workbook: WorkbookModel;
|
|
33
|
+
sheetIndex: number;
|
|
34
|
+
history: CommandHistory;
|
|
35
|
+
engine: FormulaEngine | undefined;
|
|
36
|
+
/** Internal clipboard buffer. */
|
|
37
|
+
clipboardData: ClipboardData | undefined;
|
|
38
|
+
setClipboardData: (data: ClipboardData | undefined) => void;
|
|
39
|
+
/** Callback to start inline cell editing. */
|
|
40
|
+
onStartEdit: (initialText?: string) => void;
|
|
41
|
+
/** Callback to request a canvas redraw. */
|
|
42
|
+
onRedraw: () => void;
|
|
43
|
+
/** Callback to open paste special dialog. */
|
|
44
|
+
onPasteSpecial?: () => void;
|
|
45
|
+
/** Callback to open find/replace bar. */
|
|
46
|
+
onFindReplace?: (replaceMode: boolean) => void;
|
|
47
|
+
/** Callback to save/download the workbook. */
|
|
48
|
+
onSave?: () => void;
|
|
49
|
+
/** Viewport size for page up/down (in rows). */
|
|
50
|
+
pageSize: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// =============================================================================
|
|
54
|
+
// Main handler
|
|
55
|
+
// =============================================================================
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Handle a keydown event. Returns true if the event was consumed
|
|
59
|
+
* (caller should preventDefault).
|
|
60
|
+
*/
|
|
61
|
+
export function handleKeyDown(
|
|
62
|
+
event: KeyboardEvent,
|
|
63
|
+
ctx: KeyboardNavContext,
|
|
64
|
+
): boolean {
|
|
65
|
+
const { selection, workbook, sheetIndex, history } = ctx;
|
|
66
|
+
const state = selection.getState();
|
|
67
|
+
const ctrl = event.ctrlKey || event.metaKey;
|
|
68
|
+
const shift = event.shiftKey;
|
|
69
|
+
|
|
70
|
+
// If currently editing, only handle navigation keys that exit editing
|
|
71
|
+
if (state.editing) {
|
|
72
|
+
return handleEditingKeys(event, ctx);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// --- Ctrl shortcuts ---
|
|
76
|
+
if (ctrl) {
|
|
77
|
+
switch (event.key.toLowerCase()) {
|
|
78
|
+
case "z":
|
|
79
|
+
if (shift) {
|
|
80
|
+
history.redo();
|
|
81
|
+
} else {
|
|
82
|
+
history.undo();
|
|
83
|
+
}
|
|
84
|
+
ctx.onRedraw();
|
|
85
|
+
return true;
|
|
86
|
+
|
|
87
|
+
case "y":
|
|
88
|
+
history.redo();
|
|
89
|
+
ctx.onRedraw();
|
|
90
|
+
return true;
|
|
91
|
+
|
|
92
|
+
case "c":
|
|
93
|
+
handleCopy(ctx, false);
|
|
94
|
+
return true;
|
|
95
|
+
|
|
96
|
+
case "x":
|
|
97
|
+
handleCopy(ctx, true);
|
|
98
|
+
return true;
|
|
99
|
+
|
|
100
|
+
case "v":
|
|
101
|
+
if (shift && ctx.onPasteSpecial) {
|
|
102
|
+
ctx.onPasteSpecial();
|
|
103
|
+
} else {
|
|
104
|
+
handlePasteWithFallback(ctx);
|
|
105
|
+
}
|
|
106
|
+
return true;
|
|
107
|
+
|
|
108
|
+
case "b":
|
|
109
|
+
toggleStyle(ctx, "fontBold");
|
|
110
|
+
return true;
|
|
111
|
+
|
|
112
|
+
case "i":
|
|
113
|
+
toggleStyle(ctx, "fontItalic");
|
|
114
|
+
return true;
|
|
115
|
+
|
|
116
|
+
case "u":
|
|
117
|
+
toggleStyle(ctx, "fontUnderline");
|
|
118
|
+
return true;
|
|
119
|
+
|
|
120
|
+
case "a":
|
|
121
|
+
selectAll(ctx);
|
|
122
|
+
return true;
|
|
123
|
+
|
|
124
|
+
case "f":
|
|
125
|
+
ctx.onFindReplace?.(false);
|
|
126
|
+
return true;
|
|
127
|
+
|
|
128
|
+
case "h":
|
|
129
|
+
ctx.onFindReplace?.(true);
|
|
130
|
+
return true;
|
|
131
|
+
|
|
132
|
+
case "s":
|
|
133
|
+
ctx.onSave?.();
|
|
134
|
+
return true;
|
|
135
|
+
|
|
136
|
+
case "home":
|
|
137
|
+
selection.goTo(1, 1);
|
|
138
|
+
ctx.onRedraw();
|
|
139
|
+
return true;
|
|
140
|
+
|
|
141
|
+
case "end": {
|
|
142
|
+
// Ctrl+End → last cell of the used range (Excel parity).
|
|
143
|
+
const sheet = workbook.sheets[sheetIndex];
|
|
144
|
+
const used = sheet.getUsedRange();
|
|
145
|
+
const targetRow = Math.max(used.maxRow || 1, 1);
|
|
146
|
+
const targetCol = Math.max(used.maxCol || 1, 1);
|
|
147
|
+
selection.goTo(targetRow, targetCol);
|
|
148
|
+
ctx.onRedraw();
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
case ";": {
|
|
153
|
+
// Ctrl+; → insert today's date as YYYY-MM-DD (Excel parity).
|
|
154
|
+
// Excel writes the date as a serial; we write the rendered string,
|
|
155
|
+
// which our number-format engine will recognize as a date.
|
|
156
|
+
const today = formatToday();
|
|
157
|
+
commitToActiveCell(ctx, today);
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
case ":": {
|
|
162
|
+
// Ctrl+Shift+; (US layout) — same key code as ":" — inserts current
|
|
163
|
+
// time. We accept the bare ":" key as well for non-US layouts where
|
|
164
|
+
// ":" is its own key.
|
|
165
|
+
const time = formatNow();
|
|
166
|
+
commitToActiveCell(ctx, time);
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
case "9": {
|
|
171
|
+
// Ctrl+9 → hide selected rows.
|
|
172
|
+
const sheet = workbook.sheets[sheetIndex];
|
|
173
|
+
const range = state.ranges[0];
|
|
174
|
+
if (!range) return false;
|
|
175
|
+
const sr = Math.min(range.start.row, range.end.row);
|
|
176
|
+
const er = Math.max(range.start.row, range.end.row);
|
|
177
|
+
const newHidden = new Set(sheet.hiddenRows);
|
|
178
|
+
for (let r = sr; r <= er; r++) newHidden.add(r);
|
|
179
|
+
ctx.history.execute(setHiddenRowsCommand(workbook, sheetIndex, newHidden));
|
|
180
|
+
ctx.onRedraw();
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
case "0": {
|
|
185
|
+
// Ctrl+0 → hide selected columns.
|
|
186
|
+
const sheet = workbook.sheets[sheetIndex];
|
|
187
|
+
const range = state.ranges[0];
|
|
188
|
+
if (!range) return false;
|
|
189
|
+
const sc = Math.min(range.start.col, range.end.col);
|
|
190
|
+
const ec = Math.max(range.start.col, range.end.col);
|
|
191
|
+
const newHidden = new Set(sheet.hiddenCols);
|
|
192
|
+
for (let c = sc; c <= ec; c++) newHidden.add(c);
|
|
193
|
+
ctx.history.execute(setHiddenColsCommand(workbook, sheetIndex, newHidden));
|
|
194
|
+
ctx.onRedraw();
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// Ctrl+Arrow: jump to edge of data region
|
|
199
|
+
if (isArrowKey(event.key)) {
|
|
200
|
+
handleCtrlArrow(event.key, ctx);
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// --- Non-ctrl keys ---
|
|
207
|
+
switch (event.key) {
|
|
208
|
+
case "ArrowUp":
|
|
209
|
+
selection.move(-1, 0, shift);
|
|
210
|
+
ctx.onRedraw();
|
|
211
|
+
return true;
|
|
212
|
+
|
|
213
|
+
case "ArrowDown":
|
|
214
|
+
selection.move(1, 0, shift);
|
|
215
|
+
ctx.onRedraw();
|
|
216
|
+
return true;
|
|
217
|
+
|
|
218
|
+
case "ArrowLeft":
|
|
219
|
+
selection.move(0, -1, shift);
|
|
220
|
+
ctx.onRedraw();
|
|
221
|
+
return true;
|
|
222
|
+
|
|
223
|
+
case "ArrowRight":
|
|
224
|
+
selection.move(0, 1, shift);
|
|
225
|
+
ctx.onRedraw();
|
|
226
|
+
return true;
|
|
227
|
+
|
|
228
|
+
case "Tab": {
|
|
229
|
+
// When the active cell is inside a multi-cell selection, wrap within
|
|
230
|
+
// the range — pressing Tab past the last column moves to the next
|
|
231
|
+
// row's first column. Excel and Sheets both do this; without it,
|
|
232
|
+
// "fill in this row" workflows break the second the user reaches the
|
|
233
|
+
// edge.
|
|
234
|
+
const range = state.ranges[0];
|
|
235
|
+
if (range && rangeIsMulti(range)) {
|
|
236
|
+
moveWithinRange(selection, range, "horizontal", shift);
|
|
237
|
+
} else {
|
|
238
|
+
selection.move(0, shift ? -1 : 1);
|
|
239
|
+
}
|
|
240
|
+
ctx.onRedraw();
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
case "Enter": {
|
|
245
|
+
const range = state.ranges[0];
|
|
246
|
+
if (range && rangeIsMulti(range)) {
|
|
247
|
+
moveWithinRange(selection, range, "vertical", shift);
|
|
248
|
+
} else {
|
|
249
|
+
selection.move(shift ? -1 : 1, 0);
|
|
250
|
+
}
|
|
251
|
+
ctx.onRedraw();
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
case "Escape":
|
|
256
|
+
return false;
|
|
257
|
+
|
|
258
|
+
case "Home":
|
|
259
|
+
selection.goTo(state.activeCell.row, 1);
|
|
260
|
+
ctx.onRedraw();
|
|
261
|
+
return true;
|
|
262
|
+
|
|
263
|
+
case "End":
|
|
264
|
+
// End key alone does nothing — handled in combo with arrow
|
|
265
|
+
return false;
|
|
266
|
+
|
|
267
|
+
case "PageUp":
|
|
268
|
+
selection.move(-ctx.pageSize, 0, shift);
|
|
269
|
+
ctx.onRedraw();
|
|
270
|
+
return true;
|
|
271
|
+
|
|
272
|
+
case "PageDown":
|
|
273
|
+
selection.move(ctx.pageSize, 0, shift);
|
|
274
|
+
ctx.onRedraw();
|
|
275
|
+
return true;
|
|
276
|
+
|
|
277
|
+
case "Delete":
|
|
278
|
+
handleDelete(ctx);
|
|
279
|
+
return true;
|
|
280
|
+
|
|
281
|
+
case "Backspace":
|
|
282
|
+
handleDelete(ctx);
|
|
283
|
+
ctx.onStartEdit("");
|
|
284
|
+
return true;
|
|
285
|
+
|
|
286
|
+
case "F2": {
|
|
287
|
+
const ref = rowColToRef(state.activeCell.row, state.activeCell.col);
|
|
288
|
+
const cell = workbook.sheets[sheetIndex].getCell(ref);
|
|
289
|
+
const existing = cell?.formula ? `=${cell.formula}` : cell?.displayValue ?? "";
|
|
290
|
+
ctx.onStartEdit(existing);
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Printable character → enter edit mode with that character (replace mode)
|
|
296
|
+
if (isPrintableKey(event)) {
|
|
297
|
+
ctx.onStartEdit(event.key);
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// =============================================================================
|
|
305
|
+
// Keys while editing
|
|
306
|
+
// =============================================================================
|
|
307
|
+
|
|
308
|
+
function handleEditingKeys(
|
|
309
|
+
event: KeyboardEvent,
|
|
310
|
+
ctx: KeyboardNavContext,
|
|
311
|
+
): boolean {
|
|
312
|
+
const { selection } = ctx;
|
|
313
|
+
const shift = event.shiftKey;
|
|
314
|
+
|
|
315
|
+
switch (event.key) {
|
|
316
|
+
case "Enter":
|
|
317
|
+
// Alt+Enter inserts a newline inside the cell (Excel parity); plain
|
|
318
|
+
// Enter commits and advances. The input element handles the literal
|
|
319
|
+
// newline insert; we just signal "don't commit" so the editor keeps
|
|
320
|
+
// focus.
|
|
321
|
+
if (event.altKey) {
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
selection.commitEdit();
|
|
325
|
+
selection.move(shift ? -1 : 1, 0);
|
|
326
|
+
ctx.onRedraw();
|
|
327
|
+
return true;
|
|
328
|
+
|
|
329
|
+
case "Tab":
|
|
330
|
+
selection.commitEdit();
|
|
331
|
+
selection.move(0, shift ? -1 : 1);
|
|
332
|
+
ctx.onRedraw();
|
|
333
|
+
return true;
|
|
334
|
+
|
|
335
|
+
case "Escape":
|
|
336
|
+
selection.cancelEdit();
|
|
337
|
+
ctx.onRedraw();
|
|
338
|
+
return true;
|
|
339
|
+
|
|
340
|
+
case "F4":
|
|
341
|
+
// F4 in edit mode cycles the reference type at the cursor:
|
|
342
|
+
// A1 → $A$1 → A$1 → $A1 → A1
|
|
343
|
+
// Returning false lets the input handle its native default; the actual
|
|
344
|
+
// cycle is implemented at the editor input layer (cell_editor.ts) since
|
|
345
|
+
// it needs cursor position. We just preventDefault here so the browser
|
|
346
|
+
// doesn't trigger Find Next.
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// All other keys are handled by the input element itself
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// =============================================================================
|
|
355
|
+
// F4 reference-type cycling (called by the cell editor input)
|
|
356
|
+
// =============================================================================
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Cycle the reference at the given cursor position through the four absolute
|
|
360
|
+
* states. Returns the new text + new cursor position, or null if no reference
|
|
361
|
+
* is found at the cursor.
|
|
362
|
+
*
|
|
363
|
+
* A1 → $A$1
|
|
364
|
+
* $A$1 → A$1
|
|
365
|
+
* A$1 → $A1
|
|
366
|
+
* $A1 → A1
|
|
367
|
+
*/
|
|
368
|
+
export function cycleReferenceAtCursor(
|
|
369
|
+
text: string,
|
|
370
|
+
cursor: number,
|
|
371
|
+
): { text: string; cursor: number } | null {
|
|
372
|
+
// Match a cell reference touching the cursor. Walk backwards/forwards from
|
|
373
|
+
// the cursor to find a contiguous reference token.
|
|
374
|
+
const refRegex = /(\$?)([A-Za-z]+)(\$?)([0-9]+)/g;
|
|
375
|
+
let match: RegExpExecArray | null;
|
|
376
|
+
while ((match = refRegex.exec(text)) !== null) {
|
|
377
|
+
const start = match.index;
|
|
378
|
+
const end = start + match[0].length;
|
|
379
|
+
if (cursor < start || cursor > end) continue;
|
|
380
|
+
const colAbs = match[1] === "$";
|
|
381
|
+
const rowAbs = match[3] === "$";
|
|
382
|
+
let nextCol: boolean;
|
|
383
|
+
let nextRow: boolean;
|
|
384
|
+
if (!colAbs && !rowAbs) { nextCol = true; nextRow = true; }
|
|
385
|
+
else if (colAbs && rowAbs) { nextCol = false; nextRow = true; }
|
|
386
|
+
else if (!colAbs && rowAbs) { nextCol = true; nextRow = false; }
|
|
387
|
+
else { nextCol = false; nextRow = false; }
|
|
388
|
+
const newRef = `${nextCol ? "$" : ""}${match[2]}${nextRow ? "$" : ""}${match[4]}`;
|
|
389
|
+
const newText = text.slice(0, start) + newRef + text.slice(end);
|
|
390
|
+
return { text: newText, cursor: start + newRef.length };
|
|
391
|
+
}
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// =============================================================================
|
|
396
|
+
// Copy / Cut (exported for toolbar use)
|
|
397
|
+
// =============================================================================
|
|
398
|
+
|
|
399
|
+
export function performCopy(ctx: KeyboardNavContext, isCut: boolean): void {
|
|
400
|
+
handleCopy(ctx, isCut);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
export function performPaste(ctx: KeyboardNavContext): void {
|
|
404
|
+
handlePasteWithFallback(ctx);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function handleCopy(ctx: KeyboardNavContext, isCut: boolean): void {
|
|
408
|
+
const { selection, workbook, sheetIndex } = ctx;
|
|
409
|
+
const range = normalizeRange(selection.getState().ranges[0]);
|
|
410
|
+
const data = copyRange(
|
|
411
|
+
workbook,
|
|
412
|
+
sheetIndex,
|
|
413
|
+
range.start.row,
|
|
414
|
+
range.start.col,
|
|
415
|
+
range.end.row,
|
|
416
|
+
range.end.col,
|
|
417
|
+
isCut,
|
|
418
|
+
);
|
|
419
|
+
ctx.setClipboardData(data);
|
|
420
|
+
|
|
421
|
+
// Write to system clipboard
|
|
422
|
+
const text = clipboardToText(data);
|
|
423
|
+
const html = clipboardToHtml(data, workbook.styles);
|
|
424
|
+
writeToSystemClipboard(text, html);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// =============================================================================
|
|
428
|
+
// Paste
|
|
429
|
+
// =============================================================================
|
|
430
|
+
|
|
431
|
+
function handlePaste(ctx: KeyboardNavContext): void {
|
|
432
|
+
const { selection, workbook, sheetIndex, engine, history } = ctx;
|
|
433
|
+
const clipboard = ctx.clipboardData;
|
|
434
|
+
if (!clipboard) return;
|
|
435
|
+
|
|
436
|
+
const active = selection.getState().activeCell;
|
|
437
|
+
const targetSheet = workbook.sheets[sheetIndex];
|
|
438
|
+
|
|
439
|
+
// Snapshot target cells before paste so we can undo
|
|
440
|
+
const snapshot = new Map<string, { value: import("./workbook_model").CellValue; displayValue: string; formula?: string; styleIndex: number; numFmtCode: string } | null>();
|
|
441
|
+
for (let r = 0; r < clipboard.rowCount; r++) {
|
|
442
|
+
for (let c = 0; c < clipboard.colCount; c++) {
|
|
443
|
+
const ref = rowColToRef(active.row + r, active.col + c);
|
|
444
|
+
const existing = targetSheet.getCell(ref);
|
|
445
|
+
snapshot.set(ref, existing ? {
|
|
446
|
+
value: existing.value,
|
|
447
|
+
displayValue: existing.displayValue,
|
|
448
|
+
formula: existing.formula,
|
|
449
|
+
styleIndex: existing.styleIndex,
|
|
450
|
+
numFmtCode: existing.numFmtCode,
|
|
451
|
+
} : null);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Snapshot cut source cells if needed
|
|
456
|
+
const cutSnapshot = new Map<string, { value: import("./workbook_model").CellValue; displayValue: string; formula?: string; styleIndex: number; numFmtCode: string }>();
|
|
457
|
+
if (clipboard.isCut) {
|
|
458
|
+
const srcSheet = workbook.sheets[clipboard.sourceSheet];
|
|
459
|
+
for (let r = 0; r < clipboard.rowCount; r++) {
|
|
460
|
+
for (let c = 0; c < clipboard.colCount; c++) {
|
|
461
|
+
const ref = rowColToRef(clipboard.origin.row + r, clipboard.origin.col + c);
|
|
462
|
+
const cell = srcSheet.getCell(ref);
|
|
463
|
+
if (cell) {
|
|
464
|
+
cutSnapshot.set(ref, {
|
|
465
|
+
value: cell.value,
|
|
466
|
+
displayValue: cell.displayValue,
|
|
467
|
+
formula: cell.formula,
|
|
468
|
+
styleIndex: cell.styleIndex,
|
|
469
|
+
numFmtCode: cell.numFmtCode,
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const wasCut = clipboard.isCut;
|
|
477
|
+
const sourceSheetIndex = clipboard.sourceSheet;
|
|
478
|
+
|
|
479
|
+
const cmd = {
|
|
480
|
+
description: `Paste at ${rowColToRef(active.row, active.col)}`,
|
|
481
|
+
execute() {
|
|
482
|
+
const result = pasteRange(workbook, clipboard, sheetIndex, active.row, active.col);
|
|
483
|
+
|
|
484
|
+
if (wasCut) {
|
|
485
|
+
const srcSheet = workbook.sheets[sourceSheetIndex];
|
|
486
|
+
for (let r = 0; r < clipboard.rowCount; r++) {
|
|
487
|
+
for (let c = 0; c < clipboard.colCount; c++) {
|
|
488
|
+
const ref = rowColToRef(clipboard.origin.row + r, clipboard.origin.col + c);
|
|
489
|
+
srcSheet.deleteCell(ref);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (engine) {
|
|
495
|
+
const sheet = workbook.sheets[sheetIndex];
|
|
496
|
+
for (const w of result.written) {
|
|
497
|
+
engine.onCellChanged(sheet.name, w.ref, w.formula);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
},
|
|
501
|
+
undo() {
|
|
502
|
+
// Restore target cells from snapshot
|
|
503
|
+
for (const [ref, snap] of snapshot) {
|
|
504
|
+
if (snap) {
|
|
505
|
+
targetSheet.set(ref, snap.value, targetSheet.styles?.get(snap.styleIndex), snap.numFmtCode, snap.formula);
|
|
506
|
+
} else {
|
|
507
|
+
targetSheet.deleteCell(ref);
|
|
508
|
+
}
|
|
509
|
+
if (engine) {
|
|
510
|
+
engine.onCellChanged(targetSheet.name, ref, snap?.formula);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Restore cut source cells
|
|
515
|
+
if (wasCut) {
|
|
516
|
+
const srcSheet = workbook.sheets[sourceSheetIndex];
|
|
517
|
+
for (const [ref, snap] of cutSnapshot) {
|
|
518
|
+
srcSheet.set(ref, snap.value, srcSheet.styles?.get(snap.styleIndex), snap.numFmtCode, snap.formula);
|
|
519
|
+
if (engine) {
|
|
520
|
+
engine.onCellChanged(srcSheet.name, ref, snap.formula);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
},
|
|
525
|
+
};
|
|
526
|
+
history.execute(cmd);
|
|
527
|
+
|
|
528
|
+
if (wasCut) {
|
|
529
|
+
ctx.setClipboardData(undefined);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
ctx.onRedraw();
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// =============================================================================
|
|
536
|
+
// Delete / Clear
|
|
537
|
+
// =============================================================================
|
|
538
|
+
|
|
539
|
+
function handleDelete(ctx: KeyboardNavContext): void {
|
|
540
|
+
const { selection, workbook, sheetIndex, history, engine } = ctx;
|
|
541
|
+
const range = normalizeRange(selection.getState().ranges[0]);
|
|
542
|
+
|
|
543
|
+
const cmd = clearRangeCommand(
|
|
544
|
+
workbook,
|
|
545
|
+
engine,
|
|
546
|
+
sheetIndex,
|
|
547
|
+
range.start.row,
|
|
548
|
+
range.start.col,
|
|
549
|
+
range.end.row,
|
|
550
|
+
range.end.col,
|
|
551
|
+
);
|
|
552
|
+
history.execute(cmd);
|
|
553
|
+
ctx.onRedraw();
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// =============================================================================
|
|
557
|
+
// Style toggling
|
|
558
|
+
// =============================================================================
|
|
559
|
+
|
|
560
|
+
type ToggleableBoolStyle = "fontBold" | "fontItalic";
|
|
561
|
+
type ToggleableUnderlineStyle = "fontUnderline";
|
|
562
|
+
|
|
563
|
+
function toggleStyle(
|
|
564
|
+
ctx: KeyboardNavContext,
|
|
565
|
+
prop: ToggleableBoolStyle | ToggleableUnderlineStyle,
|
|
566
|
+
): void {
|
|
567
|
+
const { selection, workbook, sheetIndex, history } = ctx;
|
|
568
|
+
const range = normalizeRange(selection.getState().ranges[0]);
|
|
569
|
+
const active = selection.getState().activeCell;
|
|
570
|
+
const ref = rowColToRef(active.row, active.col);
|
|
571
|
+
const cell = workbook.sheets[sheetIndex].getCell(ref);
|
|
572
|
+
const style = workbook.styles.get(cell?.styleIndex ?? 0);
|
|
573
|
+
|
|
574
|
+
let override: Record<string, unknown>;
|
|
575
|
+
if (prop === "fontUnderline") {
|
|
576
|
+
override = { fontUnderline: style.fontUnderline ? undefined : "single" as const };
|
|
577
|
+
} else {
|
|
578
|
+
override = { [prop]: !style[prop] };
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const cmd = setRangeStyleCommand(
|
|
582
|
+
workbook,
|
|
583
|
+
sheetIndex,
|
|
584
|
+
range.start.row,
|
|
585
|
+
range.start.col,
|
|
586
|
+
range.end.row,
|
|
587
|
+
range.end.col,
|
|
588
|
+
override,
|
|
589
|
+
);
|
|
590
|
+
history.execute(cmd);
|
|
591
|
+
ctx.onRedraw();
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// =============================================================================
|
|
595
|
+
// Select All
|
|
596
|
+
// =============================================================================
|
|
597
|
+
|
|
598
|
+
function selectAll(ctx: KeyboardNavContext): void {
|
|
599
|
+
const { selection, workbook, sheetIndex } = ctx;
|
|
600
|
+
const sheet = workbook.sheets[sheetIndex];
|
|
601
|
+
const used = sheet.getUsedRange();
|
|
602
|
+
selection.click(1, 1);
|
|
603
|
+
selection.click(used.maxRow, used.maxCol, true);
|
|
604
|
+
ctx.onRedraw();
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// =============================================================================
|
|
608
|
+
// Ctrl+Arrow — jump to data edge
|
|
609
|
+
// =============================================================================
|
|
610
|
+
|
|
611
|
+
function handleCtrlArrow(key: string, ctx: KeyboardNavContext): void {
|
|
612
|
+
const { selection, workbook, sheetIndex } = ctx;
|
|
613
|
+
const state = selection.getState();
|
|
614
|
+
const active = state.activeCell;
|
|
615
|
+
const sheet = workbook.sheets[sheetIndex];
|
|
616
|
+
const used = sheet.getUsedRange();
|
|
617
|
+
|
|
618
|
+
let targetRow = active.row;
|
|
619
|
+
let targetCol = active.col;
|
|
620
|
+
|
|
621
|
+
const hasData = (r: number, c: number): boolean => {
|
|
622
|
+
const ref = rowColToRef(r, c);
|
|
623
|
+
const cell = sheet.getCell(ref);
|
|
624
|
+
return cell !== undefined && cell.value !== null && cell.value !== "";
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
switch (key) {
|
|
628
|
+
case "ArrowUp":
|
|
629
|
+
targetRow = findEdge(active.row, -1, 1, used.maxRow, (r) => hasData(r, active.col));
|
|
630
|
+
break;
|
|
631
|
+
case "ArrowDown":
|
|
632
|
+
targetRow = findEdge(active.row, 1, 1, used.maxRow, (r) => hasData(r, active.col));
|
|
633
|
+
break;
|
|
634
|
+
case "ArrowLeft":
|
|
635
|
+
targetCol = findEdge(active.col, -1, 1, used.maxCol, (c) => hasData(active.row, c));
|
|
636
|
+
break;
|
|
637
|
+
case "ArrowRight":
|
|
638
|
+
targetCol = findEdge(active.col, 1, 1, used.maxCol, (c) => hasData(active.row, c));
|
|
639
|
+
break;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
if (ctx.selection.getState().dragging) return;
|
|
643
|
+
selection.goTo(targetRow, targetCol);
|
|
644
|
+
ctx.onRedraw();
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Find the edge of a data region.
|
|
649
|
+
* If current position has data, jump to the last consecutive data cell in the direction.
|
|
650
|
+
* If current position is empty, jump to the next data cell in the direction.
|
|
651
|
+
* If no data found, jump to the boundary.
|
|
652
|
+
*/
|
|
653
|
+
function findEdge(
|
|
654
|
+
current: number,
|
|
655
|
+
direction: 1 | -1,
|
|
656
|
+
min: number,
|
|
657
|
+
max: number,
|
|
658
|
+
hasData: (pos: number) => boolean,
|
|
659
|
+
): number {
|
|
660
|
+
const currentHasData = hasData(current);
|
|
661
|
+
let pos = current + direction;
|
|
662
|
+
|
|
663
|
+
if (currentHasData) {
|
|
664
|
+
// Skip consecutive data cells
|
|
665
|
+
while (pos >= min && pos <= max && hasData(pos)) {
|
|
666
|
+
pos += direction;
|
|
667
|
+
}
|
|
668
|
+
// Back up to last data cell (or stay if we hit a non-data immediately)
|
|
669
|
+
pos -= direction;
|
|
670
|
+
if (pos === current) {
|
|
671
|
+
// No consecutive data — jump to next data cell or boundary
|
|
672
|
+
pos = current + direction;
|
|
673
|
+
while (pos >= min && pos <= max && !hasData(pos)) {
|
|
674
|
+
pos += direction;
|
|
675
|
+
}
|
|
676
|
+
if (pos < min || pos > max) {
|
|
677
|
+
return direction === -1 ? min : max;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
} else {
|
|
681
|
+
// Current empty — find next data cell
|
|
682
|
+
while (pos >= min && pos <= max && !hasData(pos)) {
|
|
683
|
+
pos += direction;
|
|
684
|
+
}
|
|
685
|
+
if (pos < min || pos > max) {
|
|
686
|
+
return direction === -1 ? min : max;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
return pos;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// =============================================================================
|
|
694
|
+
// Helpers
|
|
695
|
+
// =============================================================================
|
|
696
|
+
|
|
697
|
+
// =============================================================================
|
|
698
|
+
// Tab/Enter wrap within a multi-cell selection range
|
|
699
|
+
// =============================================================================
|
|
700
|
+
|
|
701
|
+
function rangeIsMulti(range: import("./selection_model").CellRange): boolean {
|
|
702
|
+
return range.start.row !== range.end.row || range.start.col !== range.end.col;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* Move the active cell inside an existing range with wrap-around. Direction
|
|
707
|
+
* is "horizontal" (Tab) or "vertical" (Enter). When `reverse` is true, wraps
|
|
708
|
+
* backwards; e.g. Shift+Tab from the top-left corner wraps to the
|
|
709
|
+
* bottom-right of the range.
|
|
710
|
+
*/
|
|
711
|
+
function moveWithinRange(
|
|
712
|
+
selection: import("./selection_model").SelectionModel,
|
|
713
|
+
range: import("./selection_model").CellRange,
|
|
714
|
+
direction: "horizontal" | "vertical",
|
|
715
|
+
reverse: boolean,
|
|
716
|
+
): void {
|
|
717
|
+
const sr = Math.min(range.start.row, range.end.row);
|
|
718
|
+
const er = Math.max(range.start.row, range.end.row);
|
|
719
|
+
const sc = Math.min(range.start.col, range.end.col);
|
|
720
|
+
const ec = Math.max(range.start.col, range.end.col);
|
|
721
|
+
const width = ec - sc + 1;
|
|
722
|
+
const height = er - sr + 1;
|
|
723
|
+
const total = width * height;
|
|
724
|
+
|
|
725
|
+
const { activeCell } = selection.getState();
|
|
726
|
+
// Linearize current position within the range, in row-major or col-major
|
|
727
|
+
// order depending on direction.
|
|
728
|
+
const rowIdx = activeCell.row - sr;
|
|
729
|
+
const colIdx = activeCell.col - sc;
|
|
730
|
+
let linear = direction === "horizontal"
|
|
731
|
+
? rowIdx * width + colIdx
|
|
732
|
+
: colIdx * height + rowIdx;
|
|
733
|
+
linear = (linear + (reverse ? -1 : 1) + total) % total;
|
|
734
|
+
|
|
735
|
+
let newRow: number;
|
|
736
|
+
let newCol: number;
|
|
737
|
+
if (direction === "horizontal") {
|
|
738
|
+
newRow = sr + Math.floor(linear / width);
|
|
739
|
+
newCol = sc + (linear % width);
|
|
740
|
+
} else {
|
|
741
|
+
newCol = sc + Math.floor(linear / height);
|
|
742
|
+
newRow = sr + (linear % height);
|
|
743
|
+
}
|
|
744
|
+
selection.goTo(newRow, newCol);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// =============================================================================
|
|
748
|
+
// Date / Time insertion (Ctrl+; and Ctrl+:)
|
|
749
|
+
// =============================================================================
|
|
750
|
+
|
|
751
|
+
function formatToday(): string {
|
|
752
|
+
const d = new Date();
|
|
753
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function formatNow(): string {
|
|
757
|
+
const d = new Date();
|
|
758
|
+
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}:${String(d.getSeconds()).padStart(2, "0")}`;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* Commit a string to the active cell via setCellValueCommand. Used by Ctrl+;
|
|
763
|
+
* (date) and Ctrl+: (time) — both bypass edit mode and write directly.
|
|
764
|
+
*/
|
|
765
|
+
function commitToActiveCell(ctx: KeyboardNavContext, text: string): void {
|
|
766
|
+
const { row, col } = ctx.selection.getState().activeCell;
|
|
767
|
+
ctx.history.execute(setCellValueCommand(ctx.workbook, ctx.engine, ctx.sheetIndex, row, col, text));
|
|
768
|
+
ctx.onRedraw();
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function isArrowKey(key: string): boolean {
|
|
772
|
+
return key === "ArrowUp" || key === "ArrowDown" || key === "ArrowLeft" || key === "ArrowRight";
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function isPrintableKey(event: KeyboardEvent): boolean {
|
|
776
|
+
// Single character, not a control/meta key
|
|
777
|
+
if (event.ctrlKey || event.metaKey || event.altKey) return false;
|
|
778
|
+
if (event.key.length !== 1) return false;
|
|
779
|
+
// Exclude non-printable ranges
|
|
780
|
+
const code = event.key.charCodeAt(0);
|
|
781
|
+
return code >= 0x20; // space and above
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Paste from internal clipboard if available, otherwise read from system clipboard.
|
|
786
|
+
* System clipboard text is parsed as TSV and pasted as values.
|
|
787
|
+
*/
|
|
788
|
+
async function handlePasteWithFallback(ctx: KeyboardNavContext): Promise<void> {
|
|
789
|
+
if (ctx.clipboardData) {
|
|
790
|
+
handlePaste(ctx);
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// No internal clipboard — try reading from system clipboard
|
|
795
|
+
const systemData = await readSystemClipboard();
|
|
796
|
+
if (!systemData) return;
|
|
797
|
+
|
|
798
|
+
const { selection, workbook, sheetIndex, history, engine } = ctx;
|
|
799
|
+
const active = selection.getState().activeCell;
|
|
800
|
+
const sheet = workbook.sheets[sheetIndex];
|
|
801
|
+
|
|
802
|
+
// Parse TSV into a grid of strings
|
|
803
|
+
const rows = systemData.split("\n").filter((line) => line.length > 0);
|
|
804
|
+
const grid = rows.map((row) => row.split("\t"));
|
|
805
|
+
|
|
806
|
+
// Snapshot target cells for undo
|
|
807
|
+
const snapshot = new Map<string, { value: import("./workbook_model").CellValue; displayValue: string; formula?: string; styleIndex: number; numFmtCode: string } | null>();
|
|
808
|
+
for (let r = 0; r < grid.length; r++) {
|
|
809
|
+
for (let c = 0; c < grid[r].length; c++) {
|
|
810
|
+
const ref = rowColToRef(active.row + r, active.col + c);
|
|
811
|
+
const existing = sheet.getCell(ref);
|
|
812
|
+
snapshot.set(ref, existing ? {
|
|
813
|
+
value: existing.value,
|
|
814
|
+
displayValue: existing.displayValue,
|
|
815
|
+
formula: existing.formula,
|
|
816
|
+
styleIndex: existing.styleIndex,
|
|
817
|
+
numFmtCode: existing.numFmtCode,
|
|
818
|
+
} : null);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
const cmd = {
|
|
823
|
+
description: `Paste from clipboard at ${rowColToRef(active.row, active.col)}`,
|
|
824
|
+
execute() {
|
|
825
|
+
for (let r = 0; r < grid.length; r++) {
|
|
826
|
+
for (let c = 0; c < grid[r].length; c++) {
|
|
827
|
+
const ref = rowColToRef(active.row + r, active.col + c);
|
|
828
|
+
const existing = sheet.getCell(ref);
|
|
829
|
+
const styleIndex = existing?.styleIndex ?? 0;
|
|
830
|
+
const numFmtCode = existing?.numFmtCode ?? "General";
|
|
831
|
+
const text = grid[r][c];
|
|
832
|
+
const value = inferValue(text);
|
|
833
|
+
sheet.set(ref, value, sheet.styles?.get(styleIndex), numFmtCode);
|
|
834
|
+
engine?.onCellChanged(sheet.name, ref, undefined);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
},
|
|
838
|
+
undo() {
|
|
839
|
+
for (const [ref, snap] of snapshot) {
|
|
840
|
+
if (snap) {
|
|
841
|
+
sheet.set(ref, snap.value, sheet.styles?.get(snap.styleIndex), snap.numFmtCode, snap.formula);
|
|
842
|
+
} else {
|
|
843
|
+
sheet.deleteCell(ref);
|
|
844
|
+
}
|
|
845
|
+
engine?.onCellChanged(sheet.name, ref, snap?.formula);
|
|
846
|
+
}
|
|
847
|
+
},
|
|
848
|
+
};
|
|
849
|
+
history.execute(cmd);
|
|
850
|
+
ctx.onRedraw();
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/** Infer a typed value from clipboard text. */
|
|
854
|
+
function inferValue(text: string): import("./workbook_model").CellValue {
|
|
855
|
+
if (!text) return null;
|
|
856
|
+
if (text === "TRUE") return true;
|
|
857
|
+
if (text === "FALSE") return false;
|
|
858
|
+
const num = Number(text);
|
|
859
|
+
if (!isNaN(num) && text.trim() !== "") return num;
|
|
860
|
+
return text;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
async function readSystemClipboard(): Promise<string | null> {
|
|
864
|
+
try {
|
|
865
|
+
if (navigator?.clipboard?.readText) {
|
|
866
|
+
return await navigator.clipboard.readText();
|
|
867
|
+
}
|
|
868
|
+
} catch {
|
|
869
|
+
// Clipboard read may not be permitted
|
|
870
|
+
}
|
|
871
|
+
return null;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
async function writeToSystemClipboard(text: string, html: string): Promise<void> {
|
|
875
|
+
try {
|
|
876
|
+
if (navigator?.clipboard?.write) {
|
|
877
|
+
const blob = new Blob([html], { type: "text/html" });
|
|
878
|
+
const textBlob = new Blob([text], { type: "text/plain" });
|
|
879
|
+
await navigator.clipboard.write([
|
|
880
|
+
new ClipboardItem({
|
|
881
|
+
"text/html": blob,
|
|
882
|
+
"text/plain": textBlob,
|
|
883
|
+
}),
|
|
884
|
+
]);
|
|
885
|
+
} else if (navigator?.clipboard?.writeText) {
|
|
886
|
+
await navigator.clipboard.writeText(text);
|
|
887
|
+
}
|
|
888
|
+
} catch {
|
|
889
|
+
// Clipboard API may not be available in all contexts
|
|
890
|
+
}
|
|
891
|
+
}
|