@svgrid/grid 1.0.2 → 1.1.1

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 (216) 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 +648 -0
  6. package/dist/GridMenus.svelte.d.ts +31 -0
  7. package/dist/SvGrid.controller.svelte.d.ts +472 -0
  8. package/dist/SvGrid.controller.svelte.js +1917 -0
  9. package/dist/SvGrid.css +2012 -0
  10. package/dist/SvGrid.helpers.d.ts +35 -0
  11. package/dist/SvGrid.helpers.js +160 -0
  12. package/dist/SvGrid.helpers.test.d.ts +1 -0
  13. package/dist/SvGrid.helpers.test.js +298 -0
  14. package/dist/SvGrid.svelte +2346 -8742
  15. package/dist/SvGrid.svelte.d.ts +4 -357
  16. package/dist/SvGrid.types.d.ts +523 -0
  17. package/dist/SvGrid.types.js +1 -0
  18. package/dist/SvGridChart.svelte +1060 -23
  19. package/dist/SvGridChart.svelte.d.ts +17 -0
  20. package/dist/aligned-grids.d.ts +6 -0
  21. package/dist/aligned-grids.js +84 -0
  22. package/dist/aligned-grids.test.d.ts +1 -0
  23. package/dist/aligned-grids.test.js +75 -0
  24. package/dist/build-api.coverage.test.d.ts +20 -0
  25. package/dist/build-api.coverage.test.js +505 -0
  26. package/dist/build-api.d.ts +5 -0
  27. package/dist/build-api.js +557 -0
  28. package/dist/cell-render.d.ts +15 -0
  29. package/dist/cell-render.js +246 -0
  30. package/dist/cell-render.test.d.ts +1 -0
  31. package/dist/cell-render.test.js +338 -0
  32. package/dist/cell-values.d.ts +28 -0
  33. package/dist/cell-values.js +89 -0
  34. package/dist/chart-export.test.d.ts +1 -0
  35. package/dist/chart-export.test.js +302 -0
  36. package/dist/chart.coverage.test.d.ts +1 -0
  37. package/dist/chart.coverage.test.js +748 -0
  38. package/dist/chart.d.ts +370 -3
  39. package/dist/chart.js +1135 -42
  40. package/dist/clipboard.d.ts +15 -0
  41. package/dist/clipboard.js +420 -0
  42. package/dist/clipboard.test.d.ts +1 -0
  43. package/dist/clipboard.test.js +700 -0
  44. package/dist/collaboration.coverage.test.d.ts +1 -0
  45. package/dist/collaboration.coverage.test.js +200 -0
  46. package/dist/column-groups.d.ts +19 -0
  47. package/dist/column-groups.js +62 -0
  48. package/dist/column-groups.test.d.ts +1 -0
  49. package/dist/column-groups.test.js +56 -0
  50. package/dist/column-types.d.ts +10 -0
  51. package/dist/column-types.js +63 -0
  52. package/dist/column-types.test.d.ts +1 -0
  53. package/dist/column-types.test.js +62 -0
  54. package/dist/columns.d.ts +30 -0
  55. package/dist/columns.js +277 -0
  56. package/dist/columns.test.d.ts +1 -0
  57. package/dist/columns.test.js +625 -0
  58. package/dist/core.d.ts +86 -1
  59. package/dist/css.d.ts +3 -0
  60. package/dist/editing.d.ts +31 -0
  61. package/dist/editing.js +529 -0
  62. package/dist/editing.test.d.ts +1 -0
  63. package/dist/editing.test.js +732 -0
  64. package/dist/editors/cell-editors.coverage.test.d.ts +1 -0
  65. package/dist/editors/cell-editors.coverage.test.js +139 -0
  66. package/dist/editors/cell-editors.d.ts +1 -1
  67. package/dist/facet-buckets.d.ts +13 -0
  68. package/dist/facet-buckets.js +54 -0
  69. package/dist/facet-buckets.test.d.ts +1 -0
  70. package/dist/facet-buckets.test.js +296 -0
  71. package/dist/features.d.ts +5 -0
  72. package/dist/features.js +30 -0
  73. package/dist/filter-operators.d.ts +16 -0
  74. package/dist/filter-operators.js +69 -0
  75. package/dist/filter-operators.test.d.ts +1 -0
  76. package/dist/filter-operators.test.js +135 -0
  77. package/dist/hyperformula-adapter.d.ts +82 -0
  78. package/dist/hyperformula-adapter.js +73 -0
  79. package/dist/hyperformula-adapter.test.d.ts +1 -0
  80. package/dist/hyperformula-adapter.test.js +205 -0
  81. package/dist/index.d.ts +9 -3
  82. package/dist/index.js +8 -2
  83. package/dist/keyboard-handlers.coverage.test.d.ts +1 -0
  84. package/dist/keyboard-handlers.coverage.test.js +495 -0
  85. package/dist/keyboard-handlers.d.ts +7 -0
  86. package/dist/keyboard-handlers.js +202 -0
  87. package/dist/menus.d.ts +40 -0
  88. package/dist/menus.js +390 -0
  89. package/dist/menus.test.d.ts +1 -0
  90. package/dist/menus.test.js +560 -0
  91. package/dist/named-views.coverage.test.d.ts +1 -0
  92. package/dist/named-views.coverage.test.js +180 -0
  93. package/dist/named-views.d.ts +27 -0
  94. package/dist/named-views.js +39 -0
  95. package/dist/row-drag.d.ts +49 -0
  96. package/dist/row-drag.js +221 -0
  97. package/dist/row-drag.test.d.ts +1 -0
  98. package/dist/row-drag.test.js +142 -0
  99. package/dist/row-resize.d.ts +43 -0
  100. package/dist/row-resize.js +158 -0
  101. package/dist/row-resize.test.d.ts +1 -0
  102. package/dist/row-resize.test.js +329 -0
  103. package/dist/scroll-sync.d.ts +9 -0
  104. package/dist/scroll-sync.js +89 -0
  105. package/dist/scroll-sync.test.d.ts +1 -0
  106. package/dist/scroll-sync.test.js +290 -0
  107. package/dist/selection.d.ts +32 -0
  108. package/dist/selection.js +427 -0
  109. package/dist/selection.multi-range.test.d.ts +1 -0
  110. package/dist/selection.multi-range.test.js +55 -0
  111. package/dist/selection.test.d.ts +1 -0
  112. package/dist/selection.test.js +647 -0
  113. package/dist/server-data-source.coverage.test.d.ts +1 -0
  114. package/dist/server-data-source.coverage.test.js +154 -0
  115. package/dist/spreadsheet.d.ts +110 -0
  116. package/dist/spreadsheet.js +242 -0
  117. package/dist/spreadsheet.test.d.ts +1 -0
  118. package/dist/spreadsheet.test.js +446 -0
  119. package/dist/summaries.d.ts +12 -0
  120. package/dist/summaries.js +65 -0
  121. package/dist/sv-grid-scrollbar.js +13 -1
  122. package/dist/svgrid-wrapper.types.d.ts +31 -1
  123. package/dist/svgrid.behavior.test.js +20 -0
  124. package/dist/svgrid.comments-autocomplete.test.d.ts +1 -0
  125. package/dist/svgrid.comments-autocomplete.test.js +96 -0
  126. package/dist/svgrid.context-menu.test.d.ts +1 -0
  127. package/dist/svgrid.context-menu.test.js +102 -0
  128. package/dist/svgrid.interaction.test.js +31 -0
  129. package/dist/svgrid.new-features.wrapper.test.js +64 -6
  130. package/dist/svgrid.wrapper.test.js +27 -1
  131. package/dist/test-setup.js +9 -3
  132. package/dist/virtualization/column-virtualizer.d.ts +2 -0
  133. package/dist/virtualization/scroll-scaling.d.ts +45 -0
  134. package/dist/virtualization/scroll-scaling.js +99 -0
  135. package/dist/virtualization/scroll-scaling.test.d.ts +1 -0
  136. package/dist/virtualization/scroll-scaling.test.js +127 -0
  137. package/dist/virtualization/svelte-virtualizer.svelte.d.ts +2 -0
  138. package/dist/virtualization/svelte-virtualizer.svelte.js +2 -0
  139. package/dist/virtualization/virtualizer.d.ts +7 -0
  140. package/dist/virtualization/virtualizer.js +30 -0
  141. package/package.json +2 -1
  142. package/src/GridFooter.svelte +164 -0
  143. package/src/GridMenus.svelte +648 -0
  144. package/src/SvGrid.controller.svelte.ts +2352 -0
  145. package/src/SvGrid.css +2012 -0
  146. package/src/SvGrid.helpers.test.ts +415 -0
  147. package/src/SvGrid.helpers.ts +185 -0
  148. package/src/SvGrid.svelte +2346 -8742
  149. package/src/SvGrid.types.ts +537 -0
  150. package/src/SvGridChart.svelte +1060 -23
  151. package/src/aligned-grids.test.ts +80 -0
  152. package/src/aligned-grids.ts +87 -0
  153. package/src/build-api.coverage.test.ts +532 -0
  154. package/src/build-api.ts +683 -0
  155. package/src/cell-render.test.ts +451 -0
  156. package/src/cell-render.ts +426 -0
  157. package/src/cell-values.ts +114 -0
  158. package/src/chart-export.test.ts +370 -0
  159. package/src/chart.coverage.test.ts +814 -0
  160. package/src/chart.ts +1352 -47
  161. package/src/clipboard.test.ts +780 -0
  162. package/src/clipboard.ts +552 -0
  163. package/src/collaboration.coverage.test.ts +220 -0
  164. package/src/column-groups.test.ts +59 -0
  165. package/src/column-groups.ts +80 -0
  166. package/src/column-types.test.ts +68 -0
  167. package/src/column-types.ts +82 -0
  168. package/src/columns.test.ts +702 -0
  169. package/src/columns.ts +419 -0
  170. package/src/core.ts +86 -0
  171. package/src/css.d.ts +3 -0
  172. package/src/editing.test.ts +837 -0
  173. package/src/editing.ts +669 -0
  174. package/src/editors/cell-editors.coverage.test.ts +156 -0
  175. package/src/editors/cell-editors.ts +1 -0
  176. package/src/facet-buckets.test.ts +353 -0
  177. package/src/facet-buckets.ts +67 -0
  178. package/src/features.ts +128 -0
  179. package/src/filter-operators.test.ts +174 -0
  180. package/src/filter-operators.ts +87 -0
  181. package/src/hyperformula-adapter.test.ts +256 -0
  182. package/src/hyperformula-adapter.ts +124 -0
  183. package/src/index.ts +49 -0
  184. package/src/keyboard-handlers.coverage.test.ts +560 -0
  185. package/src/keyboard-handlers.ts +353 -0
  186. package/src/keyboard.ts +97 -97
  187. package/src/menus.test.ts +620 -0
  188. package/src/menus.ts +555 -0
  189. package/src/named-views.coverage.test.ts +210 -0
  190. package/src/named-views.ts +48 -0
  191. package/src/row-drag.test.ts +168 -0
  192. package/src/row-drag.ts +255 -0
  193. package/src/row-resize.test.ts +369 -0
  194. package/src/row-resize.ts +171 -0
  195. package/src/scroll-sync.test.ts +330 -0
  196. package/src/scroll-sync.ts +218 -0
  197. package/src/selection.multi-range.test.ts +61 -0
  198. package/src/selection.test.ts +722 -0
  199. package/src/selection.ts +579 -0
  200. package/src/server-data-source.coverage.test.ts +180 -0
  201. package/src/spreadsheet.test.ts +489 -0
  202. package/src/spreadsheet.ts +304 -0
  203. package/src/summaries.ts +204 -0
  204. package/src/sv-grid-scrollbar.ts +13 -1
  205. package/src/svgrid-wrapper.types.ts +31 -1
  206. package/src/svgrid.behavior.test.ts +22 -0
  207. package/src/svgrid.comments-autocomplete.test.ts +112 -0
  208. package/src/svgrid.context-menu.test.ts +126 -0
  209. package/src/svgrid.interaction.test.ts +30 -0
  210. package/src/svgrid.new-features.wrapper.test.ts +67 -6
  211. package/src/svgrid.wrapper.test.ts +27 -1
  212. package/src/test-setup.ts +9 -6
  213. package/src/virtualization/scroll-scaling.test.ts +148 -0
  214. package/src/virtualization/scroll-scaling.ts +121 -0
  215. package/src/virtualization/svelte-virtualizer.svelte.ts +2 -0
  216. package/src/virtualization/virtualizer.ts +26 -0
