@svgrid/grid 1.0.2 → 1.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.
Files changed (143) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +137 -39
  3. package/dist/GridFooter.svelte +164 -0
  4. package/dist/GridFooter.svelte.d.ts +29 -0
  5. package/dist/GridMenus.svelte +570 -0
  6. package/dist/GridMenus.svelte.d.ts +31 -0
  7. package/dist/SvGrid.controller.svelte.d.ts +429 -0
  8. package/dist/SvGrid.controller.svelte.js +1732 -0
  9. package/dist/SvGrid.css +1709 -0
  10. package/dist/SvGrid.helpers.d.ts +35 -0
  11. package/dist/SvGrid.helpers.js +160 -0
  12. package/dist/SvGrid.svelte +344 -7043
  13. package/dist/SvGrid.svelte.d.ts +4 -357
  14. package/dist/SvGrid.types.d.ts +436 -0
  15. package/dist/SvGrid.types.js +1 -0
  16. package/dist/SvGridChart.svelte +1060 -23
  17. package/dist/SvGridChart.svelte.d.ts +17 -0
  18. package/dist/build-api.d.ts +5 -0
  19. package/dist/build-api.js +527 -0
  20. package/dist/cell-render.d.ts +15 -0
  21. package/dist/cell-render.js +246 -0
  22. package/dist/cell-values.d.ts +28 -0
  23. package/dist/cell-values.js +89 -0
  24. package/dist/chart.d.ts +370 -3
  25. package/dist/chart.js +1135 -42
  26. package/dist/clipboard.d.ts +15 -0
  27. package/dist/clipboard.js +356 -0
  28. package/dist/columns.d.ts +30 -0
  29. package/dist/columns.js +277 -0
  30. package/dist/core.d.ts +1 -1
  31. package/dist/css.d.ts +3 -0
  32. package/dist/editing.d.ts +24 -0
  33. package/dist/editing.js +343 -0
  34. package/dist/editors/cell-editors.d.ts +1 -1
  35. package/dist/facet-buckets.d.ts +13 -0
  36. package/dist/facet-buckets.js +54 -0
  37. package/dist/features.d.ts +5 -0
  38. package/dist/features.js +30 -0
  39. package/dist/filter-operators.d.ts +16 -0
  40. package/dist/filter-operators.js +69 -0
  41. package/dist/hyperformula-adapter.d.ts +82 -0
  42. package/dist/hyperformula-adapter.js +73 -0
  43. package/dist/index.d.ts +5 -2
  44. package/dist/index.js +5 -2
  45. package/dist/keyboard-handlers.d.ts +7 -0
  46. package/dist/keyboard-handlers.js +197 -0
  47. package/dist/menus.d.ts +40 -0
  48. package/dist/menus.js +389 -0
  49. package/dist/named-views.d.ts +27 -0
  50. package/dist/named-views.js +39 -0
  51. package/dist/row-resize.d.ts +43 -0
  52. package/dist/row-resize.js +158 -0
  53. package/dist/scroll-sync.d.ts +9 -0
  54. package/dist/scroll-sync.js +86 -0
  55. package/dist/selection.d.ts +26 -0
  56. package/dist/selection.js +387 -0
  57. package/dist/spreadsheet.d.ts +80 -0
  58. package/dist/spreadsheet.js +194 -0
  59. package/dist/summaries.d.ts +12 -0
  60. package/dist/summaries.js +65 -0
  61. package/dist/svgrid-wrapper.types.d.ts +12 -1
  62. package/dist/svgrid.comments-autocomplete.test.d.ts +1 -0
  63. package/dist/svgrid.comments-autocomplete.test.js +96 -0
  64. package/dist/svgrid.context-menu.test.d.ts +1 -0
  65. package/dist/svgrid.context-menu.test.js +102 -0
  66. package/dist/svgrid.new-features.wrapper.test.js +30 -4
  67. package/dist/svgrid.wrapper.test.js +27 -1
  68. package/dist/virtualization/column-virtualizer.d.ts +2 -0
  69. package/dist/virtualization/scroll-scaling.d.ts +28 -0
  70. package/dist/virtualization/scroll-scaling.js +64 -0
  71. package/dist/virtualization/scroll-scaling.test.d.ts +1 -0
  72. package/dist/virtualization/scroll-scaling.test.js +86 -0
  73. package/dist/virtualization/svelte-virtualizer.svelte.d.ts +2 -0
  74. package/dist/virtualization/svelte-virtualizer.svelte.js +2 -0
  75. package/dist/virtualization/virtualizer.d.ts +7 -0
  76. package/dist/virtualization/virtualizer.js +30 -0
  77. package/package.json +1 -1
  78. package/src/GridFooter.svelte +164 -0
  79. package/src/GridMenus.svelte +570 -0
  80. package/src/SvGrid.controller.svelte.ts +2195 -0
  81. package/src/SvGrid.css +1747 -0
  82. package/src/SvGrid.helpers.test.ts +415 -0
  83. package/src/SvGrid.helpers.ts +185 -0
  84. package/src/SvGrid.svelte +348 -7043
  85. package/src/SvGrid.types.ts +456 -0
  86. package/src/SvGridChart.svelte +1060 -23
  87. package/src/build-api.coverage.test.ts +532 -0
  88. package/src/build-api.ts +663 -0
  89. package/src/cell-render.test.ts +451 -0
  90. package/src/cell-render.ts +426 -0
  91. package/src/cell-values.ts +114 -0
  92. package/src/chart-export.test.ts +370 -0
  93. package/src/chart.coverage.test.ts +814 -0
  94. package/src/chart.ts +1352 -47
  95. package/src/clipboard.test.ts +731 -0
  96. package/src/clipboard.ts +524 -0
  97. package/src/collaboration.coverage.test.ts +220 -0
  98. package/src/columns.test.ts +702 -0
  99. package/src/columns.ts +419 -0
  100. package/src/core.ts +8 -0
  101. package/src/css.d.ts +3 -0
  102. package/src/editing.test.ts +837 -0
  103. package/src/editing.ts +513 -0
  104. package/src/editors/cell-editors.coverage.test.ts +156 -0
  105. package/src/editors/cell-editors.ts +1 -0
  106. package/src/facet-buckets.test.ts +353 -0
  107. package/src/facet-buckets.ts +67 -0
  108. package/src/features.ts +128 -0
  109. package/src/filter-operators.test.ts +174 -0
  110. package/src/filter-operators.ts +87 -0
  111. package/src/hyperformula-adapter.test.ts +256 -0
  112. package/src/hyperformula-adapter.ts +124 -0
  113. package/src/index.ts +37 -0
  114. package/src/keyboard-handlers.coverage.test.ts +560 -0
  115. package/src/keyboard-handlers.ts +353 -0
  116. package/src/keyboard.ts +97 -97
  117. package/src/menus.test.ts +620 -0
  118. package/src/menus.ts +554 -0
  119. package/src/named-views.coverage.test.ts +210 -0
  120. package/src/named-views.ts +48 -0
  121. package/src/row-resize.test.ts +369 -0
  122. package/src/row-resize.ts +171 -0
  123. package/src/scroll-sync.test.ts +330 -0
  124. package/src/scroll-sync.ts +216 -0
  125. package/src/selection.test.ts +722 -0
  126. package/src/selection.ts +545 -0
  127. package/src/server-data-source.coverage.test.ts +180 -0
  128. package/src/spreadsheet.test.ts +445 -0
  129. package/src/spreadsheet.ts +246 -0
  130. package/src/summaries.ts +204 -0
  131. package/src/sv-grid-scrollbar.ts +13 -1
  132. package/src/svgrid-wrapper.types.ts +12 -1
  133. package/src/svgrid.behavior.test.ts +22 -0
  134. package/src/svgrid.comments-autocomplete.test.ts +112 -0
  135. package/src/svgrid.context-menu.test.ts +126 -0
  136. package/src/svgrid.interaction.test.ts +30 -0
  137. package/src/svgrid.new-features.wrapper.test.ts +65 -4
  138. package/src/svgrid.wrapper.test.ts +27 -1
  139. package/src/test-setup.ts +9 -6
  140. package/src/virtualization/scroll-scaling.test.ts +148 -0
  141. package/src/virtualization/scroll-scaling.ts +121 -0
  142. package/src/virtualization/svelte-virtualizer.svelte.ts +2 -0
  143. package/src/virtualization/virtualizer.ts +26 -0