package/dist/core.d.ts CHANGED
@@ -37,6 +37,30 @@ export type CellContext<TData extends RowData> = {
37
37
  table: SvGrid<TData>;
38
38
  getValue: () => unknown;
39
39
  };
40
+ /** Params passed to a column's `colSpan(...)` / `rowSpan(...)` callbacks. */
41
+ export type CellSpanParams<TData extends RowData = RowData> = {
42
+ /** The row's underlying data object. */
43
+ data: TData;
44
+ /** Display-row index in the current (filtered/sorted) row set. */
45
+ rowIndex: number;
46
+ /** The column's id. */
47
+ columnId: string;
48
+ /** The cell's base value for this column. */
49
+ value: unknown;
50
+ };
51
+ /** Params passed to a column's `valueParser(...)` on edit commit. */
52
+ export type ValueParserParams<TData extends RowData = RowData> = {
53
+ /** The value after built-in per-`editorType` coercion. */
54
+ newValue: unknown;
55
+ /** The cell's previous value. */
56
+ oldValue: unknown;
57
+ /** The raw string the editor produced (pre-coercion). */
58
+ rawInput: string;
59
+ /** The row's underlying data object. */
60
+ data: TData;
61
+ /** The column's id. */
62
+ columnId: string;
63
+ };
40
64
  /**
41
65
  * Context passed to a custom `cellEditor` snippet/component. Three write
42
66
  * helpers cover the lifecycle:
@@ -110,7 +134,46 @@ export type ColumnDef<TFeatures extends TableFeatures, TData extends RowData> =
110
134
  footer?: ColumnDefTemplate<HeaderContext<TData>>;
111
135
  cell?: ColumnDefTemplate<CellContext<TData>>;
112
136
  columns?: Array<ColumnDef<TFeatures, TData>>;
113
- editorType?: 'text' | 'number' | 'date' | 'datetime' | 'time' | 'password' | 'checkbox' | 'list' | 'chips' | 'select' | 'rich-select' | 'textarea' | 'color' | 'rating';
137
+ /**
138
+ * Declarative cell spanning (merged cells). Return how many COLUMNS this
139
+ * cell spans to the right (1 = no span). Value-driven, AG-Grid-style. Feed
140
+ * `spansToMerges(rows, columns)` into `spreadsheetLayout` to apply - it uses
141
+ * the same real `colspan`/`rowspan` merge engine (no separate code path).
142
+ */
143
+ colSpan?: (params: CellSpanParams<TData>) => number;
144
+ /**
145
+ * Declarative cell spanning (merged cells). Return how many ROWS this cell
146
+ * spans downward (1 = no span). See `colSpan` for how to apply.
147
+ */
148
+ rowSpan?: (params: CellSpanParams<TData>) => number;
149
+ /**
150
+ * High-level data type for the column. A convenience that resolves to the
151
+ * right `editorType`, alignment, date `format`, and filter operators without
152
+ * setting each by hand:
153
+ * 'text' → text editor, left-aligned
154
+ * 'number' → number editor, right-aligned, numeric filter operators
155
+ * 'boolean' → checkbox editor, centered
156
+ * 'date' → date editor (Date values), right-aligned, `{ type: 'date' }` format
157
+ * 'dateString' → date editor for ISO date STRINGS (e.g. '2026-06-27')
158
+ * Anything you set explicitly (`editorType`, `align`, `format`) still wins -
159
+ * `cellDataType` only fills the gaps. Grid-level `inferColumnTypes` infers
160
+ * this from the first data row for columns that declare neither.
161
+ */
162
+ cellDataType?: 'text' | 'number' | 'boolean' | 'date' | 'dateString';
163
+ /**
164
+ * For a column INSIDE a collapsible column group: `'open'` shows this column
165
+ * only while the group is expanded, `'closed'` only while collapsed. Omit to
166
+ * always show it. Setting it on any direct child gives the parent group a
167
+ * collapse toggle. Pair with `openByDefault` on the group.
168
+ */
169
+ columnGroupShow?: 'open' | 'closed';
170
+ /**
171
+ * For a GROUP column (one with `columns: [...]`): start the group expanded.
172
+ * Defaults to `false` (collapsed), matching AG Grid - so only the always-on
173
+ * and `columnGroupShow: 'closed'` children show until the user expands it.
174
+ */
175
+ openByDefault?: boolean;
176
+ editorType?: 'text' | 'number' | 'date' | 'datetime' | 'time' | 'password' | 'checkbox' | 'list' | 'chips' | 'select' | 'rich-select' | 'autocomplete' | 'textarea' | 'color' | 'rating';
114
177
  /**
115
178
  * Custom in-cell editor. Receives the cell context PLUS a `commit(value)`
116
179
  * and `cancel()` helper. Use when none of the built-in `editorType`s fit;
@@ -143,6 +206,21 @@ export type ColumnDef<TFeatures extends TableFeatures, TData extends RowData> =
143
206
  * `false`.
144
207
  */