@@ -0,0 +1,524 @@
1
+ // clipboard handlers extracted from the controller. Imperative event handlers
2
+ // that read/write controller state via the `ctx` handle; the reactive core
3
+ // ($state/$derived/$effect) stays in the controller.
4
+ import {
5
+ applyExcelFilter,
6
+ normalizeForFilter,
7
+ createColumnVirtualizer,
8
+ createCoreRowModel,
9
+ createExpandedRowModel,
10
+ createFilteredRowModel,
11
+ createGroupedRowModel,
12
+ createPaginatedRowModel,
13
+ createSvelteVirtualizer,
14
+ createSortedRowModel,
15
+ createSvGrid,
16
+ filterFns,
17
+ getGridCellA11yProps,
18
+ getGridCellDomId,
19
+ getGridHeaderA11yProps,
20
+ getGridRootA11yProps,
21
+ getGridRowA11yProps,
22
+ parseEditorValue,
23
+ normalizeEditorOptions,
24
+ sortFns,
25
+ tableFeatures,
26
+ rowSortingFeature,
27
+ columnFilteringFeature,
28
+ columnGroupingFeature,
29
+ type CellContext,
30
+ type EditorContext,
31
+ type CellEditorOption,
32
+ type CellEditorType,
33
+ type CellFormatter,
34
+ type CellFormatConfig,
35
+ type Column,
36
+ type ColumnDef,
37
+ type Row,
38
+ type RowData,
39
+ type SvGridApi,
40
+ type TableFeatures,
41
+ } from "./index";
42
+ import "./sv-grid-scrollbar";
43
+ import type { Snippet } from "svelte";
44
+ import { getKeyboardIntent, getNextActiveCell } from "./keyboard";
45
+ import {
46
+ formatNumericWithConfig,
47
+ getDateFormatter,
48
+ resolveDatePattern,
49
+ } from "./cell-formatting";
50
+ import {
51
+ RenderSnippetConfig,
52
+ RenderComponentConfig,
53
+ } from "./render-component";
54
+ import { buildFillPattern } from "./fill-patterns";
55
+ import { buildSparkline, toSparklineValues } from "./sparkline";
56
+ import {
57
+ resolveCellFormat,
58
+ computeColumnStat,
59
+ formatsNeedingStats,
60
+ type ColumnStat,
61
+ type ResolvedCellFormat,
62
+ } from "./conditional-formatting";
63
+ import SvGridDropdown from "./SvGridDropdown.svelte";
64
+ import type {
65
+ Props,
66
+ SelectionPoint,
67
+ SelectionRange,
68
+ CellEditState,
69
+ FilterOperator,
70
+ FilterOption,
71
+ MenuPosition,
72
+ } from "./SvGrid.types";
73
+ import {
74
+ cfTextStyle,
75
+ fmtStat,
76
+ getCellKey,
77
+ resolveClassList,
78
+ toDateInputValue,
79
+ toDateTimeLocalInputValue,
80
+ getEditableInputValue,
81
+ getEditorInputType,
82
+ toValueArray,
83
+ getOptionLabel,
84
+ getOptionColor,
85
+ colorfulChipStyle,
86
+ getEditorClass,
87
+ asDate,
88
+ clampMenuX,
89
+ cssEscape,
90
+ rawToNumber,
91
+ formatFacetNumber,
92
+ formatFacetDate,
93
+ } from "./SvGrid.helpers";
94
+ import {
95
+ filterOperatorOptions,
96
+ fallbackOperatorOption,
97
+ TEXT_OPERATORS,
98
+ NUMBER_OPERATORS,
99
+ DATE_OPERATORS,
100
+ CHECKBOX_OPERATORS,
101
+ operatorOption,
102
+ operatorsForColumn,
103
+ defaultOperatorFor,
104
+ operatorLabelFor,
105
+ } from "./filter-operators";
106
+ import {
107
+ type FacetBucket,
108
+ isBucketableColumn,
109
+ buildBuckets,
110
+ isInBucket,
111
+ } from "./facet-buckets";
112
+ import {
113
+ getColumnBaseValue,
114
+ isGroupRow,
115
+ toolPanelHeaderLabel,
116
+ formatSummaryNumeric,
117
+ getColumnAlign,
118
+ getPinnedCellValue,
119
+ getColumnAccessorValue,
120
+ columnDefMatchesId,
121
+ } from "./cell-values";
122
+
123
+ export function createClipboard<
124
+ TFeatures extends TableFeatures = TableFeatures,
125
+ TData extends RowData = RowData,
126
+ >(ctx: any) {
127
+ /** Read the raw underlying value for the cell at (rowIndex, columnId)
128
+ * for pattern extraction. */
129
+ function readCellRaw(rowIndex: number, columnId: string): unknown {
130
+ const row = ctx.internalData[rowIndex];
131
+ const column = ctx.findColumnById(columnId);
132
+ if (!row || !column?.columnDef.field) return undefined;
133
+ return (row as Record<string, unknown>)[column.columnDef.field];
134
+ }
135
+
136
+ /** Write a value into (rowIndex, columnId) without going through the
137
+ * edit lifecycle. Fires `onCellValueChange` per write so consumers
138
+ * can react (formula recompute, autosave, etc.). */
139
+ function writeCellRaw(rowIndex: number, columnId: string, value: unknown) {
140
+ const row = ctx.internalData[rowIndex];
141
+ const column = ctx.findColumnById(columnId);
142
+ if (!row || !column?.columnDef.field) return;
143
+ const field = column.columnDef.field;
144
+ const oldValue = (row as Record<string, unknown>)[field];
145
+ if (oldValue === value) return;
146
+ // Resolve the row's id BEFORE swapping internalData - otherwise the
147
+ // recomputed `allRows` references the new row object and the
148
+ // `r.original === row` lookup fails, dropping our edit out of the
149
+ // `editedCellValues` map (which getCellDisplayValue consults first).
150
+ const rowId = ctx.allRows.find((r: any) => r.original === row)?.id;
151
+ const next = ctx.internalData.slice() as Array<TData>;
152
+ next[rowIndex] = { ...row, [field]: value } as TData;
153
+ ctx.internalData = next;
154
+ if (rowId) {
155
+ const key = getCellKey(rowId, columnId);
156
+ ctx.editedCellValues = { ...ctx.editedCellValues, [key]: value };
157
+ }
158
+ ctx.props.onCellValueChange?.({
159
+ rowIndex,
160
+ columnId,
161
+ oldValue,
162
+ newValue: value,
163
+ row: next[rowIndex] as TData,
164
+ });
165
+ }
166
+
167
+ /** Apply the pattern fill on pointerup. Each NEW row (or column) is
168
+ * filled from a pattern derived from the matching column (or row) of
169
+ * the source. Handles all four drag directions. */
170
+ function applyFillPattern() {
171
+ const d = ctx.fillDrag;
172
+ if (!d) return;
173
+ // Clear fillDrag FIRST so a thrown error doesn't leave the grid
174
+ // stuck tracking the pointer.
175
+ ctx.fillDrag = null;
176
+ const newMinRow = Math.min(d.sourceMinRow, d.targetRow);
177
+ const newMaxRow = Math.max(d.sourceMaxRow, d.targetRow);
178
+ const newMinCol = Math.min(d.sourceMinCol, d.targetCol);
179
+ const newMaxCol = Math.max(d.sourceMaxCol, d.targetCol);
180
+
181
+ const verticalExtension =
182
+ newMaxRow > d.sourceMaxRow || newMinRow < d.sourceMinRow;
183
+ const horizontalExtension =
184
+ newMaxCol > d.sourceMaxCol || newMinCol < d.sourceMinCol;
185
+
186
+ if (verticalExtension) {
187
+ // For each column in the source range, build a pattern from the
188
+ // column's source values and apply to the new rows (above or below).
189
+ for (let c = d.sourceMinCol; c <= d.sourceMaxCol; c += 1) {
190
+ const column = ctx.allColumns[c];
191
+ if (!column?.columnDef.field) continue;
192
+ if (column.columnDef.editable === false) continue;
193
+ const sourceColValues: unknown[] = [];
194
+ for (let r = d.sourceMinRow; r <= d.sourceMaxRow; r += 1) {
195
+ sourceColValues.push(readCellRaw(r, column.id));
196
+ }
197
+ if (newMaxRow > d.sourceMaxRow) {
198
+ const targetRows = newMaxRow - d.sourceMaxRow;
199
+ const fills = buildFillPattern(sourceColValues, targetRows);
200
+ for (let i = 0; i < targetRows; i += 1) {
201
+ const targetRow = d.sourceMaxRow + 1 + i;
202
+ if (ctx.isCellEditableAt(targetRow, c))
203
+ writeCellRaw(targetRow, column.id, fills[i]);
204
+ }
205
+ }
206
+ if (newMinRow < d.sourceMinRow) {
207
+ // Filling upward - reverse-extrapolate.
208
+ const reversed = sourceColValues.slice().reverse();
209
+ const targetRows = d.sourceMinRow - newMinRow;
210
+ const fills = buildFillPattern(reversed, targetRows);
211
+ for (let i = 0; i < targetRows; i += 1) {
212
+ const targetRow = d.sourceMinRow - 1 - i;
213
+ if (ctx.isCellEditableAt(targetRow, c))
214
+ writeCellRaw(targetRow, column.id, fills[i]);
215
+ }
216
+ }
217
+ }
218
+ } else if (horizontalExtension) {
219
+ // For each row in source range, build pattern from the row's source
220
+ // values across columns and apply to new columns.
221
+ for (let r = d.sourceMinRow; r <= d.sourceMaxRow; r += 1) {
222
+ const sourceRowValues: unknown[] = [];
223
+ for (let c = d.sourceMinCol; c <= d.sourceMaxCol; c += 1) {
224
+ const col = ctx.allColumns[c];
225
+ if (!col) continue;
226
+ sourceRowValues.push(readCellRaw(r, col.id));
227
+ }
228
+ if (newMaxCol > d.sourceMaxCol) {
229
+ const targetCols = newMaxCol - d.sourceMaxCol;
230
+ const fills = buildFillPattern(sourceRowValues, targetCols);
231
+ for (let i = 0; i < targetCols; i += 1) {
232
+ const targetCol = d.sourceMaxCol + 1 + i;
233
+ const col = ctx.allColumns[targetCol];
234
+ if (col && ctx.isCellEditableAt(r, targetCol))
235
+ writeCellRaw(r, col.id, fills[i]);
236
+ }
237
+ }
238
+ if (newMinCol < d.sourceMinCol) {
239
+ const reversed = sourceRowValues.slice().reverse();
240
+ const targetCols = d.sourceMinCol - newMinCol;
241
+ const fills = buildFillPattern(reversed, targetCols);
242
+ for (let i = 0; i < targetCols; i += 1) {
243
+ const targetCol = d.sourceMinCol - 1 - i;
244
+ const col = ctx.allColumns[targetCol];
245
+ if (col && ctx.isCellEditableAt(r, targetCol))
246
+ writeCellRaw(r, col.id, fills[i]);
247
+ }
248
+ }
249
+ }
250
+ }
251
+
252
+ // Extend the selection to the new range so the user can immediately
253
+ // see what got filled.
254
+ ctx.selectionRange = {
255
+ anchor: { rowIndex: newMinRow, colIndex: newMinCol },
256
+ focus: { rowIndex: newMaxRow, colIndex: newMaxCol },
257
+ };
258
+ }
259
+
260
+ /** Clear the underlying value of every cell in the current selection
261
+ * range (or just the active cell when nothing is range-selected).
262
+ * Mirrors Excel's `Delete` key - values go to `null`, formatting and
263
+ * the row identity stay intact. */
264
+ function clearSelectedCellValues() {
265
+ const anchor = ctx.selectionRange.anchor;
266
+ const focus = ctx.selectionRange.focus;
267
+ if (anchor && focus) {
268
+ const minRow = Math.min(anchor.rowIndex, focus.rowIndex);
269
+ const maxRow = Math.max(anchor.rowIndex, focus.rowIndex);
270
+ const minCol = Math.min(anchor.colIndex, focus.colIndex);
271
+ const maxCol = Math.max(anchor.colIndex, focus.colIndex);
272
+ for (let r = minRow; r <= maxRow; r += 1) {
273
+ for (let c = minCol; c <= maxCol; c += 1) {
274
+ const col = ctx.allColumns[c];
275
+ if (col?.columnDef.field && ctx.isCellEditableAt(r, c)) {
276
+ writeCellRaw(r, col.id, null);
277
+ }
278
+ }
279
+ }
280
+ return;
281
+ }
282
+ const a = ctx.grid.getState().activeCell;
283
+ if (a && ctx.userHasActivatedCell) {
284
+ const col = ctx.allColumns[a.colIndex];
285
+ if (col?.columnDef.field && ctx.isCellEditableAt(a.rowIndex, a.colIndex)) {
286
+ writeCellRaw(a.rowIndex, col.id, null);
287
+ }
288
+ }
289
+ }
290
+
291
+ /** Fill-handle pointerdown - seed the drag with the current selection
292
+ * range (or active cell as a 1x1) and start tracking the pointer. */
293
+ function startFillDrag(
294
+ event: PointerEvent,
295
+ rowIndex: number,
296
+ colIndex: number,
297
+ ) {
298
+ event.stopPropagation();
299
+ event.preventDefault();
300
+ const anchor = ctx.selectionRange.anchor;
301
+ const focus = ctx.selectionRange.focus;
302
+ if (anchor && focus) {
303
+ ctx.fillDrag = {
304
+ sourceMinRow: Math.min(anchor.rowIndex, focus.rowIndex),
305
+ sourceMaxRow: Math.max(anchor.rowIndex, focus.rowIndex),
306
+ sourceMinCol: Math.min(anchor.colIndex, focus.colIndex),
307
+ sourceMaxCol: Math.max(anchor.colIndex, focus.colIndex),
308
+ targetRow: rowIndex,
309
+ targetCol: colIndex,
310
+ };
311
+ } else {
312
+ ctx.fillDrag = {
313
+ sourceMinRow: rowIndex,
314
+ sourceMaxRow: rowIndex,
315
+ sourceMinCol: colIndex,
316
+ sourceMaxCol: colIndex,
317
+ targetRow: rowIndex,
318
+ targetCol: colIndex,
319
+ };
320
+ }
321
+ // Don't `setPointerCapture` - we use `elementFromPoint` during the
322
+ // drag to find the hovered cell, and capture would route the events
323
+ // back to the handle, breaking the lookup. Window-level handlers
324
+ // (`onWindowPointerMove` / `endDragSelection`) keep tracking.
325
+ }
326
+
327
+ /** Pointermove during fill-drag. We don't get cell coords from the
328
+ * event directly - find the td under the pointer via elementFromPoint
329
+ * and read its data-svgrid-row / data-svgrid-col attributes. */
330
+ function onFillPointerMove(event: PointerEvent) {
331
+ if (!ctx.fillDrag) return;
332
+ const el = document.elementFromPoint(
333
+ event.clientX,
334
+ event.clientY,
335
+ ) as HTMLElement | null;
336
+ const cell = el?.closest(
337
+ "td[data-svgrid-row][data-svgrid-col]",
338
+ ) as HTMLElement | null;
339
+ if (!cell) return;
340
+ const r = Number(cell.dataset.svgridRow);
341
+ const c = Number(cell.dataset.svgridCol);
342
+ if (!Number.isFinite(r) || !Number.isFinite(c)) return;
343
+ if (r === ctx.fillDrag.targetRow && c === ctx.fillDrag.targetCol) return;
344
+ ctx.fillDrag = { ...ctx.fillDrag, targetRow: r, targetCol: c };
345
+ }
346
+
347
+ function onFillPointerUp() {
348
+ if (!ctx.fillDrag) return;
349
+ applyFillPattern();
350
+ }
351
+
352
+ function toggleBooleanCell(rowIndex: number, colIndex: number) {
353
+ const row = ctx.allRows[rowIndex];
354
+ const column = ctx.allColumns[colIndex];
355
+ if (!row || !column) return;
356
+ if (!column.columnDef.field) return;
357
+
358
+ const baseValue = getColumnBaseValue(row, column);
359
+ const currentValue = Boolean(
360
+ ctx.getCellDisplayValue(row.id, column.id, baseValue),
361
+ );
362
+ const nextValue = !currentValue;
363
+ (row.original as Record<string, unknown>)[column.columnDef.field] =
364
+ nextValue;
365
+
366
+ const key = getCellKey(row.id, column.id);
367
+ ctx.editedCellValues = {
368
+ ...ctx.editedCellValues,
369
+ [key]: nextValue,
370
+ };
371
+ ctx.grid.store.setState((prev: any) => ({ ...prev }));
372
+ }
373
+
374
+ /**
375
+ * Write text to the OS clipboard, with a legacy fallback for insecure
376
+ * contexts. The async Clipboard API requires a secure context (HTTPS or
377
+ * localhost); on plain HTTP - e.g. a grid served by XAMPP/Apache over a LAN
378
+ * host - `navigator.clipboard` is undefined and copy/cut would silently do
379
+ * nothing. There we fall back to a temporary <textarea> + execCommand('copy'),
380
+ * which still works in an insecure context. This MUST be called synchronously
381
+ * from the user gesture (the keydown handler) so execCommand is allowed.
382
+ */
383
+ function writeClipboardText(text: string) {
384
+ if (navigator.clipboard?.writeText) {
385
+ void navigator.clipboard.writeText(text).catch(() => legacyCopyText(text));
386
+ return;
387
+ }
388
+ legacyCopyText(text);
389
+ }
390
+
391
+ function legacyCopyText(text: string): boolean {
392
+ if (typeof document === "undefined") return false;
393
+ const active = document.activeElement as HTMLElement | null;
394
+ const ta = document.createElement("textarea");
395
+ ta.value = text;
396
+ ta.setAttribute("readonly", "");
397
+ // Offscreen but still selectable; opacity/0-size can suppress selection.
398
+ ta.style.cssText =
399
+ "position:fixed;top:0;left:-9999px;width:1px;height:1px;padding:0;border:0;";
400
+ document.body.appendChild(ta);
401
+ let ok = false;
402
+ try {
403
+ ta.select();
404
+ ta.setSelectionRange(0, text.length);
405
+ ok = document.execCommand("copy");
406
+ } catch {
407
+ ok = false;
408
+ }
409
+ document.body.removeChild(ta);
410
+ // Restore focus to the grid root so keyboard nav keeps working afterward.
411
+ (active ?? (ctx.gridRootEl as HTMLElement | null))?.focus?.({
412
+ preventScroll: true,
413
+ });
414
+ return ok;
415
+ }
416
+
417
+ function copySelectionToClipboard() {
418
+ const anchor = ctx.selectionRange.anchor;
419
+ const focus = ctx.selectionRange.focus;
420
+ if (!anchor || !focus) return;
421
+ const minRow = Math.min(anchor.rowIndex, focus.rowIndex);
422
+ const maxRow = Math.max(anchor.rowIndex, focus.rowIndex);
423
+ const minCol = Math.min(anchor.colIndex, focus.colIndex);
424
+ const maxCol = Math.max(anchor.colIndex, focus.colIndex);
425
+ const lines: Array<string> = [];
426
+ for (let r = minRow; r <= maxRow; r += 1) {
427
+ const row = ctx.allRows[r];
428
+ if (!row || isGroupRow(row)) continue;
429
+ const cells: Array<string> = [];
430
+ for (let c = minCol; c <= maxCol; c += 1) {
431
+ const column = ctx.allColumns[c];
432
+ if (!column) {
433
+ cells.push("");
434
+ continue;
435
+ }
436
+ const base = getColumnBaseValue(row, column);
437
+ const display = ctx.getCellDisplayValue(row.id, column.id, base);
438
+ cells.push(String(display ?? ""));
439
+ }
440
+ lines.push(cells.join("\t"));
441
+ }
442
+ const text = lines.join("\n");
443
+ writeClipboardText(text);
444
+ }
445
+
446
+ /**
447
+ * Clear every editable cell in the current selection range. Used by
448
+ * Ctrl/Cmd+X (after a copy) and by the Delete / Backspace keys.
449
+ * Returns true if anything was changed - the caller uses that to
450
+ * decide whether to call `preventDefault()` and refresh the store.
451
+ */
452
+ function clearSelectedCells(): boolean {
453
+ const anchor = ctx.selectionRange.anchor ?? ctx.grid.getState().activeCell;
454
+ if (!anchor) return false;
455
+ const focus = ctx.selectionRange.focus ?? anchor;
456
+ const startRow = Math.min(anchor.rowIndex, focus.rowIndex);
457
+ const startCol = Math.min(anchor.colIndex, focus.colIndex);
458
+ const endRow = Math.max(anchor.rowIndex, focus.rowIndex);
459
+ const endCol = Math.max(anchor.colIndex, focus.colIndex);
460
+
461
+ const next = ctx.internalData.slice() as Array<TData>;
462
+ let mutated = false;
463
+ for (let r = startRow; r <= endRow; r += 1) {
464
+ const row = ctx.allRows[r];
465
+ if (!row || isGroupRow(row)) continue;
466
+ const dataIndex = Number(row.id);
467
+ if (
468
+ !Number.isInteger(dataIndex) ||
469
+ dataIndex < 0 ||
470
+ dataIndex >= next.length
471
+ ) continue;
472
+ const originalRow = next[dataIndex];
473
+ if (!originalRow) continue;
474
+ const updated: Record<string, unknown> = {
475
+ ...(originalRow as Record<string, unknown>),
476
+ };
477
+ let rowChanged = false;
478
+ for (let c = startCol; c <= endCol; c += 1) {
479
+ const column = ctx.allColumns[c];
480
+ if (!column?.columnDef.field) continue;
481
+ if (!ctx.isCellEditableAt(r, c)) continue;
482
+ // Clear means: empty string for text, undefined for everything
483
+ // else. parseEditorValue handles the per-type coercion.
484
+ const editorType = (column.columnDef.editorType ??
485
+ "text") as CellEditorType;
486
+ updated[column.columnDef.field] = parseEditorValue(editorType, "");
487
+ rowChanged = true;
488
+ }
489
+ if (rowChanged) {
490
+ next[dataIndex] = updated as TData;
491
+ mutated = true;
492
+ }
493
+ }
494
+ if (mutated) {
495
+ ctx.internalData = next;
496
+ ctx.grid.store.setState((prev: any) => ({ ...prev }));
497
+ }
498
+ return mutated;
499
+ }
500
+
501
+ async function cutSelectionToClipboard(): Promise<void> {
502
+ // Cut = copy + clear. Wait for the copy to land on the clipboard so
503
+ // a slow writeText can't race the clear and leave the user with no
504
+ // way to undo via paste.
505
+ copySelectionToClipboard();
506
+ // Best-effort flush; clipboard.writeText is fire-and-forget but we
507
+ // already kicked it off above. The clear is synchronous.
508
+ clearSelectedCells();
509
+ }
510
+
511
+ return {
512
+ readCellRaw,
513
+ writeCellRaw,
514
+ applyFillPattern,
515
+ clearSelectedCellValues,
516
+ startFillDrag,
517
+ onFillPointerMove,
518
+ onFillPointerUp,
519
+ toggleBooleanCell,
520
+ copySelectionToClipboard,
521
+ clearSelectedCells,
522
+ cutSelectionToClipboard,
523
+ };
524
+ }