145
208
  editable?: boolean | ((context: CellContext<TData>) => boolean);
209
+ /**
210
+ * Transform the committed edit value before it is written to the row.
211
+ * Runs after the built-in per-`editorType` coercion, so `newValue` is
212
+ * already type-parsed; return the final value to store (e.g. round a
213
+ * number, uppercase a code, look up an id). AG-Grid-style `valueParser`.
214
+ */
215
+ valueParser?: (params: ValueParserParams<TData>) => unknown;
216
+ /**
217
+ * Briefly flash / highlight this column's cell when its value changes
218
+ * (streaming feeds, edits, server pushes). `true` uses the default flash;
219
+ * pass `{ className }` to apply your own animation class instead.
220
+ */
221
+ cellFlash?: boolean | {
222
+ className?: string;
223
+ };
146
224
  /**
147
225
  * When `false`, this column never shows a sort indicator and clicking
148
226
  * its header is a no-op - `api.setSort(thisColumn, ...)` is also
@@ -203,6 +281,13 @@ export type ColumnDef<TFeatures extends TableFeatures, TData extends RowData> =
203
281
  sparkline?: SparklineConfig;
204
282
  /** Initial column width in pixels. Falls back to the grid's `columnWidth` prop. */
205
283
  width?: number;
284
+ /**
285
+ * Initial visibility. Set `false` to start the column hidden while still
286
+ * listing it in the Choose Columns UI for the user to re-enable. Applied
287
+ * once at mount; after that `api.setColumnVisible` / user toggles win.
288
+ * On a group column, `false` hides the whole group's leaf columns.
289
+ */
290
+ visible?: boolean;
206
291
  /**
207
292
  * Horizontal alignment for header and body cells. When omitted, the
208
293
  * default is inferred from `editorType`:
package/dist/css.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ // Ambient declaration so TypeScript accepts side-effect CSS imports
2
+ // (e.g. `import "./SvGrid.css"`). The bundler resolves the actual asset.
3
+ declare module "*.css";
@@ -0,0 +1,31 @@
1
+ import { type Column, type Row, type RowData, type TableFeatures } from "./index";
2
+ import "./sv-grid-scrollbar";
3
+ type HistoryStep = {
4
+ rowId: string;
5
+ columnId: string;
6
+ field: string;
7
+ before: unknown;
8
+ after: unknown;
9
+ };
10
+ export declare function createEditing<TFeatures extends TableFeatures = TableFeatures, TData extends RowData = RowData>(ctx: any): {
11
+ isCellEditable: (column: Column<TData>, row?: Row<TData>) => boolean;
12
+ isCellEditableAt: (rowIndex: number, colIndex: number) => boolean;
13
+ getRowColumnValue: (row: Row<TData>, columnId: string) => unknown;
14
+ getCellDisplayValue: (rowId: string, columnId: string, baseValue: unknown) => any;
15
+ startEditingWithChar: (rowIndex: number, colIndex: number, char: string) => boolean;
16
+ startEditing: (rowIndex: number, columnId: string) => boolean;
17
+ stopEditing: (cancel?: boolean) => boolean;
18
+ startFullRowEdit: (rowIndex: number) => boolean;
19
+ setFullRowDraft: (columnId: string, value: unknown) => void;
20
+ commitFullRowEdit: () => void;
21
+ cancelFullRowEdit: () => void;
22
+ saveEditingCell: () => void;
23
+ applyHistoryStep: (step: HistoryStep, direction: "undo" | "redo") => void;
24
+ updateEditingCellValue: (value: string) => void;
25
+ onEditorKeyDown: (event: KeyboardEvent) => void;
26
+ focusOnMount: (node: HTMLInputElement | HTMLTextAreaElement) => void;
27
+ onCellDoubleClick: (rowIndex: number, colIndex: number) => void;
28
+ pasteFromClipboard: () => Promise<void>;
29
+ onGridPaste: (event: ClipboardEvent) => void;
30
+ };
31
+ export {};
@@ -0,0 +1,529 @@
1
+ // editing handlers extracted from the controller. Imperative event handlers
2
+ // reading/writing controller state via the `ctx` handle; the reactive core
3
+ // ($state/$derived/$effect) stays in the controller.
4
+ import { parseEditorValue, } from "./index";
5
+ import "./sv-grid-scrollbar";
6
+ import { getCellKey, toValueArray, } from "./SvGrid.helpers";
7
+ import { getColumnBaseValue, isGroupRow, } from "./cell-values";
8
+ export function createEditing(ctx) {
9
+ /**
10
+ * Resolve `columnDef.editable` for one row × column pair.
11
+ *
12
+ * - `undefined` / `true` → editable
13
+ * - `false` → not editable (column-level lockdown, fastest path)
14
+ * - function → call it with the cell's context and honour the
15
+ * boolean it returns (cell-level lockdown - used for things like
16
+ * "this field is owned by the server, this one isn't, by row")
17
+ *
18
+ * Called from every editing entry point: double-click, type-to-edit,
19
+ * fill-handle drag, Delete-clear, clipboard paste. A central helper
20
+ * keeps the cell-level callback signature consistent and saves a
21
+ * lookup of CellContext at each site.
22
+ */
23
+ function isCellEditable(column, row) {
24
+ const editable = column.columnDef.editable;
25
+ if (editable === false)
26
+ return false;
27
+ if (typeof editable !== "function")
28
+ return true;
29
+ if (!row)
30
+ return true;
31
+ const cellCtx = {
32
+ cell: {
33
+ id: `${row.id}_${column.id}`,
34
+ row,
35
+ column,
36
+ getValue: () => getColumnBaseValue(row, column),
37
+ getContext: () => cellCtx,
38
+ },
39
+ row,
40
+ column,
41
+ table: ctx.grid,
42
+ getValue: () => getColumnBaseValue(row, column),
43
+ };
44
+ try {
45
+ return editable(cellCtx) !== false;
46
+ }
47
+ catch {
48
+ // A throwing predicate shouldn't crash the grid - log and treat
49
+ // as not-editable so any error biases toward safety.
50
+ return false;
51
+ }
52
+ }
53
+ /** Variant that takes raw row + column indices for the rare callers
54
+ * (fill-handle, paste, Delete) that already work in index-space. */
55
+ function isCellEditableAt(rowIndex, colIndex) {
56
+ const row = ctx.allRows[rowIndex];
57
+ const column = ctx.allColumns[colIndex];
58
+ if (!column)
59
+ return false;
60
+ return isCellEditable(column, row);
61
+ }
62
+ function getRowColumnValue(row, columnId) {
63
+ const column = ctx.allColumns.find((entry) => entry.id === columnId);
64
+ return column
65
+ ? getColumnBaseValue(row, column)
66
+ : row.getCellValueByColumnId(columnId);
67
+ }
68
+ function getCellDisplayValue(rowId, columnId, baseValue) {
69
+ const key = getCellKey(rowId, columnId);
70
+ return key in ctx.editedCellValues ? ctx.editedCellValues[key] : baseValue;
71
+ }
72
+ /** Start editing seeded with a typed character (Excel-style type-to-edit). */
73
+ function startEditingWithChar(rowIndex, colIndex, char) {
74
+ if (!ctx.editingEnabled)
75
+ return false;
76
+ const row = ctx.allRows[rowIndex];
77
+ const column = ctx.allColumns[colIndex];
78
+ if (!row || !column || isGroupRow(row))
79
+ return false;
80
+ if (!isCellEditable(column, row))
81
+ return false;
82
+ const editorType = (column.columnDef.editorType ??
83
+ "text");
84
+ if (editorType === "checkbox" ||
85
+ editorType === "date" ||
86
+ editorType === "datetime" ||
87
+ editorType === "list" ||
88
+ editorType === "chips") {
89
+ return false;
90
+ }
91
+ if (editorType === "number" && !/[0-9.+\-]/.test(char))
92
+ return false;
93
+ ctx.editorSelectAll = false;
94
+ ctx.editingCell = {
95
+ rowId: row.id,
96
+ columnId: column.id,
97
+ editorType,
98
+ value: char,
99
+ };
100
+ ctx.setActiveCell(rowIndex, colIndex);
101
+ ctx.setSelection(rowIndex, colIndex);
102
+ return true;
103
+ }
104
+ function saveEditingCell() {
105
+ if (!ctx.editingCell)
106
+ return;
107
+ const row = ctx.allRows.find((entry) => entry.id === ctx.editingCell?.rowId);
108
+ const column = ctx.allColumns.find((entry) => entry.id === ctx.editingCell?.columnId);
109
+ const parsedValue = parseEditorValue(ctx.editingCell.editorType, ctx.editingCell.value, {
110
+ multiple: column?.columnDef.editorMultiple === true,
111
+ });
112
+ let oldValue = undefined;
113
+ let finalValue = parsedValue;
114
+ if (row?.original && column?.columnDef.field) {
115
+ oldValue = row.original[column.columnDef.field];
116
+ // Per-column valueParser refines the type-coerced value before storing.
117
+ const parser = column.columnDef.valueParser;
118
+ if (parser) {
119
+ finalValue = parser({
120
+ newValue: parsedValue,
121
+ oldValue,
122
+ rawInput: ctx.editingCell.value,
123
+ data: row.original,
124
+ columnId: ctx.editingCell.columnId,
125
+ });
126
+ }
127
+ row.original[column.columnDef.field] =
128
+ finalValue;
129
+ }
130
+ const key = getCellKey(ctx.editingCell.rowId, ctx.editingCell.columnId);
131
+ ctx.editedCellValues = {
132
+ ...ctx.editedCellValues,
133
+ [key]: finalValue,
134
+ };
135
+ ctx.grid.store.setState((prev) => ({ ...prev }));
136
+ // Record into the history at the current pointer. Any forward
137
+ // history (steps the user could have redone) is truncated - this
138
+ // is the standard "edit invalidates redo" rule.
139
+ if (oldValue !== finalValue && row?.original && column?.columnDef.field) {
140
+ const step = {
141
+ rowId: ctx.editingCell.rowId,
142
+ columnId: ctx.editingCell.columnId,
143
+ field: column.columnDef.field,
144
+ before: oldValue,
145
+ after: finalValue,
146
+ };
147
+ const truncated = ctx.history.slice(0, ctx.historyPtr + 1);
148
+ truncated.push(step);
149
+ // Cap the buffer at UNDO_LIMIT; drop the OLDEST entries so the
150
+ // pointer stays valid relative to the newest steps.
151
+ if (truncated.length > ctx.UNDO_LIMIT) {
152
+ const drop = truncated.length - ctx.UNDO_LIMIT;
153
+ ctx.history = truncated.slice(drop);
154
+ ctx.historyPtr = ctx.history.length - 1;
155
+ }
156
+ else {
157
+ ctx.history = truncated;
158
+ ctx.historyPtr = ctx.history.length - 1;
159
+ }
160
+ ctx.historyVersion += 1;
161
+ }
162
+ // Notify the consumer AFTER the row has been updated so any callback-
163
+ // driven recompute (cascade totals, server save, undo stack) sees the
164
+ // post-write state. `rowIndex` matches the position in `props.data`.
165
+ if (ctx.props.onCellValueChange && row?.original && column) {
166
+ const rowIndex = ctx.internalData.indexOf(row.original);
167
+ ctx.props.onCellValueChange({
168
+ rowIndex,
169
+ columnId: column.id,
170
+ oldValue,
171
+ newValue: finalValue,
172
+ row: row.original,
173
+ });
174
+ }
175
+ ctx.editingCell = null;
176
+ }
177
+ /** Apply an undo / redo step directly to the underlying row, bypassing
178
+ * the editor pipeline so we don't accidentally re-push to the stack. */
179
+ function applyHistoryStep(step, direction) {
180
+ const row = ctx.allRows.find((r) => r.id === step.rowId);
181
+ const col = ctx.allColumns.find((c) => c.id === step.columnId);
182
+ if (!row?.original || !col)
183
+ return;
184
+ const value = direction === 'undo' ? step.before : step.after;
185
+ row.original[step.field] = value;
186
+ const key = getCellKey(step.rowId, step.columnId);
187
+ ctx.editedCellValues = { ...ctx.editedCellValues, [key]: value };
188
+ ctx.grid.store.setState((prev) => ({ ...prev }));
189
+ if (ctx.props.onCellValueChange) {
190
+ const rowIndex = ctx.internalData.indexOf(row.original);
191
+ ctx.props.onCellValueChange({
192
+ rowIndex,
193
+ columnId: step.columnId,
194
+ oldValue: direction === 'undo' ? step.after : step.before,
195
+ newValue: value,
196
+ row: row.original,
197
+ });
198
+ }
199
+ }
200
+ function updateEditingCellValue(value) {
201
+ ctx.editingCell = ctx.editingCell ? { ...ctx.editingCell, value } : ctx.editingCell;
202
+ }
203
+ function onEditorKeyDown(event) {
204
+ event.stopPropagation();
205
+ if (event.key === "Enter") {
206
+ event.preventDefault();
207
+ saveEditingCell();
208
+ ctx.gridRootEl?.focus({ preventScroll: true });
209
+ }
210
+ else if (event.key === "Escape") {
211
+ event.preventDefault();
212
+ ctx.editingCell = null;
213
+ ctx.gridRootEl?.focus({ preventScroll: true });
214
+ }
215
+ }
216
+ function focusOnMount(node) {
217
+ const selectAll = ctx.editorSelectAll;
218
+ requestAnimationFrame(() => {
219
+ node.focus({ preventScroll: true });
220
+ try {
221
+ if (selectAll) {
222
+ node.select();
223
+ }
224
+ else {
225
+ const end = node.value.length;
226
+ node.setSelectionRange(end, end);
227
+ }
228
+ }
229
+ catch {
230
+ /* date/number inputs may not support text selection */
231
+ }
232
+ });
233
+ }
234
+ function onCellDoubleClick(rowIndex, colIndex) {
235
+ if (!ctx.editingEnabled)
236
+ return;
237
+ const row = ctx.allRows[rowIndex];
238
+ const column = ctx.allColumns[colIndex];
239
+ if (!row || !column)
240
+ return;
241
+ if (isGroupRow(row))
242
+ return;
243
+ // Full-row editing: put the whole row into edit instead of one cell.
244
+ if (ctx.props.fullRowEditing) {
245
+ if (ctx.fullRowEdit?.rowId === row.id)
246
+ return;
247
+ startFullRowEdit(rowIndex);
248
+ return;
249
+ }
250
+ if (!isCellEditable(column, row))
251
+ return;
252
+ if (ctx.editingCell?.rowId === row.id && ctx.editingCell?.columnId === column.id)
253
+ return;
254
+ const editorType = (column.columnDef.editorType ??
255
+ "text");
256
+ ctx.editorSelectAll = true;
257
+ let initialValue = getCellDisplayValue(row.id, column.id, row.getCellValueByColumnId(column.id));
258
+ if (editorType === "list" || editorType === "chips") {
259
+ if (column.columnDef.editorMultiple) {
260
+ // Seed with an array so the editor can mutate slot-by-slot.
261
+ initialValue = toValueArray(initialValue);
262
+ }
263
+ else if (Array.isArray(initialValue)) {
264
+ initialValue = initialValue[0] ?? "";
265
+ }
266
+ }
267
+ ctx.editingCell = {
268
+ rowId: row.id,
269
+ columnId: column.id,
270
+ editorType,
271
+ value: initialValue,
272
+ };
273
+ ctx.setActiveCell(rowIndex, colIndex);
274
+ ctx.setSelection(rowIndex, colIndex);
275
+ }
276
+ async function pasteFromClipboard() {
277
+ // Preferred path: the async Clipboard API. Requires a secure context
278
+ // (HTTPS or localhost) AND read permission; it also works on Firefox,
279
+ // where the native `paste` event does NOT fire on a non-editable element.
280
+ // On plain HTTP (a XAMPP/Apache LAN host) `navigator.clipboard` is
281
+ // undefined - there the Ctrl+V keydown handler skips preventDefault and
282
+ // lets the browser deliver a native `paste` event to `onGridPaste`.
283
+ if (!navigator.clipboard?.readText)
284
+ return;
285
+ let text;
286
+ try {
287
+ text = await navigator.clipboard.readText();
288
+ }
289
+ catch {
290
+ return;
291
+ }
292
+ applyPastedText(text);
293
+ }
294
+ /**
295
+ * Native `paste` ClipboardEvent fallback (insecure-context path). A real
296
+ * Ctrl/Cmd+V on the focused grid root fires this with `clipboardData`
297
+ * readable even over plain HTTP and with no permission prompt. Wired on the
298
+ * grid root in SvGrid.svelte; the keydown handler lets the gesture through
299
+ * (no preventDefault) whenever the async API is unavailable.
300
+ */
301
+ function onGridPaste(event) {
302
+ // When the async API is available the keydown handler already pasted via
303
+ // pasteFromClipboard(); ignore the (suppressed) native event so we don't
304
+ // paste twice.
305
+ if (typeof navigator.clipboard?.readText === "function")
306
+ return;
307
+ const text = event.clipboardData?.getData("text/plain") ?? "";
308
+ if (!text)
309
+ return;
310
+ event.preventDefault();
311
+ applyPastedText(text);
312
+ }
313
+ function applyPastedText(text) {
314
+ const anchor = ctx.selectionRange.anchor ?? ctx.grid.getState().activeCell;
315
+ if (!anchor)
316
+ return;
317
+ const focus = ctx.selectionRange.focus ?? anchor;
318
+ const startRow = Math.min(anchor.rowIndex, focus.rowIndex);
319
+ const startCol = Math.min(anchor.colIndex, focus.colIndex);
320
+ const endRow = Math.max(anchor.rowIndex, focus.rowIndex);
321
+ const endCol = Math.max(anchor.colIndex, focus.colIndex);
322
+ const lines = text.replace(/\r\n/g, "\n").split("\n");
323
+ if (lines.length > 0 && lines[lines.length - 1] === "")
324
+ lines.pop();
325
+ if (!lines.length)
326
+ return;
327
+ // Excel/Sheets behaviour: when the clipboard holds a single cell and
328
+ // the selection is a range, fill the entire range with that value.
329
+ // For larger clipboards (multi-row/col TSV), paste at the top-left
330
+ // corner and let the clipboard determine the span (the original
331
+ // behaviour).
332
+ const clipboardIsSingleCell = lines.length === 1 && (lines[0]?.split("\t").length ?? 0) === 1;
333
+ const selectionIsRange = startRow !== endRow || startCol !== endCol;
334
+ const fillRange = clipboardIsSingleCell && selectionIsRange;
335
+ const rowSpan = fillRange ? endRow - startRow + 1 : lines.length;
336
+ const colSpan = fillRange
337
+ ? endCol - startCol + 1
338
+ : Math.max(...lines.map((l) => l.split("\t").length));
339
+ const next = ctx.internalData.slice();
340
+ for (let i = 0; i < rowSpan; i += 1) {
341
+ const targetRowIndex = startRow + i;
342
+ const row = ctx.allRows[targetRowIndex];
343
+ if (!row || isGroupRow(row))
344
+ continue;
345
+ // Map the visible row back to its index in the data array via row.id (default = String(index)).
346
+ const dataIndex = Number(row.id);
347
+ if (!Number.isInteger(dataIndex) ||
348
+ dataIndex < 0 ||
349
+ dataIndex >= next.length)
350
+ continue;
351
+ const originalRow = next[dataIndex];
352
+ if (!originalRow)
353
+ continue;
354
+ const sourceCells = fillRange
355
+ ? null
356
+ : lines[i]?.split("\t") ?? [];
357
+ const updated = {
358
+ ...originalRow,
359
+ };
360
+ for (let j = 0; j < colSpan; j += 1) {
361
+ const column = ctx.allColumns[startCol + j];
362
+ if (!column?.columnDef.field)
363
+ continue;
364
+ if (!isCellEditableAt(targetRowIndex, startCol + j))
365
+ continue;
366
+ const editorType = (column.columnDef.editorType ??
367
+ "text");
368
+ const raw = fillRange ? lines[0] : sourceCells?.[j] ?? "";
369
+ updated[column.columnDef.field] = parseEditorValue(editorType, raw);
370
+ }
371
+ next[dataIndex] = updated;
372
+ }
373
+ ctx.internalData = next;
374
+ ctx.grid.store.setState((prev) => ({ ...prev }));
375
+ }
376
+ /** Programmatically begin editing a cell (mirrors a double-click). */
377
+ function startEditing(rowIndex, columnId) {
378
+ if (!ctx.editingEnabled)
379
+ return false;
380
+ const row = ctx.allRows[rowIndex];
381
+ if (!row || isGroupRow(row))
382
+ return false;
383
+ const colIndex = ctx.allColumns.findIndex((c) => c.id === columnId);
384
+ if (colIndex < 0)
385
+ return false;
386
+ const column = ctx.allColumns[colIndex];
387
+ if (!isCellEditable(column, row))
388
+ return false;
389
+ if (ctx.editingCell?.rowId === row.id &&
390
+ ctx.editingCell?.columnId === columnId)
391
+ return true;
392
+ onCellDoubleClick(rowIndex, colIndex);
393
+ return (ctx.editingCell?.rowId === row.id &&
394
+ ctx.editingCell?.columnId === columnId);
395
+ }
396
+ /** Commit (default) or cancel the active edit, if any. */
397
+ function stopEditing(cancel = false) {
398
+ if (!ctx.editingCell)
399
+ return false;
400
+ if (cancel) {
401
+ ctx.editingCell = null;
402
+ }
403
+ else {
404
+ saveEditingCell();
405
+ }
406
+ return true;
407
+ }
408
+ // ---- Full-row editing -------------------------------------------------
409
+ /** Put the whole row into edit mode, seeding a draft per editable column. */
410
+ function startFullRowEdit(rowIndex) {
411
+ if (!ctx.editingEnabled || !ctx.props.fullRowEditing)
412
+ return false;
413
+ const row = ctx.allRows[rowIndex];
414
+ if (!row || isGroupRow(row))
415
+ return false;
416
+ if (ctx.fullRowEdit && ctx.fullRowEdit.rowId !== row.id)
417
+ commitFullRowEdit();
418
+ ctx.editingCell = null; // never both at once
419
+ const draft = {};
420
+ for (const column of ctx.allColumns) {
421
+ if (!column.columnDef.field)
422
+ continue;
423
+ if (!isCellEditable(column, row))
424
+ continue;
425
+ draft[column.id] = getCellDisplayValue(row.id, column.id, row.getCellValueByColumnId(column.id));
426
+ }
427
+ ctx.fullRowEdit = { rowId: row.id, draft };
428
+ ctx.setActiveCell(rowIndex, Math.max(0, ctx.allColumns.findIndex((c) => c.id in draft)));
429
+ return true;
430
+ }
431
+ /** Stage one column's draft value during full-row editing. */
432
+ function setFullRowDraft(columnId, value) {
433
+ const fr = ctx.fullRowEdit;
434
+ if (!fr)
435
+ return;
436
+ ctx.fullRowEdit = { rowId: fr.rowId, draft: { ...fr.draft, [columnId]: value } };
437
+ }
438
+ /** Commit every drafted column of the full-row edit in one data update. */
439
+ function commitFullRowEdit() {
440
+ const fr = ctx.fullRowEdit;
441
+ if (!fr)
442
+ return;
443
+ const row = ctx.allRows.find((r) => r.id === fr.rowId);
444
+ if (!row?.original) {
445
+ ctx.fullRowEdit = null;
446
+ return;
447
+ }
448
+ const changed = [];
449
+ for (const column of ctx.allColumns) {
450
+ if (!(column.id in fr.draft))
451
+ continue;
452
+ const field = column.columnDef.field;
453
+ if (!field)
454
+ continue;
455
+ const editorType = (column.columnDef.editorType ?? "text");
456
+ const parsed = parseEditorValue(editorType, fr.draft[column.id], {
457
+ multiple: column.columnDef.editorMultiple === true,
458
+ });
459
+ const oldValue = row.original[field];
460
+ const parser = column.columnDef.valueParser;
461
+ const finalValue = parser
462
+ ? parser({
463
+ newValue: parsed,
464
+ oldValue,
465
+ rawInput: String(fr.draft[column.id] ?? ""),
466
+ data: row.original,
467
+ columnId: column.id,
468
+ })
469
+ : parsed;
470
+ if (oldValue !== finalValue) {
471
+ row.original[field] = finalValue;
472
+ const key = getCellKey(fr.rowId, column.id);
473
+ ctx.editedCellValues = { ...ctx.editedCellValues, [key]: finalValue };
474
+ changed.push({ columnId: column.id, field, before: oldValue, after: finalValue });
475
+ }
476
+ }
477
+ // Record the whole-row change as consecutive history steps.
478
+ if (changed.length) {
479
+ let hist = ctx.history.slice(0, ctx.historyPtr + 1);
480
+ for (const c of changed) {
481
+ hist.push({ rowId: fr.rowId, columnId: c.columnId, field: c.field, before: c.before, after: c.after });
482
+ }
483
+ if (hist.length > ctx.UNDO_LIMIT)
484
+ hist = hist.slice(hist.length - ctx.UNDO_LIMIT);
485
+ ctx.history = hist;
486
+ ctx.historyPtr = ctx.history.length - 1;
487
+ ctx.historyVersion += 1;
488
+ }
489
+ ctx.grid.store.setState((prev) => ({ ...prev }));
490
+ if (ctx.props.onCellValueChange && changed.length) {
491
+ const rowIndex = ctx.internalData.indexOf(row.original);
492
+ for (const c of changed) {
493
+ ctx.props.onCellValueChange({
494
+ rowIndex,
495
+ columnId: c.columnId,
496
+ oldValue: c.before,
497
+ newValue: c.after,
498
+ row: row.original,
499
+ });
500
+ }
501
+ }
502
+ ctx.fullRowEdit = null;
503
+ }
504
+ /** Discard the full-row edit. */
505
+ function cancelFullRowEdit() {
506
+ ctx.fullRowEdit = null;
507
+ }
508
+ return {
509
+ isCellEditable,
510
+ isCellEditableAt,
511
+ getRowColumnValue,
512
+ getCellDisplayValue,
513
+ startEditingWithChar,
514
+ startEditing,
515
+ stopEditing,
516
+ startFullRowEdit,
517
+ setFullRowDraft,
518
+ commitFullRowEdit,
519
+ cancelFullRowEdit,
520
+ saveEditingCell,
521
+ applyHistoryStep,
522
+ updateEditingCellValue,
523
+ onEditorKeyDown,
524
+ focusOnMount,
525
+ onCellDoubleClick,
526
+ pasteFromClipboard,
527
+ onGridPaste,
528
+ };
529
+ }
@@ -0,0 +1 @@
1
+ export {};