@svgrid/grid 3.0.0 → 3.0.2

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 (304) hide show
  1. package/CHANGELOG.md +116 -88
  2. package/README.md +199 -199
  3. package/dist/FlexRender.svelte +96 -96
  4. package/dist/GridFooter.svelte +190 -181
  5. package/dist/GridFooter.svelte.d.ts +8 -0
  6. package/dist/GridMenus.svelte +18 -10
  7. package/dist/GridMenus.svelte.d.ts +2 -1
  8. package/dist/SvAutoComplete.svelte +169 -169
  9. package/dist/SvAvatar.svelte +75 -75
  10. package/dist/SvCalendar.svelte +503 -503
  11. package/dist/SvCarousel.svelte +141 -141
  12. package/dist/SvCheckBox.svelte +102 -102
  13. package/dist/SvCircularProgress.svelte +109 -109
  14. package/dist/SvColorInput.svelte +181 -181
  15. package/dist/SvComboBox.svelte +279 -279
  16. package/dist/SvContextMenu.svelte +116 -116
  17. package/dist/SvCountryInput.svelte +163 -163
  18. package/dist/SvDrawer.svelte +254 -254
  19. package/dist/SvDropDownList.svelte +378 -378
  20. package/dist/SvDurationInput.svelte +126 -126
  21. package/dist/SvField.svelte +293 -293
  22. package/dist/SvForm.svelte +437 -437
  23. package/dist/SvGrid.controller.svelte.d.ts +17 -0
  24. package/dist/SvGrid.controller.svelte.js +128 -14
  25. package/dist/SvGrid.css +79 -10
  26. package/dist/SvGrid.svelte +191 -186
  27. package/dist/SvGrid.types.d.ts +42 -1
  28. package/dist/SvGridCellEditor.svelte +748 -744
  29. package/dist/SvGridChart.svelte +2246 -1716
  30. package/dist/SvGridChart.svelte.d.ts +63 -1
  31. package/dist/SvGridChartPanel.svelte +633 -485
  32. package/dist/SvGridChartView.svelte +79 -70
  33. package/dist/SvGridChartView.svelte.d.ts +7 -0
  34. package/dist/SvGridDropdown.svelte +728 -728
  35. package/dist/SvGridSelect.svelte +270 -270
  36. package/dist/SvGroupCell.svelte +141 -116
  37. package/dist/SvGroupCell.svelte.d.ts +8 -0
  38. package/dist/SvListBox.svelte +334 -334
  39. package/dist/SvMaskedInput.svelte +122 -122
  40. package/dist/SvMenu.svelte +124 -124
  41. package/dist/SvMenuList.svelte +146 -146
  42. package/dist/SvMultiSelect.svelte +293 -293
  43. package/dist/SvNumberInput.svelte +172 -172
  44. package/dist/SvOtpInput.svelte +158 -158
  45. package/dist/SvPasswordInput.svelte +151 -151
  46. package/dist/SvPhoneInput.svelte +133 -133
  47. package/dist/SvPopover.svelte +197 -197
  48. package/dist/SvProgress.svelte +116 -116
  49. package/dist/SvRadioGroup.svelte +107 -107
  50. package/dist/SvRating.svelte +112 -112
  51. package/dist/SvResult.svelte +73 -73
  52. package/dist/SvRichText.svelte +211 -211
  53. package/dist/SvRowGroupPanel.svelte +186 -170
  54. package/dist/SvRowGroupPanel.svelte.d.ts +8 -0
  55. package/dist/SvScrollArea.svelte +61 -61
  56. package/dist/SvSlider.svelte +203 -203
  57. package/dist/SvSwitchButton.svelte +108 -108
  58. package/dist/SvTagsInput.svelte +115 -115
  59. package/dist/SvTextInput.svelte +147 -147
  60. package/dist/SvTimePicker.svelte +245 -245
  61. package/dist/SvToaster.svelte +159 -159
  62. package/dist/SvToggleButton.svelte +85 -85
  63. package/dist/SvTooltip.svelte +161 -161
  64. package/dist/SvTour.svelte +208 -208
  65. package/dist/SvTree.svelte +444 -444
  66. package/dist/SvTreeSelect.svelte +239 -239
  67. package/dist/ai.d.ts +11 -2
  68. package/dist/ai.js +51 -7
  69. package/dist/cdn/GridMenus-DyFA-QxA.js +602 -0
  70. package/dist/cdn/GridMenus-M2XP9t0_.js +603 -0
  71. package/dist/cdn/{SvDateRangeInput-DflbiP7N.js → SvDateRangeInput-CaOuMs8O.js} +1 -1
  72. package/dist/cdn/{SvDateRangeInput-Bh0A0JkF.js → SvDateRangeInput-DMLKmGEc.js} +1 -1
  73. package/dist/cdn/{SvDateTimePicker-Bivn8dAP.js → SvDateTimePicker-CCbDZNZB.js} +1 -1
  74. package/dist/cdn/{SvDateTimePicker-BWwpfB_o.js → SvDateTimePicker-sonaH0oh.js} +1 -1
  75. package/dist/cdn/{SvGridCellEditor-Ba7rY3eu.js → SvGridCellEditor-BWnTi2N7.js} +2 -2
  76. package/dist/cdn/{SvGridCellEditor-AXL8cHGO.js → SvGridCellEditor-CjEJMJKc.js} +2 -2
  77. package/dist/cdn/SvGridChart-DAb9RbRs.js +1480 -0
  78. package/dist/cdn/SvGridChart-Yl-IXZFO.js +1481 -0
  79. package/dist/cdn/SvGridChartPanel-DayQpDsR.js +834 -0
  80. package/dist/cdn/SvGridChartPanel-aqf8n-vJ.js +810 -0
  81. package/dist/cdn/{SvGridChartView-eSPuJE6g.js → SvGridChartView-CxfakxR0.js} +5 -3
  82. package/dist/cdn/{SvGridChartView-SmPW10dI.js → SvGridChartView-Dm_YEx9L.js} +5 -3
  83. package/dist/cdn/{SvGridDropdown-B1ZcLpgs.js → SvGridDropdown-D0VdjeR8.js} +1 -1
  84. package/dist/cdn/{SvGridDropdown-CtdbHcIS.js → SvGridDropdown-D13MtJ2j.js} +1 -1
  85. package/dist/cdn/cell-formatting-C2Hf5gqd.js +86 -0
  86. package/dist/cdn/chart-CKJRCPOk.js +1652 -0
  87. package/dist/cdn/{date-format-DZT7T1wf.js → date-format-BNii4zeD.js} +2 -2
  88. package/dist/cdn/{date-format-CcP1tafP.js → date-format-BnnHlqGw.js} +2 -2
  89. package/dist/cdn/{export-format-Cv1Dll6k.js → export-format-CUDLy2yn.js} +84 -168
  90. package/dist/cdn/{src-B_YS5AOc.js → src-BJ2opd0j.js} +3642 -3450
  91. package/dist/cdn/{src-DYXXpuSk.js → src-DNbz94Ct.js} +8649 -8457
  92. package/dist/cdn/svgrid.js +16 -15
  93. package/dist/cdn/svgrid.svelte-external.js +16 -15
  94. package/dist/cell-formatting.d.ts +7 -0
  95. package/dist/cell-formatting.js +7 -1
  96. package/dist/chart-export.d.ts +23 -2
  97. package/dist/chart-export.js +88 -14
  98. package/dist/chart.d.ts +410 -18
  99. package/dist/chart.js +824 -24
  100. package/dist/editing.js +22 -13
  101. package/dist/grid-icons.d.ts +49 -0
  102. package/dist/grid-icons.js +86 -0
  103. package/dist/index.d.ts +3 -2
  104. package/dist/index.js +3 -2
  105. package/package.json +1 -1
  106. package/src/FlexRender.svelte +96 -96
  107. package/src/GridFooter.svelte +190 -181
  108. package/src/GridMenus.svelte +18 -10
  109. package/src/SvAutoComplete.svelte +169 -169
  110. package/src/SvAvatar.svelte +75 -75
  111. package/src/SvCalendar.svelte +503 -503
  112. package/src/SvCalendar.test.ts +226 -226
  113. package/src/SvCarousel.svelte +141 -141
  114. package/src/SvCheckBox.svelte +102 -102
  115. package/src/SvCircularProgress.svelte +109 -109
  116. package/src/SvColorInput.svelte +181 -181
  117. package/src/SvComboBox.svelte +279 -279
  118. package/src/SvContextMenu.svelte +116 -116
  119. package/src/SvCountryInput.svelte +163 -163
  120. package/src/SvDrawer.svelte +254 -254
  121. package/src/SvDropDownList.svelte +378 -378
  122. package/src/SvDurationInput.svelte +126 -126
  123. package/src/SvField.svelte +293 -293
  124. package/src/SvForm.svelte +437 -437
  125. package/src/SvForm.test.ts +411 -411
  126. package/src/SvGrid.controller.svelte.ts +146 -13
  127. package/src/SvGrid.css +79 -10
  128. package/src/SvGrid.svelte +191 -186
  129. package/src/SvGrid.types.ts +2133 -2092
  130. package/src/SvGridCellEditor.svelte +748 -744
  131. package/src/SvGridChart.svelte +2246 -1716
  132. package/src/SvGridChart.test.ts +243 -1
  133. package/src/SvGridChartPanel.svelte +633 -485
  134. package/src/SvGridChartView.svelte +79 -70
  135. package/src/SvGridDropdown.svelte +728 -728
  136. package/src/SvGridSelect.svelte +270 -270
  137. package/src/SvGroupCell.svelte +141 -116
  138. package/src/SvListBox.svelte +334 -334
  139. package/src/SvMaskedInput.svelte +122 -122
  140. package/src/SvMenu.svelte +124 -124
  141. package/src/SvMenu.test.ts +97 -97
  142. package/src/SvMenuList.svelte +146 -146
  143. package/src/SvMultiSelect.svelte +293 -293
  144. package/src/SvNumberInput.svelte +172 -172
  145. package/src/SvOtpInput.svelte +158 -158
  146. package/src/SvPasswordInput.svelte +151 -151
  147. package/src/SvPhoneInput.svelte +133 -133
  148. package/src/SvPopover.svelte +197 -197
  149. package/src/SvProgress.svelte +116 -116
  150. package/src/SvRadioGroup.svelte +107 -107
  151. package/src/SvRating.svelte +112 -112
  152. package/src/SvResult.svelte +73 -73
  153. package/src/SvRichText.svelte +211 -211
  154. package/src/SvRowGroupPanel.svelte +186 -170
  155. package/src/SvScrollArea.svelte +61 -61
  156. package/src/SvSlider.svelte +203 -203
  157. package/src/SvSwitchButton.svelte +108 -108
  158. package/src/SvTagsInput.svelte +115 -115
  159. package/src/SvTextInput.svelte +147 -147
  160. package/src/SvTimePicker.svelte +245 -245
  161. package/src/SvToaster.svelte +159 -159
  162. package/src/SvToaster.test.ts +95 -95
  163. package/src/SvToggleButton.svelte +85 -85
  164. package/src/SvTooltip.svelte +161 -161
  165. package/src/SvTour.svelte +208 -208
  166. package/src/SvTree.svelte +444 -444
  167. package/src/SvTreeSelect.svelte +239 -239
  168. package/src/a11y/dismissable.test.ts +119 -119
  169. package/src/a11y/dismissable.ts +114 -114
  170. package/src/a11y.contract.test.ts +49 -49
  171. package/src/a11y.test.ts +59 -59
  172. package/src/a11y.ts +61 -61
  173. package/src/ai.test.ts +561 -502
  174. package/src/ai.ts +1472 -1419
  175. package/src/build-api.coverage.test.ts +633 -633
  176. package/src/build-api.ts +846 -846
  177. package/src/builtin-editors.grid.test.ts +83 -83
  178. package/src/cell-formatting.ts +177 -171
  179. package/src/cell-render.test.ts +513 -513
  180. package/src/cell-render.ts +496 -496
  181. package/src/cell-values.ts +148 -148
  182. package/src/chart-export.test.ts +112 -0
  183. package/src/chart-export.ts +270 -202
  184. package/src/chart-view.svelte.ts +36 -36
  185. package/src/chart.test.ts +830 -1
  186. package/src/chart.ts +3342 -2321
  187. package/src/collaboration.test.ts +104 -104
  188. package/src/collaboration.ts +167 -167
  189. package/src/column-groups.ts +78 -78
  190. package/src/core.performance.test.ts +30 -30
  191. package/src/core.ts +1865 -1865
  192. package/src/createAutocomplete.svelte.ts +132 -132
  193. package/src/createCombobox.svelte.ts +191 -191
  194. package/src/createCountryInput.svelte.ts +157 -157
  195. package/src/createDropdownList.svelte.ts +168 -168
  196. package/src/createForm.svelte.ts +386 -386
  197. package/src/createGrid.svelte.ts +42 -42
  198. package/src/createGrid.test.ts +10 -10
  199. package/src/createGridState.svelte.ts +17 -17
  200. package/src/createListbox.svelte.ts +250 -250
  201. package/src/createMenu.svelte.ts +224 -224
  202. package/src/createPopoverSelect.svelte.ts +213 -213
  203. package/src/createSlider.svelte.ts +191 -191
  204. package/src/createTooltip.svelte.ts +144 -144
  205. package/src/createTree.svelte.ts +322 -322
  206. package/src/datetime/date-core.ts +206 -206
  207. package/src/datetime/date-restrict.ts +61 -61
  208. package/src/datetime/timezone.ts +135 -135
  209. package/src/dock-manager-model.ts +596 -596
  210. package/src/dock-model.ts +374 -374
  211. package/src/editing.test.ts +974 -974
  212. package/src/editing.ts +618 -609
  213. package/src/editor-contract.ts +171 -171
  214. package/src/editor-registry.grid.test.ts +144 -144
  215. package/src/editor-registry.ts +122 -122
  216. package/src/export-data-api.test.ts +126 -126
  217. package/src/export-format.test.ts +107 -107
  218. package/src/export-format.ts +601 -601
  219. package/src/filter-operators.ts +160 -160
  220. package/src/filtering/excel-filters.ts +325 -325
  221. package/src/flex-render.ts +3 -3
  222. package/src/form-field.ts +127 -127
  223. package/src/grid-icons.test.ts +46 -0
  224. package/src/grid-icons.ts +184 -0
  225. package/src/group-display.test.ts +167 -167
  226. package/src/group-display.ts +200 -200
  227. package/src/headless.ts +87 -87
  228. package/src/icon-overrides.grid.test.ts +188 -0
  229. package/src/icon-seam.test.ts +100 -0
  230. package/src/index.ts +19 -0
  231. package/src/js-scroller.svelte.ts +173 -173
  232. package/src/keyboard-handlers.ts +270 -270
  233. package/src/keyboard.test.ts +59 -59
  234. package/src/keyboard.ts +97 -97
  235. package/src/list-nav.test.ts +49 -49
  236. package/src/list-nav.ts +29 -29
  237. package/src/list-option.test.ts +56 -56
  238. package/src/list-option.ts +179 -179
  239. package/src/menus.ts +597 -597
  240. package/src/merge-objects.ts +48 -48
  241. package/src/overlays.test.ts +90 -90
  242. package/src/positioning.ts +268 -268
  243. package/src/render-component.ts +28 -28
  244. package/src/row-drag.test.ts +401 -401
  245. package/src/row-drag.ts +419 -419
  246. package/src/row-resize.test.ts +524 -524
  247. package/src/row-resize.ts +228 -228
  248. package/src/scheduler-ical.ts +181 -181
  249. package/src/scheduler-model.test.ts +562 -562
  250. package/src/scheduler-model.ts +873 -873
  251. package/src/selection.test.ts +885 -885
  252. package/src/server-data-source.test.ts +383 -383
  253. package/src/server-data-source.ts +469 -469
  254. package/src/sparkline.test.ts +68 -68
  255. package/src/sparkline.ts +169 -169
  256. package/src/spreadsheet.test.ts +488 -488
  257. package/src/spreadsheet.ts +312 -312
  258. package/src/static-functions.ts +11 -11
  259. package/src/subscribe.ts +38 -38
  260. package/src/summaries.ts +113 -113
  261. package/src/svgrid-wrapper.types.ts +563 -563
  262. package/src/svgrid.async-editor-options.test.ts +273 -273
  263. package/src/svgrid.auto-row-height.test.ts +204 -204
  264. package/src/svgrid.behavior.test.ts +910 -910
  265. package/src/svgrid.charting.test.ts +654 -534
  266. package/src/svgrid.comments-autocomplete.test.ts +127 -127
  267. package/src/svgrid.context-menu.test.ts +147 -147
  268. package/src/svgrid.features.test.ts +157 -157
  269. package/src/svgrid.filter-depth.test.ts +163 -163
  270. package/src/svgrid.filter-menu-listbox.svelte.test.ts +382 -377
  271. package/src/svgrid.filter-menu-scroll.test.ts +112 -112
  272. package/src/svgrid.grand-total.test.ts +188 -188
  273. package/src/svgrid.group-display-mode.test.ts +171 -171
  274. package/src/svgrid.group-footers.test.ts +121 -121
  275. package/src/svgrid.group-pagination.test.ts +153 -153
  276. package/src/svgrid.new-features.wrapper.test.ts +251 -251
  277. package/src/svgrid.tree-data.test.ts +186 -186
  278. package/src/svgrid.wrapper.test.ts +63 -63
  279. package/src/svgriddropdown.async-panel.svelte.test.ts +195 -195
  280. package/src/test-setup.ts +62 -62
  281. package/src/themes/index.ts +288 -288
  282. package/src/toast-store.svelte.ts +250 -250
  283. package/src/toast-store.test.ts +147 -147
  284. package/src/tree-row-model.test.ts +168 -168
  285. package/src/ui-buttons.test.ts +144 -144
  286. package/src/ui-inputs.test.ts +118 -118
  287. package/src/ui-localization.test.ts +113 -113
  288. package/src/ui-range.test.ts +70 -70
  289. package/src/ui-selection.test.ts +155 -155
  290. package/src/ui-tier1.test.ts +142 -142
  291. package/src/virtual.test.ts +88 -88
  292. package/src/virtualization/column-virtualizer.test.ts +27 -27
  293. package/src/virtualization/column-virtualizer.ts +30 -30
  294. package/src/virtualization/svelte-virtualizer.svelte.ts +26 -26
  295. package/src/virtualization/types.ts +30 -30
  296. package/src/virtualization/virtualizer.test.ts +47 -47
  297. package/src/virtualization/virtualizer.ts +322 -322
  298. package/dist/cdn/GridMenus-DMihUtma.js +0 -600
  299. package/dist/cdn/GridMenus-nmNDj1a3.js +0 -601
  300. package/dist/cdn/SvGridChart-Bs2GIR2Q.js +0 -1202
  301. package/dist/cdn/SvGridChart-CwhFz7GV.js +0 -1203
  302. package/dist/cdn/SvGridChartPanel-1WXuStzM.js +0 -561
  303. package/dist/cdn/SvGridChartPanel-e5_nqNP9.js +0 -585
  304. package/dist/cdn/chart-i4XcXyHJ.js +0 -1214
@@ -1,744 +1,748 @@
1
- <script lang="ts" generics="TFeatures extends TableFeatures = TableFeatures, TData extends RowData = RowData">
2
- /**
3
- * The in-cell and full-row editor UI, lifted out of SvGrid.svelte.
4
- *
5
- * Editing is off by default (`editable` / `enableInlineEditing` both default
6
- * to false) and this markup only ever renders while a cell is actually being
7
- * edited, so it has no business sitting in the base bundle. SvGrid.svelte
8
- * loads it with `import()` the moment editing is enabled - well before any
9
- * double-click can happen - so behaviour is unchanged either way.
10
- *
11
- * Keep `SvGridDropdown` and `SvDateTimePicker` DYNAMIC below. Importing
12
- * either statically would drag ~22 KB into this chunk for every text edit,
13
- * and a static import from anywhere reachable by SvGrid.svelte has silently
14
- * defeated a lazy boundary in this codebase before.
15
- */
16
- import {
17
- type CellEditorOption,
18
- type Column,
19
- type EditorContext,
20
- type Row,
21
- type RowData,
22
- type TableFeatures,
23
- } from "./index";
24
- import {
25
- dateToTimeString,
26
- getEditableInputValue,
27
- getEditorClass,
28
- getEditorInputType,
29
- getCellEditorInputType,
30
- isNumericEditorInput,
31
- timeStringToDate,
32
- toValueArray,
33
- } from "./SvGrid.helpers";
34
- import {
35
- getCellEditor,
36
- hasCellEditor,
37
- resolveEditorProps,
38
- type CellEditorContext,
39
- } from "./editor-registry";
40
- import type { SvGridController } from "./SvGrid.controller.svelte";
41
-
42
- let {
43
- ctrl,
44
- column,
45
- row,
46
- fullRow = false,
47
- }: {
48
- ctrl: SvGridController<TFeatures, TData>;
49
- column: Column<TData>;
50
- row: Row<TData>;
51
- /** Render the full-row editor for this cell instead of the single-cell one. */
52
- fullRow?: boolean;
53
- } = $props();
54
-
55
- // Heavy editors stay behind their own `import()` so a text edit never pays
56
- // for the dropdown or the date picker.
57
- let DropdownEditor = $state<typeof import("./SvGridDropdown.svelte").default | null>(null);
58
- let DateEditor = $state<typeof import("./SvDateTimePicker.svelte").default | null>(null);
59
- $effect(() => {
60
- const t = ctrl.editingCell?.editorType;
61
- if (
62
- (t === "list" || t === "chips" || t === "select" || t === "rich-select") &&
63
- !DropdownEditor
64
- )
65
- import("./SvGridDropdown.svelte").then((m) => (DropdownEditor = m.default));
66
- if ((t === "date" || t === "datetime" || t === "time") && !DateEditor)
67
- import("./SvDateTimePicker.svelte").then((m) => (DateEditor = m.default));
68
- });
69
-
70
- const grid = $derived(ctrl.grid);
71
- const fullRowEdit = $derived(ctrl.fullRowEdit);
72
- const getColumnEditorOptions = $derived(ctrl.getColumnEditorOptions);
73
- const areEditorOptionsLoading = $derived(ctrl.areEditorOptionsLoading);
74
- const saveEditingCell = $derived(ctrl.saveEditingCell);
75
- const updateEditingCellValue = $derived(ctrl.updateEditingCellValue);
76
- const onEditorKeyDown = $derived(ctrl.onEditorKeyDown);
77
- const focusOnMount = $derived(ctrl.focusOnMount);
78
- const toggleCheckboxWithKeyboard = $derived(ctrl.toggleCheckboxWithKeyboard);
79
-
80
- /**
81
- * Focus a button-based editor on mount. `focusOnMount` is for text controls -
82
- * it also places the caret - and the checkbox and rating editors, being
83
- * <button>s, had no equivalent and so never took focus at all. Their own key
84
- * handlers therefore never fired: neither could be operated from the
85
- * keyboard, and Escape never reached them, so the cell stayed stuck in edit
86
- * mode until the user clicked somewhere else.
87
- *
88
- * `enabled` lets a group of buttons (the rating stars) pick which one holds
89
- * focus, since an action cannot be applied conditionally.
90
- */
91
- function focusControlOnMount(node: HTMLElement, enabled: boolean = true) {
92
- if (!enabled) return;
93
- requestAnimationFrame(() => node.focus({ preventScroll: true }));
94
- }
95
-
96
- function fullRowKeydown(e: KeyboardEvent) {
97
- if (e.key === "Enter") {
98
- e.preventDefault();
99
- ctrl.commitFullRowEdit();
100
- ctrl.gridRootEl?.focus({ preventScroll: true });
101
- } else if (e.key === "Escape") {
102
- e.preventDefault();
103
- ctrl.cancelFullRowEdit();
104
- ctrl.gridRootEl?.focus({ preventScroll: true });
105
- }
106
- }
107
- function optionValueOf(o: unknown): string {
108
- return typeof o === "object" && o !== null && "value" in o
109
- ? String((o as { value: unknown }).value)
110
- : String(o);
111
- }
112
- function optionLabelOf(o: unknown): string {
113
- return typeof o === "object" && o !== null && "label" in o
114
- ? String((o as { label: unknown }).label)
115
- : String(o);
116
- }
117
-
118
- // Build the interaction context handed to a CUSTOM cell editor registered via
119
- // `registerCellEditor`. Maps the grid's editing lifecycle onto the uniform
120
- // change / commit / cancel contract every registered editor speaks.
121
- function buildRegisteredEditorContext(): CellEditorContext {
122
- const cell = ctrl.editingCell;
123
- return {
124
- value: cell?.value,
125
- rowId: cell?.rowId ?? "",
126
- columnId: cell?.columnId ?? "",
127
- onChange: (v: unknown) => updateEditingCellValue(v),
128
- onCommit: (v?: unknown) => {
129
- if (v !== undefined) updateEditingCellValue(v);
130
- saveEditingCell();
131
- },
132
- onCancel: () => {
133
- ctrl.editingCell = null;
134
- ctrl.gridRootEl?.focus({ preventScroll: true });
135
- },
136
- // Tab out of an editor: commit, then step to the adjacent cell. Without
137
- // this a registered editor's Tab walks focus out of the grid (#48) - the
138
- // built-in editors already route through the same helper.
139
- onCommitAndMove: (v: unknown, direction: 1 | -1) => {
140
- if (v !== undefined) updateEditingCellValue(v);
141
- ctrl.commitAndMoveByTab(direction === -1);
142
- },
143
- // Close a popover editor without committing. Same effect as cancel today;
144
- // named separately so an editor can distinguish "dismiss my panel" from
145
- // "abandon the edit" if that ever diverges.
146
- onRequestClose: () => {
147
- ctrl.editingCell = null;
148
- ctrl.gridRootEl?.focus({ preventScroll: true });
149
- },
150
- inCell: true,
151
- };
152
- }
153
- </script>
154
-
155
- {#snippet chipsEditor(
156
- opts: CellEditorOption[],
157
- multi: boolean,
158
- selectedArr: Array<string | number>,
159
- optsLoading: boolean,
160
- )}
161
- {#if opts.length > 0}
162
- <!-- Options-driven chips editor: defer to the custom dropdown,
163
- which renders the selected values as chips in its trigger and
164
- pops out a styled listbox. Identical UX to the list editor
165
- with renderChipsInTrigger flipped on. -->
166
- {#if DropdownEditor}
167
- <DropdownEditor
168
- options={opts}
169
- loading={optsLoading}
170
- value={ctrl.editingCell?.value}
171
- multiple={multi}
172
- placeholder="Pick…"
173
- renderChipsInTrigger={true}
174
- onChange={(next) => {
175
- ctrl.editingCell = ctrl.editingCell
176
- ? { ...ctrl.editingCell, value: next }
177
- : ctrl.editingCell;
178
- }}
179
- onCommit={() => saveEditingCell()}
180
- onCancel={() => {
181
- ctrl.editingCell = null;
182
- ctrl.gridRootEl?.focus({ preventScroll: true });
183
- }}
184
- />
185
- {/if}
186
- {:else}
187
- <!-- Free-form chips: typed tags. Enter / comma commits a chip,
188
- Backspace on empty input removes the last chip, blur saves. -->
189
- <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
190
- <div
191
- class="sv-grid-cell-editor sv-grid-cell-editor-chips"
192
- role="group"
193
- tabindex={-1}
194
- >
195
- <div class="sv-grid-chips-row">
196
- {#each selectedArr as v, idx (String(v) + "_" + idx)}
197
- <span class="sv-grid-chip sv-grid-chip-removable">
198
- {String(v)}
199
- <button
200
- type="button"
201
- class="sv-grid-chip-remove"
202
- aria-label="Remove {String(v)}"
203
- onmousedown={(event) => event.preventDefault()}
204
- onclick={() => {
205
- const next = selectedArr.filter((_, i) => i !== idx);
206
- ctrl.editingCell = ctrl.editingCell
207
- ? {
208
- ...ctrl.editingCell,
209
- value: multi ? next : (next[0] ?? null),
210
- }
211
- : ctrl.editingCell;
212
- }}>×</button
213
- >
214
- </span>
215
- {/each}
216
- <input
217
- use:focusOnMount
218
- class="sv-grid-chip-input"
219
- type="text"
220
- placeholder={multi ? "Type, Enter to add" : "Type a value"}
221
- onkeydown={(event) => {
222
- if (event.key === "Enter" || (multi && event.key === ",")) {
223
- event.preventDefault();
224
- event.stopPropagation();
225
- const input = event.currentTarget as HTMLInputElement;
226
- const raw = input.value.trim();
227
- if (raw) {
228
- const next = multi ? [...selectedArr, raw] : [raw];
229
- ctrl.editingCell = ctrl.editingCell
230
- ? { ...ctrl.editingCell, value: multi ? next : raw }
231
- : ctrl.editingCell;
232
- input.value = "";
233
- if (!multi) saveEditingCell();
234
- }
235
- } else if (event.key === "Escape") {
236
- onEditorKeyDown(event);
237
- } else if (event.key === "Backspace") {
238
- const input = event.currentTarget as HTMLInputElement;
239
- if (input.value === "" && selectedArr.length > 0) {
240
- event.preventDefault();
241
- const next = selectedArr.slice(0, -1);
242
- ctrl.editingCell = ctrl.editingCell
243
- ? {
244
- ...ctrl.editingCell,
245
- value: multi ? next : (next[0] ?? null),
246
- }
247
- : ctrl.editingCell;
248
- }
249
- }
250
- }}
251
- onblur={() => saveEditingCell()}
252
- />
253
- {#if multi}
254
- <button
255
- type="button"
256
- class="sv-grid-chip-commit"
257
- onmousedown={(event) => event.preventDefault()}
258
- onclick={() => saveEditingCell()}
259
- aria-label="Commit chip selection">Done</button
260
- >
261
- {/if}
262
- </div>
263
- </div>
264
- {/if}
265
- {/snippet}
266
-
267
- {#if fullRow}
268
- {@const et = column.columnDef.editorType ?? "text"}
269
- {@const val = fullRowEdit?.draft[column.id]}
270
- {#if et === "checkbox"}
271
- <input
272
- type="checkbox"
273
- class="sv-grid-fr-editor sv-grid-fr-checkbox"
274
- checked={Boolean(val)}
275
- onchange={(e) =>
276
- ctrl.setFullRowDraft(column.id, e.currentTarget.checked)}
277
- onkeydown={fullRowKeydown}
278
- onpointerdown={(e) => e.stopPropagation()}
279
- onclick={(e) => e.stopPropagation()}
280
- />
281
- {:else if et === "list" || et === "select" || et === "rich-select"}
282
- {@const opts = getColumnEditorOptions(column, row)}
283
- <select
284
- class="sv-grid-cell-editor sv-grid-fr-editor"
285
- value={String(val ?? "")}
286
- onchange={(e) => ctrl.setFullRowDraft(column.id, e.currentTarget.value)}
287
- onkeydown={fullRowKeydown}
288
- onpointerdown={(e) => e.stopPropagation()}
289
- onclick={(e) => e.stopPropagation()}
290
- >
291
- {#each opts as o (optionValueOf(o))}
292
- <option value={optionValueOf(o)}>{optionLabelOf(o)}</option>
293
- {/each}
294
- </select>
295
- {:else}
296
- {@const inputType =
297
- et === "number"
298
- ? "number"
299
- : et === "date"
300
- ? "date"
301
- : et === "datetime"
302
- ? "datetime-local"
303
- : et === "time"
304
- ? "time"
305
- : et === "password"
306
- ? "password"
307
- : "text"}
308
- <input
309
- type={inputType}
310
- class="sv-grid-cell-editor sv-grid-fr-editor"
311
- value={String(val ?? "")}
312
- oninput={(e) => ctrl.setFullRowDraft(column.id, e.currentTarget.value)}
313
- onkeydown={fullRowKeydown}
314
- onpointerdown={(e) => e.stopPropagation()}
315
- onclick={(e) => e.stopPropagation()}
316
- />
317
- {/if}
318
- {:else}
319
- {#if column.columnDef.cellEditor}
320
- <!-- Custom editor slot. The columnDef provides a snippet that
321
- receives the editor context (value + commit + cancel) so the
322
- consumer fully owns the in-cell UI. -->
323
- {@const customEditor = column.columnDef
324
- .cellEditor as unknown as import("svelte").Snippet<
325
- [EditorContext<TData>]
326
- >}
327
- {@render customEditor({
328
- cell: row.getAllCells().find((c) => c.column.id === column.id)!,
329
- row,
330
- column,
331
- table: grid,
332
- getValue: () => ctrl.editingCell?.value,
333
- value: ctrl.editingCell?.value,
334
- update: (next: unknown) => {
335
- // Stage the draft without closing. Live-preview controls
336
- // (sliders, color pickers) call this on every input tick.
337
- ctrl.editingCell = ctrl.editingCell
338
- ? { ...ctrl.editingCell, value: next }
339
- : ctrl.editingCell;
340
- },
341
- commit: (next?: unknown) => {
342
- // Write + close. If the caller passed a value, stage it
343
- // first; otherwise save whatever update() last wrote.
344
- if (next !== undefined) {
345
- ctrl.editingCell = ctrl.editingCell
346
- ? { ...ctrl.editingCell, value: next }
347
- : ctrl.editingCell;
348
- }
349
- saveEditingCell();
350
- },
351
- cancel: () => {
352
- ctrl.editingCell = null;
353
- ctrl.gridRootEl?.focus({ preventScroll: true });
354
- },
355
- })}
356
- {:else if ctrl.editingCell?.editorType === "checkbox"}
357
- <button
358
- use:focusControlOnMount
359
- type="button"
360
- class="sv-grid-checkbox"
361
- role="checkbox"
362
- aria-checked={Boolean(ctrl.editingCell.value)}
363
- aria-label="Edit checkbox value"
364
- onclick={(event) => {
365
- event.stopPropagation();
366
- const nextValue = !ctrl.editingCell?.value;
367
- ctrl.editingCell = ctrl.editingCell
368
- ? { ...ctrl.editingCell, value: nextValue }
369
- : ctrl.editingCell;
370
- saveEditingCell();
371
- }}
372
- onkeydown={(event) => {
373
- // Enter / Space are the checkbox's own activation keys. Everything
374
- // else - Escape to cancel, Tab to commit and move on - goes to the
375
- // shared editor handler, which this editor used to bypass entirely.
376
- if (event.key === "Enter" || event.key === " ") {
377
- toggleCheckboxWithKeyboard(event, () => {
378
- event.stopPropagation();
379
- const nextValue = !ctrl.editingCell?.value;
380
- ctrl.editingCell = ctrl.editingCell
381
- ? { ...ctrl.editingCell, value: nextValue }
382
- : ctrl.editingCell;
383
- saveEditingCell();
384
- });
385
- return;
386
- }
387
- onEditorKeyDown(event);
388
- }}
389
- onblur={() => saveEditingCell()}
390
- ></button>
391
- {:else if ctrl.editingCell?.editorType === "list"}
392
- {@const opts = getColumnEditorOptions(column, row)}
393
- {@const optsLoading = areEditorOptionsLoading(column, row)}
394
- {@const multi = column.columnDef.editorMultiple === true}
395
- {#if DropdownEditor}
396
- <DropdownEditor
397
- options={opts}
398
- loading={optsLoading}
399
- value={ctrl.editingCell?.value}
400
- multiple={multi}
401
- placeholder="Select…"
402
- onChange={(next) => {
403
- ctrl.editingCell = ctrl.editingCell
404
- ? { ...ctrl.editingCell, value: next }
405
- : ctrl.editingCell;
406
- }}
407
- onCommit={() => saveEditingCell()}
408
- onCancel={() => {
409
- ctrl.editingCell = null;
410
- ctrl.gridRootEl?.focus({ preventScroll: true });
411
- }}
412
- />
413
- {/if}
414
- {:else if ctrl.editingCell?.editorType === "chips"}
415
- {@const opts = getColumnEditorOptions(column, row)}
416
- {@const optsLoading = areEditorOptionsLoading(column, row)}
417
- {@const multi = column.columnDef.editorMultiple === true}
418
- {@const selectedArr = toValueArray(ctrl.editingCell?.value)}
419
- {@render chipsEditor(opts, multi, selectedArr, optsLoading)}
420
- {:else if ctrl.editingCell?.editorType === "rating"}
421
- {@const ratingVal = Math.max(
422
- 0,
423
- Math.min(5, Math.round(Number(ctrl.editingCell?.value) || 0)),
424
- )}
425
- <!-- Focus the star for the current rating (the first one when unrated),
426
- the radio a radiogroup is expected to enter on. Without it nothing
427
- in this editor held focus, so `onEditorKeyDown` below never ran and
428
- Escape could not close the cell. -->
429
- {@const focusStar = Math.min(5, Math.max(1, ratingVal))}
430
- <span class="sv-grid-rating-editor" role="radiogroup" aria-label="Rating">
431
- {#each [1, 2, 3, 4, 5] as n (n)}
432
- <button
433
- use:focusControlOnMount={n === focusStar}
434
- type="button"
435
- role="radio"
436
- aria-checked={ratingVal >= n}
437
- aria-label={`${n} ${n === 1 ? "star" : "stars"}`}
438
- class={`sv-grid-rating-star ${ratingVal >= n ? "sv-grid-rating-star-on" : ""}`}
439
- onmousedown={(event) => event.preventDefault()}
440
- onclick={(event) => {
441
- event.stopPropagation();
442
- ctrl.editingCell = ctrl.editingCell
443
- ? { ...ctrl.editingCell, value: n }
444
- : ctrl.editingCell;
445
- saveEditingCell();
446
- }}
447
- onkeydown={(event) => {
448
- // Arrows move between the stars, the way a radiogroup is
449
- // expected to work - without them the editor takes focus (see
450
- // focusControlOnMount) but Space can only re-pick the star that
451
- // already holds the value. Enter / Escape / Tab still fall
452
- // through to the shared handler.
453
- const key = event.key;
454
- if (
455
- key !== "ArrowLeft" &&
456
- key !== "ArrowRight" &&
457
- key !== "Home" &&
458
- key !== "End"
459
- ) {
460
- onEditorKeyDown(event);
461
- return;
462
- }
463
- event.preventDefault();
464
- event.stopPropagation();
465
- const current = Math.min(5, Math.max(1, ratingVal));
466
- const next =
467
- key === "Home"
468
- ? 1
469
- : key === "End"
470
- ? 5
471
- : key === "ArrowLeft"
472
- ? Math.max(1, current - 1)
473
- : Math.min(5, current + 1);
474
- ctrl.editingCell = ctrl.editingCell
475
- ? { ...ctrl.editingCell, value: next }
476
- : ctrl.editingCell;
477
- const stars = (
478
- event.currentTarget as HTMLElement
479
- ).parentElement?.querySelectorAll<HTMLElement>(
480
- ".sv-grid-rating-star",
481
- );
482
- stars?.[next - 1]?.focus({ preventScroll: true });
483
- }}>★</button
484
- >
485
- {/each}
486
- <button
487
- type="button"
488
- aria-label="Clear rating"
489
- class="sv-grid-rating-clear"
490
- onmousedown={(event) => event.preventDefault()}
491
- onclick={(event) => {
492
- event.stopPropagation();
493
- ctrl.editingCell = ctrl.editingCell
494
- ? { ...ctrl.editingCell, value: 0 }
495
- : ctrl.editingCell;
496
- saveEditingCell();
497
- }}>×</button
498
- >
499
- </span>
500
- {:else if ctrl.editingCell?.editorType === "select"}
501
- <!-- Custom dropdown: opens a themed popover identical in feel to
502
- the existing 'list' editor (single-select, no typeahead). -->
503
- {@const selectOpts = getColumnEditorOptions(column, row)}
504
- {@const optsLoading = areEditorOptionsLoading(column, row)}
505
- {#if DropdownEditor}
506
- <DropdownEditor
507
- options={selectOpts}
508
- loading={optsLoading}
509
- value={ctrl.editingCell?.value}
510
- multiple={false}
511
- placeholder="Select…"
512
- onChange={(next) => {
513
- ctrl.editingCell = ctrl.editingCell
514
- ? { ...ctrl.editingCell, value: next }
515
- : ctrl.editingCell;
516
- }}
517
- onCommit={() => saveEditingCell()}
518
- onCancel={() => {
519
- ctrl.editingCell = null;
520
- ctrl.gridRootEl?.focus({ preventScroll: true });
521
- }}
522
- />
523
- {/if}
524
- {:else if ctrl.editingCell?.editorType === "rich-select"}
525
- <!-- Searchable combobox: same popover as 'select' with a
526
- typeahead filter input baked in at the top. -->
527
- {@const richOpts = getColumnEditorOptions(column, row)}
528
- {@const optsLoading = areEditorOptionsLoading(column, row)}
529
- {#if DropdownEditor}
530
- <DropdownEditor
531
- options={richOpts}
532
- loading={optsLoading}
533
- value={ctrl.editingCell?.value}
534
- multiple={false}
535
- searchable={true}
536
- placeholder="Search…"
537
- onChange={(next) => {
538
- ctrl.editingCell = ctrl.editingCell
539
- ? { ...ctrl.editingCell, value: next }
540
- : ctrl.editingCell;
541
- }}
542
- onCommit={() => saveEditingCell()}
543
- onCancel={() => {
544
- ctrl.editingCell = null;
545
- ctrl.gridRootEl?.focus({ preventScroll: true });
546
- }}
547
- />
548
- {/if}
549
- {:else if ctrl.editingCell?.editorType === "textarea"}
550
- <!-- Multi-line editor. Commits on Tab, Ctrl/Cmd+Enter, or blur.
551
- Plain Enter inserts a newline (the whole point of textarea).
552
- Esc cancels. -->
553
- <textarea
554
- use:focusOnMount
555
- class="sv-grid-cell-editor sv-grid-cell-editor-textarea"
556
- rows="4"
557
- value={String(ctrl.editingCell?.value ?? "")}
558
- onpointerdown={(event) => event.stopPropagation()}
559
- oninput={(event) =>
560
- updateEditingCellValue(
561
- (event.currentTarget as HTMLTextAreaElement).value,
562
- )}
563
- onkeydown={(event) => {
564
- event.stopPropagation();
565
- if (event.key === "Escape") {
566
- event.preventDefault();
567
- ctrl.editingCell = null;
568
- ctrl.gridRootEl?.focus({ preventScroll: true });
569
- return;
570
- }
571
- // Tab and Ctrl/Cmd+Enter both commit. Plain Enter inserts a newline.
572
- // Tab goes through the shared helper so it advances the active cell
573
- // like every other editor does (#48).
574
- if (event.key === "Tab") {
575
- event.preventDefault();
576
- ctrl.commitAndMoveByTab(event.shiftKey);
577
- return;
578
- }
579
- if (event.key === "Enter" && (event.ctrlKey || event.metaKey)) {
580
- event.preventDefault();
581
- saveEditingCell();
582
- ctrl.gridRootEl?.focus({ preventScroll: true });
583
- }
584
- }}
585
- onblur={() => saveEditingCell()}
586
- ></textarea>
587
- {:else if ctrl.editingCell?.editorType === "autocomplete"}
588
- <!-- Free-text autocomplete: a text input with a live-filtered
589
- suggestion list. Typing edits the value freely; clicking a
590
- suggestion (or blur) commits. Accepts values not in the list. -->
591
- {@const acOpts = getColumnEditorOptions(column, row)}
592
- {@const acText = String(ctrl.editingCell?.value ?? "")}
593
- {@const acFiltered = acText.trim()
594
- ? acOpts.filter((o) =>
595
- String(o.label).toLowerCase().includes(acText.toLowerCase()),
596
- )
597
- : acOpts}
598
- <div class="sv-grid-autocomplete">
599
- <input
600
- use:focusOnMount
601
- class="sv-grid-cell-editor sv-grid-cell-editor-autocomplete"
602
- type="text"
603
- value={acText}
604
- onpointerdown={(event) => event.stopPropagation()}
605
- oninput={(event) =>
606
- updateEditingCellValue(
607
- (event.currentTarget as HTMLInputElement).value,
608
- )}
609
- onblur={() => saveEditingCell()}
610
- onkeydown={onEditorKeyDown}
611
- />
612
- {#if acFiltered.length > 0}
613
- <div class="sv-grid-autocomplete-list" role="listbox">
614
- {#each acFiltered.slice(0, 50) as opt (opt.value)}
615
- <button
616
- type="button"
617
- class="sv-grid-autocomplete-option"
618
- role="option"
619
- aria-selected={String(opt.value) === acText}
620
- onmousedown={(event) => {
621
- event.preventDefault();
622
- ctrl.editingCell = ctrl.editingCell
623
- ? { ...ctrl.editingCell, value: opt.value }
624
- : ctrl.editingCell;
625
- saveEditingCell();
626
- }}>{opt.label}</button
627
- >
628
- {/each}
629
- </div>
630
- {/if}
631
- </div>
632
- {:else if ctrl.editingCell?.editorType === "date"}
633
- <!-- Rich date editor: SvCalendar popover over a formatted input. Opts
634
- out to the native <input type="date"> via editorType 'date-native'. -->
635
- {#if DateEditor}
636
- <DateEditor
637
- value={ctrl.editingCell?.value as string | number | Date | null}
638
- formatString="yyyy-MM-dd"
639
- dropDownDisplayMode="calendar"
640
- autoOpen
641
- block
642
- onChange={(d) => updateEditingCellValue(d)}
643
- onCommit={() => saveEditingCell()}
644
- onCancel={() => {
645
- ctrl.editingCell = null;
646
- ctrl.gridRootEl?.focus({ preventScroll: true });
647
- }}
648
- />
649
- {/if}
650
- {:else if ctrl.editingCell?.editorType === "datetime"}
651
- {#if DateEditor}
652
- <DateEditor
653
- value={ctrl.editingCell?.value as string | number | Date | null}
654
- formatString="yyyy-MM-dd HH:mm"
655
- dropDownDisplayMode="both"
656
- autoOpen
657
- block
658
- onChange={(d) => updateEditingCellValue(d)}
659
- onCommit={() => saveEditingCell()}
660
- onCancel={() => {
661
- ctrl.editingCell = null;
662
- ctrl.gridRootEl?.focus({ preventScroll: true });
663
- }}
664
- />
665
- {/if}
666
- {:else if ctrl.editingCell?.editorType === "time"}
667
- <!-- Rich time editor: SvTimePicker dial. Stores the 'HH:MM' string the
668
- `time` parser expects; opts out via editorType 'time-native'. -->
669
- {#if DateEditor}
670
- <DateEditor
671
- value={timeStringToDate(ctrl.editingCell?.value)}
672
- formatString="HH:mm"
673
- dropDownDisplayMode="time"
674
- autoOpen
675
- block
676
- onChange={(d) => updateEditingCellValue(dateToTimeString(d))}
677
- onCommit={() => saveEditingCell()}
678
- onCancel={() => {
679
- ctrl.editingCell = null;
680
- ctrl.gridRootEl?.focus({ preventScroll: true });
681
- }}
682
- />
683
- {/if}
684
- {:else if ctrl.editingCell?.editorType === "color"}
685
- <!-- Native <input type="color"> opens its picker in a separate OS
686
- overlay; once the picker closes, focus stays on the input so
687
- `blur` never fires on its own. Commit on `change` (which fires
688
- exactly once when the picker is dismissed) so the chosen color
689
- is saved without needing the user to click elsewhere. -->
690
- <input
691
- use:focusOnMount
692
- class={getEditorClass("color")}
693
- type="color"
694
- value={getEditableInputValue("color", ctrl.editingCell?.value)}
695
- oninput={(event) =>
696
- updateEditingCellValue(
697
- (event.currentTarget as HTMLInputElement).value,
698
- )}
699
- onchange={(event) => {
700
- updateEditingCellValue(
701
- (event.currentTarget as HTMLInputElement).value,
702
- );
703
- saveEditingCell();
704
- }}
705
- onblur={() => saveEditingCell()}
706
- onkeydown={onEditorKeyDown}
707
- />
708
- {:else if hasCellEditor(String(ctrl.editingCell?.editorType ?? ""))}
709
- <!-- Custom editor registered via `registerCellEditor(type, Component)`:
710
- mount it and hand it the uniform change / commit / cancel context. -->
711
- {@const _reg = getCellEditor(String(ctrl.editingCell?.editorType))!}
712
- {@const CustomCellEditor = _reg.component}
713
- <CustomCellEditor {...resolveEditorProps(_reg, buildRegisteredEditorContext())} />
714
- {:else}
715
- {@const editorType = ctrl.editingCell?.editorType ?? "text"}
716
- {@const isNumberEditor = getCellEditorInputType(editorType) === "text" &&
717
- getEditorInputType(editorType) === "number"}
718
- <input
719
- use:focusOnMount
720
- class={getEditorClass(editorType)}
721
- type={getCellEditorInputType(editorType)}
722
- inputmode={isNumberEditor ? "decimal" : undefined}
723
- value={getEditableInputValue(
724
- editorType,
725
- ctrl.editingCell?.value,
726
- )}
727
- oninput={(event) => {
728
- const input = event.currentTarget as HTMLInputElement;
729
- // The number editor is a TEXT input so intermediate values survive
730
- // (see getCellEditorInputType). That means nothing stops a letter
731
- // being typed, which type="number" used to - so reject it here and
732
- // put the last good draft back, rather than letting "12x" reach the
733
- // commit and coerce to null.
734
- if (isNumberEditor && !isNumericEditorInput(input.value)) {
735
- input.value = String(ctrl.editingCell?.value ?? "");
736
- return;
737
- }
738
- updateEditingCellValue(input.value);
739
- }}
740
- onblur={() => saveEditingCell()}
741
- onkeydown={onEditorKeyDown}
742
- />
743
- {/if}
744
- {/if}
1
+ <script lang="ts" generics="TFeatures extends TableFeatures = TableFeatures, TData extends RowData = RowData">
2
+ /**
3
+ * The in-cell and full-row editor UI, lifted out of SvGrid.svelte.
4
+ *
5
+ * Editing is off by default (`editable` / `enableInlineEditing` both default
6
+ * to false) and this markup only ever renders while a cell is actually being
7
+ * edited, so it has no business sitting in the base bundle. SvGrid.svelte
8
+ * loads it with `import()` the moment editing is enabled - well before any
9
+ * double-click can happen - so behaviour is unchanged either way.
10
+ *
11
+ * Keep `SvGridDropdown` and `SvDateTimePicker` DYNAMIC below. Importing
12
+ * either statically would drag ~22 KB into this chunk for every text edit,
13
+ * and a static import from anywhere reachable by SvGrid.svelte has silently
14
+ * defeated a lazy boundary in this codebase before.
15
+ */
16
+ import {
17
+ type CellEditorOption,
18
+ type Column,
19
+ type EditorContext,
20
+ type Row,
21
+ type RowData,
22
+ type TableFeatures,
23
+ } from "./index";
24
+ import {
25
+ dateToTimeString,
26
+ getEditableInputValue,
27
+ getEditorClass,
28
+ getEditorInputType,
29
+ getCellEditorInputType,
30
+ isNumericEditorInput,
31
+ timeStringToDate,
32
+ toValueArray,
33
+ } from "./SvGrid.helpers";
34
+ import {
35
+ getCellEditor,
36
+ hasCellEditor,
37
+ resolveEditorProps,
38
+ type CellEditorContext,
39
+ } from "./editor-registry";
40
+ import type { SvGridController } from "./SvGrid.controller.svelte";
41
+
42
+ let {
43
+ ctrl,
44
+ column,
45
+ row,
46
+ fullRow = false,
47
+ }: {
48
+ ctrl: SvGridController<TFeatures, TData>;
49
+ column: Column<TData>;
50
+ row: Row<TData>;
51
+ /** Render the full-row editor for this cell instead of the single-cell one. */
52
+ fullRow?: boolean;
53
+ } = $props();
54
+
55
+ // Heavy editors stay behind their own `import()` so a text edit never pays
56
+ // for the dropdown or the date picker.
57
+ let DropdownEditor = $state<typeof import("./SvGridDropdown.svelte").default | null>(null);
58
+ let DateEditor = $state<typeof import("./SvDateTimePicker.svelte").default | null>(null);
59
+ $effect(() => {
60
+ const t = ctrl.editingCell?.editorType;
61
+ if (
62
+ (t === "list" || t === "chips" || t === "select" || t === "rich-select") &&
63
+ !DropdownEditor
64
+ )
65
+ import("./SvGridDropdown.svelte").then((m) => (DropdownEditor = m.default));
66
+ if ((t === "date" || t === "datetime" || t === "time") && !DateEditor)
67
+ import("./SvDateTimePicker.svelte").then((m) => (DateEditor = m.default));
68
+ });
69
+
70
+ const grid = $derived(ctrl.grid);
71
+ const fullRowEdit = $derived(ctrl.fullRowEdit);
72
+ const getColumnEditorOptions = $derived(ctrl.getColumnEditorOptions);
73
+ const areEditorOptionsLoading = $derived(ctrl.areEditorOptionsLoading);
74
+ const saveEditingCell = $derived(ctrl.saveEditingCell);
75
+ const updateEditingCellValue = $derived(ctrl.updateEditingCellValue);
76
+ const onEditorKeyDown = $derived(ctrl.onEditorKeyDown);
77
+ const focusOnMount = $derived(ctrl.focusOnMount);
78
+ const toggleCheckboxWithKeyboard = $derived(ctrl.toggleCheckboxWithKeyboard);
79
+
80
+ /**
81
+ * Focus a button-based editor on mount. `focusOnMount` is for text controls -
82
+ * it also places the caret - and the checkbox and rating editors, being
83
+ * <button>s, had no equivalent and so never took focus at all. Their own key
84
+ * handlers therefore never fired: neither could be operated from the
85
+ * keyboard, and Escape never reached them, so the cell stayed stuck in edit
86
+ * mode until the user clicked somewhere else.
87
+ *
88
+ * `enabled` lets a group of buttons (the rating stars) pick which one holds
89
+ * focus, since an action cannot be applied conditionally.
90
+ *
91
+ * Synchronous, for the same reason as `focusOnMount`: a deferred frame left
92
+ * the cell marked as editing with nothing inside it focused, and keystrokes
93
+ * in that window were dropped by both the grid root and the editor.
94
+ */
95
+ function focusControlOnMount(node: HTMLElement, enabled: boolean = true) {
96
+ if (!enabled) return;
97
+ node.focus({ preventScroll: true });
98
+ }
99
+
100
+ function fullRowKeydown(e: KeyboardEvent) {
101
+ if (e.key === "Enter") {
102
+ e.preventDefault();
103
+ ctrl.commitFullRowEdit();
104
+ ctrl.gridRootEl?.focus({ preventScroll: true });
105
+ } else if (e.key === "Escape") {
106
+ e.preventDefault();
107
+ ctrl.cancelFullRowEdit();
108
+ ctrl.gridRootEl?.focus({ preventScroll: true });
109
+ }
110
+ }
111
+ function optionValueOf(o: unknown): string {
112
+ return typeof o === "object" && o !== null && "value" in o
113
+ ? String((o as { value: unknown }).value)
114
+ : String(o);
115
+ }
116
+ function optionLabelOf(o: unknown): string {
117
+ return typeof o === "object" && o !== null && "label" in o
118
+ ? String((o as { label: unknown }).label)
119
+ : String(o);
120
+ }
121
+
122
+ // Build the interaction context handed to a CUSTOM cell editor registered via
123
+ // `registerCellEditor`. Maps the grid's editing lifecycle onto the uniform
124
+ // change / commit / cancel contract every registered editor speaks.
125
+ function buildRegisteredEditorContext(): CellEditorContext {
126
+ const cell = ctrl.editingCell;
127
+ return {
128
+ value: cell?.value,
129
+ rowId: cell?.rowId ?? "",
130
+ columnId: cell?.columnId ?? "",
131
+ onChange: (v: unknown) => updateEditingCellValue(v),
132
+ onCommit: (v?: unknown) => {
133
+ if (v !== undefined) updateEditingCellValue(v);
134
+ saveEditingCell();
135
+ },
136
+ onCancel: () => {
137
+ ctrl.editingCell = null;
138
+ ctrl.gridRootEl?.focus({ preventScroll: true });
139
+ },
140
+ // Tab out of an editor: commit, then step to the adjacent cell. Without
141
+ // this a registered editor's Tab walks focus out of the grid (#48) - the
142
+ // built-in editors already route through the same helper.
143
+ onCommitAndMove: (v: unknown, direction: 1 | -1) => {
144
+ if (v !== undefined) updateEditingCellValue(v);
145
+ ctrl.commitAndMoveByTab(direction === -1);
146
+ },
147
+ // Close a popover editor without committing. Same effect as cancel today;
148
+ // named separately so an editor can distinguish "dismiss my panel" from
149
+ // "abandon the edit" if that ever diverges.
150
+ onRequestClose: () => {
151
+ ctrl.editingCell = null;
152
+ ctrl.gridRootEl?.focus({ preventScroll: true });
153
+ },
154
+ inCell: true,
155
+ };
156
+ }
157
+ </script>
158
+
159
+ {#snippet chipsEditor(
160
+ opts: CellEditorOption[],
161
+ multi: boolean,
162
+ selectedArr: Array<string | number>,
163
+ optsLoading: boolean,
164
+ )}
165
+ {#if opts.length > 0}
166
+ <!-- Options-driven chips editor: defer to the custom dropdown,
167
+ which renders the selected values as chips in its trigger and
168
+ pops out a styled listbox. Identical UX to the list editor
169
+ with renderChipsInTrigger flipped on. -->
170
+ {#if DropdownEditor}
171
+ <DropdownEditor
172
+ options={opts}
173
+ loading={optsLoading}
174
+ value={ctrl.editingCell?.value}
175
+ multiple={multi}
176
+ placeholder="Pick…"
177
+ renderChipsInTrigger={true}
178
+ onChange={(next) => {
179
+ ctrl.editingCell = ctrl.editingCell
180
+ ? { ...ctrl.editingCell, value: next }
181
+ : ctrl.editingCell;
182
+ }}
183
+ onCommit={() => saveEditingCell()}
184
+ onCancel={() => {
185
+ ctrl.editingCell = null;
186
+ ctrl.gridRootEl?.focus({ preventScroll: true });
187
+ }}
188
+ />
189
+ {/if}
190
+ {:else}
191
+ <!-- Free-form chips: typed tags. Enter / comma commits a chip,
192
+ Backspace on empty input removes the last chip, blur saves. -->
193
+ <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
194
+ <div
195
+ class="sv-grid-cell-editor sv-grid-cell-editor-chips"
196
+ role="group"
197
+ tabindex={-1}
198
+ >
199
+ <div class="sv-grid-chips-row">
200
+ {#each selectedArr as v, idx (String(v) + "_" + idx)}
201
+ <span class="sv-grid-chip sv-grid-chip-removable">
202
+ {String(v)}
203
+ <button
204
+ type="button"
205
+ class="sv-grid-chip-remove"
206
+ aria-label="Remove {String(v)}"
207
+ onmousedown={(event) => event.preventDefault()}
208
+ onclick={() => {
209
+ const next = selectedArr.filter((_, i) => i !== idx);
210
+ ctrl.editingCell = ctrl.editingCell
211
+ ? {
212
+ ...ctrl.editingCell,
213
+ value: multi ? next : (next[0] ?? null),
214
+ }
215
+ : ctrl.editingCell;
216
+ }}>×</button
217
+ >
218
+ </span>
219
+ {/each}
220
+ <input
221
+ use:focusOnMount
222
+ class="sv-grid-chip-input"
223
+ type="text"
224
+ placeholder={multi ? "Type, Enter to add" : "Type a value"}
225
+ onkeydown={(event) => {
226
+ if (event.key === "Enter" || (multi && event.key === ",")) {
227
+ event.preventDefault();
228
+ event.stopPropagation();
229
+ const input = event.currentTarget as HTMLInputElement;
230
+ const raw = input.value.trim();
231
+ if (raw) {
232
+ const next = multi ? [...selectedArr, raw] : [raw];
233
+ ctrl.editingCell = ctrl.editingCell
234
+ ? { ...ctrl.editingCell, value: multi ? next : raw }
235
+ : ctrl.editingCell;
236
+ input.value = "";
237
+ if (!multi) saveEditingCell();
238
+ }
239
+ } else if (event.key === "Escape") {
240
+ onEditorKeyDown(event);
241
+ } else if (event.key === "Backspace") {
242
+ const input = event.currentTarget as HTMLInputElement;
243
+ if (input.value === "" && selectedArr.length > 0) {
244
+ event.preventDefault();
245
+ const next = selectedArr.slice(0, -1);
246
+ ctrl.editingCell = ctrl.editingCell
247
+ ? {
248
+ ...ctrl.editingCell,
249
+ value: multi ? next : (next[0] ?? null),
250
+ }
251
+ : ctrl.editingCell;
252
+ }
253
+ }
254
+ }}
255
+ onblur={() => saveEditingCell()}
256
+ />
257
+ {#if multi}
258
+ <button
259
+ type="button"
260
+ class="sv-grid-chip-commit"
261
+ onmousedown={(event) => event.preventDefault()}
262
+ onclick={() => saveEditingCell()}
263
+ aria-label="Commit chip selection">Done</button
264
+ >
265
+ {/if}
266
+ </div>
267
+ </div>
268
+ {/if}
269
+ {/snippet}
270
+
271
+ {#if fullRow}
272
+ {@const et = column.columnDef.editorType ?? "text"}
273
+ {@const val = fullRowEdit?.draft[column.id]}
274
+ {#if et === "checkbox"}
275
+ <input
276
+ type="checkbox"
277
+ class="sv-grid-fr-editor sv-grid-fr-checkbox"
278
+ checked={Boolean(val)}
279
+ onchange={(e) =>
280
+ ctrl.setFullRowDraft(column.id, e.currentTarget.checked)}
281
+ onkeydown={fullRowKeydown}
282
+ onpointerdown={(e) => e.stopPropagation()}
283
+ onclick={(e) => e.stopPropagation()}
284
+ />
285
+ {:else if et === "list" || et === "select" || et === "rich-select"}
286
+ {@const opts = getColumnEditorOptions(column, row)}
287
+ <select
288
+ class="sv-grid-cell-editor sv-grid-fr-editor"
289
+ value={String(val ?? "")}
290
+ onchange={(e) => ctrl.setFullRowDraft(column.id, e.currentTarget.value)}
291
+ onkeydown={fullRowKeydown}
292
+ onpointerdown={(e) => e.stopPropagation()}
293
+ onclick={(e) => e.stopPropagation()}
294
+ >
295
+ {#each opts as o (optionValueOf(o))}
296
+ <option value={optionValueOf(o)}>{optionLabelOf(o)}</option>
297
+ {/each}
298
+ </select>
299
+ {:else}
300
+ {@const inputType =
301
+ et === "number"
302
+ ? "number"
303
+ : et === "date"
304
+ ? "date"
305
+ : et === "datetime"
306
+ ? "datetime-local"
307
+ : et === "time"
308
+ ? "time"
309
+ : et === "password"
310
+ ? "password"
311
+ : "text"}
312
+ <input
313
+ type={inputType}
314
+ class="sv-grid-cell-editor sv-grid-fr-editor"
315
+ value={String(val ?? "")}
316
+ oninput={(e) => ctrl.setFullRowDraft(column.id, e.currentTarget.value)}
317
+ onkeydown={fullRowKeydown}
318
+ onpointerdown={(e) => e.stopPropagation()}
319
+ onclick={(e) => e.stopPropagation()}
320
+ />
321
+ {/if}
322
+ {:else}
323
+ {#if column.columnDef.cellEditor}
324
+ <!-- Custom editor slot. The columnDef provides a snippet that
325
+ receives the editor context (value + commit + cancel) so the
326
+ consumer fully owns the in-cell UI. -->
327
+ {@const customEditor = column.columnDef
328
+ .cellEditor as unknown as import("svelte").Snippet<
329
+ [EditorContext<TData>]
330
+ >}
331
+ {@render customEditor({
332
+ cell: row.getAllCells().find((c) => c.column.id === column.id)!,
333
+ row,
334
+ column,
335
+ table: grid,
336
+ getValue: () => ctrl.editingCell?.value,
337
+ value: ctrl.editingCell?.value,
338
+ update: (next: unknown) => {
339
+ // Stage the draft without closing. Live-preview controls
340
+ // (sliders, color pickers) call this on every input tick.
341
+ ctrl.editingCell = ctrl.editingCell
342
+ ? { ...ctrl.editingCell, value: next }
343
+ : ctrl.editingCell;
344
+ },
345
+ commit: (next?: unknown) => {
346
+ // Write + close. If the caller passed a value, stage it
347
+ // first; otherwise save whatever update() last wrote.
348
+ if (next !== undefined) {
349
+ ctrl.editingCell = ctrl.editingCell
350
+ ? { ...ctrl.editingCell, value: next }
351
+ : ctrl.editingCell;
352
+ }
353
+ saveEditingCell();
354
+ },
355
+ cancel: () => {
356
+ ctrl.editingCell = null;
357
+ ctrl.gridRootEl?.focus({ preventScroll: true });
358
+ },
359
+ })}
360
+ {:else if ctrl.editingCell?.editorType === "checkbox"}
361
+ <button
362
+ use:focusControlOnMount
363
+ type="button"
364
+ class="sv-grid-checkbox"
365
+ role="checkbox"
366
+ aria-checked={Boolean(ctrl.editingCell.value)}
367
+ aria-label="Edit checkbox value"
368
+ onclick={(event) => {
369
+ event.stopPropagation();
370
+ const nextValue = !ctrl.editingCell?.value;
371
+ ctrl.editingCell = ctrl.editingCell
372
+ ? { ...ctrl.editingCell, value: nextValue }
373
+ : ctrl.editingCell;
374
+ saveEditingCell();
375
+ }}
376
+ onkeydown={(event) => {
377
+ // Enter / Space are the checkbox's own activation keys. Everything
378
+ // else - Escape to cancel, Tab to commit and move on - goes to the
379
+ // shared editor handler, which this editor used to bypass entirely.
380
+ if (event.key === "Enter" || event.key === " ") {
381
+ toggleCheckboxWithKeyboard(event, () => {
382
+ event.stopPropagation();
383
+ const nextValue = !ctrl.editingCell?.value;
384
+ ctrl.editingCell = ctrl.editingCell
385
+ ? { ...ctrl.editingCell, value: nextValue }
386
+ : ctrl.editingCell;
387
+ saveEditingCell();
388
+ });
389
+ return;
390
+ }
391
+ onEditorKeyDown(event);
392
+ }}
393
+ onblur={() => saveEditingCell()}
394
+ ></button>
395
+ {:else if ctrl.editingCell?.editorType === "list"}
396
+ {@const opts = getColumnEditorOptions(column, row)}
397
+ {@const optsLoading = areEditorOptionsLoading(column, row)}
398
+ {@const multi = column.columnDef.editorMultiple === true}
399
+ {#if DropdownEditor}
400
+ <DropdownEditor
401
+ options={opts}
402
+ loading={optsLoading}
403
+ value={ctrl.editingCell?.value}
404
+ multiple={multi}
405
+ placeholder="Select…"
406
+ onChange={(next) => {
407
+ ctrl.editingCell = ctrl.editingCell
408
+ ? { ...ctrl.editingCell, value: next }
409
+ : ctrl.editingCell;
410
+ }}
411
+ onCommit={() => saveEditingCell()}
412
+ onCancel={() => {
413
+ ctrl.editingCell = null;
414
+ ctrl.gridRootEl?.focus({ preventScroll: true });
415
+ }}
416
+ />
417
+ {/if}
418
+ {:else if ctrl.editingCell?.editorType === "chips"}
419
+ {@const opts = getColumnEditorOptions(column, row)}
420
+ {@const optsLoading = areEditorOptionsLoading(column, row)}
421
+ {@const multi = column.columnDef.editorMultiple === true}
422
+ {@const selectedArr = toValueArray(ctrl.editingCell?.value)}
423
+ {@render chipsEditor(opts, multi, selectedArr, optsLoading)}
424
+ {:else if ctrl.editingCell?.editorType === "rating"}
425
+ {@const ratingVal = Math.max(
426
+ 0,
427
+ Math.min(5, Math.round(Number(ctrl.editingCell?.value) || 0)),
428
+ )}
429
+ <!-- Focus the star for the current rating (the first one when unrated),
430
+ the radio a radiogroup is expected to enter on. Without it nothing
431
+ in this editor held focus, so `onEditorKeyDown` below never ran and
432
+ Escape could not close the cell. -->
433
+ {@const focusStar = Math.min(5, Math.max(1, ratingVal))}
434
+ <span class="sv-grid-rating-editor" role="radiogroup" aria-label="Rating">
435
+ {#each [1, 2, 3, 4, 5] as n (n)}
436
+ <button
437
+ use:focusControlOnMount={n === focusStar}
438
+ type="button"
439
+ role="radio"
440
+ aria-checked={ratingVal >= n}
441
+ aria-label={`${n} ${n === 1 ? "star" : "stars"}`}
442
+ class={`sv-grid-rating-star ${ratingVal >= n ? "sv-grid-rating-star-on" : ""}`}
443
+ onmousedown={(event) => event.preventDefault()}
444
+ onclick={(event) => {
445
+ event.stopPropagation();
446
+ ctrl.editingCell = ctrl.editingCell
447
+ ? { ...ctrl.editingCell, value: n }
448
+ : ctrl.editingCell;
449
+ saveEditingCell();
450
+ }}
451
+ onkeydown={(event) => {
452
+ // Arrows move between the stars, the way a radiogroup is
453
+ // expected to work - without them the editor takes focus (see
454
+ // focusControlOnMount) but Space can only re-pick the star that
455
+ // already holds the value. Enter / Escape / Tab still fall
456
+ // through to the shared handler.
457
+ const key = event.key;
458
+ if (
459
+ key !== "ArrowLeft" &&
460
+ key !== "ArrowRight" &&
461
+ key !== "Home" &&
462
+ key !== "End"
463
+ ) {
464
+ onEditorKeyDown(event);
465
+ return;
466
+ }
467
+ event.preventDefault();
468
+ event.stopPropagation();
469
+ const current = Math.min(5, Math.max(1, ratingVal));
470
+ const next =
471
+ key === "Home"
472
+ ? 1
473
+ : key === "End"
474
+ ? 5
475
+ : key === "ArrowLeft"
476
+ ? Math.max(1, current - 1)
477
+ : Math.min(5, current + 1);
478
+ ctrl.editingCell = ctrl.editingCell
479
+ ? { ...ctrl.editingCell, value: next }
480
+ : ctrl.editingCell;
481
+ const stars = (
482
+ event.currentTarget as HTMLElement
483
+ ).parentElement?.querySelectorAll<HTMLElement>(
484
+ ".sv-grid-rating-star",
485
+ );
486
+ stars?.[next - 1]?.focus({ preventScroll: true });
487
+ }}>★</button
488
+ >
489
+ {/each}
490
+ <button
491
+ type="button"
492
+ aria-label="Clear rating"
493
+ class="sv-grid-rating-clear"
494
+ onmousedown={(event) => event.preventDefault()}
495
+ onclick={(event) => {
496
+ event.stopPropagation();
497
+ ctrl.editingCell = ctrl.editingCell
498
+ ? { ...ctrl.editingCell, value: 0 }
499
+ : ctrl.editingCell;
500
+ saveEditingCell();
501
+ }}>×</button
502
+ >
503
+ </span>
504
+ {:else if ctrl.editingCell?.editorType === "select"}
505
+ <!-- Custom dropdown: opens a themed popover identical in feel to
506
+ the existing 'list' editor (single-select, no typeahead). -->
507
+ {@const selectOpts = getColumnEditorOptions(column, row)}
508
+ {@const optsLoading = areEditorOptionsLoading(column, row)}
509
+ {#if DropdownEditor}
510
+ <DropdownEditor
511
+ options={selectOpts}
512
+ loading={optsLoading}
513
+ value={ctrl.editingCell?.value}
514
+ multiple={false}
515
+ placeholder="Select…"
516
+ onChange={(next) => {
517
+ ctrl.editingCell = ctrl.editingCell
518
+ ? { ...ctrl.editingCell, value: next }
519
+ : ctrl.editingCell;
520
+ }}
521
+ onCommit={() => saveEditingCell()}
522
+ onCancel={() => {
523
+ ctrl.editingCell = null;
524
+ ctrl.gridRootEl?.focus({ preventScroll: true });
525
+ }}
526
+ />
527
+ {/if}
528
+ {:else if ctrl.editingCell?.editorType === "rich-select"}
529
+ <!-- Searchable combobox: same popover as 'select' with a
530
+ typeahead filter input baked in at the top. -->
531
+ {@const richOpts = getColumnEditorOptions(column, row)}
532
+ {@const optsLoading = areEditorOptionsLoading(column, row)}
533
+ {#if DropdownEditor}
534
+ <DropdownEditor
535
+ options={richOpts}
536
+ loading={optsLoading}
537
+ value={ctrl.editingCell?.value}
538
+ multiple={false}
539
+ searchable={true}
540
+ placeholder="Search…"
541
+ onChange={(next) => {
542
+ ctrl.editingCell = ctrl.editingCell
543
+ ? { ...ctrl.editingCell, value: next }
544
+ : ctrl.editingCell;
545
+ }}
546
+ onCommit={() => saveEditingCell()}
547
+ onCancel={() => {
548
+ ctrl.editingCell = null;
549
+ ctrl.gridRootEl?.focus({ preventScroll: true });
550
+ }}
551
+ />
552
+ {/if}
553
+ {:else if ctrl.editingCell?.editorType === "textarea"}
554
+ <!-- Multi-line editor. Commits on Tab, Ctrl/Cmd+Enter, or blur.
555
+ Plain Enter inserts a newline (the whole point of textarea).
556
+ Esc cancels. -->
557
+ <textarea
558
+ use:focusOnMount
559
+ class="sv-grid-cell-editor sv-grid-cell-editor-textarea"
560
+ rows="4"
561
+ value={String(ctrl.editingCell?.value ?? "")}
562
+ onpointerdown={(event) => event.stopPropagation()}
563
+ oninput={(event) =>
564
+ updateEditingCellValue(
565
+ (event.currentTarget as HTMLTextAreaElement).value,
566
+ )}
567
+ onkeydown={(event) => {
568
+ event.stopPropagation();
569
+ if (event.key === "Escape") {
570
+ event.preventDefault();
571
+ ctrl.editingCell = null;
572
+ ctrl.gridRootEl?.focus({ preventScroll: true });
573
+ return;
574
+ }
575
+ // Tab and Ctrl/Cmd+Enter both commit. Plain Enter inserts a newline.
576
+ // Tab goes through the shared helper so it advances the active cell
577
+ // like every other editor does (#48).
578
+ if (event.key === "Tab") {
579
+ event.preventDefault();
580
+ ctrl.commitAndMoveByTab(event.shiftKey);
581
+ return;
582
+ }
583
+ if (event.key === "Enter" && (event.ctrlKey || event.metaKey)) {
584
+ event.preventDefault();
585
+ saveEditingCell();
586
+ ctrl.gridRootEl?.focus({ preventScroll: true });
587
+ }
588
+ }}
589
+ onblur={() => saveEditingCell()}
590
+ ></textarea>
591
+ {:else if ctrl.editingCell?.editorType === "autocomplete"}
592
+ <!-- Free-text autocomplete: a text input with a live-filtered
593
+ suggestion list. Typing edits the value freely; clicking a
594
+ suggestion (or blur) commits. Accepts values not in the list. -->
595
+ {@const acOpts = getColumnEditorOptions(column, row)}
596
+ {@const acText = String(ctrl.editingCell?.value ?? "")}
597
+ {@const acFiltered = acText.trim()
598
+ ? acOpts.filter((o) =>
599
+ String(o.label).toLowerCase().includes(acText.toLowerCase()),
600
+ )
601
+ : acOpts}
602
+ <div class="sv-grid-autocomplete">
603
+ <input
604
+ use:focusOnMount
605
+ class="sv-grid-cell-editor sv-grid-cell-editor-autocomplete"
606
+ type="text"
607
+ value={acText}
608
+ onpointerdown={(event) => event.stopPropagation()}
609
+ oninput={(event) =>
610
+ updateEditingCellValue(
611
+ (event.currentTarget as HTMLInputElement).value,
612
+ )}
613
+ onblur={() => saveEditingCell()}
614
+ onkeydown={onEditorKeyDown}
615
+ />
616
+ {#if acFiltered.length > 0}
617
+ <div class="sv-grid-autocomplete-list" role="listbox">
618
+ {#each acFiltered.slice(0, 50) as opt (opt.value)}
619
+ <button
620
+ type="button"
621
+ class="sv-grid-autocomplete-option"
622
+ role="option"
623
+ aria-selected={String(opt.value) === acText}
624
+ onmousedown={(event) => {
625
+ event.preventDefault();
626
+ ctrl.editingCell = ctrl.editingCell
627
+ ? { ...ctrl.editingCell, value: opt.value }
628
+ : ctrl.editingCell;
629
+ saveEditingCell();
630
+ }}>{opt.label}</button
631
+ >
632
+ {/each}
633
+ </div>
634
+ {/if}
635
+ </div>
636
+ {:else if ctrl.editingCell?.editorType === "date"}
637
+ <!-- Rich date editor: SvCalendar popover over a formatted input. Opts
638
+ out to the native <input type="date"> via editorType 'date-native'. -->
639
+ {#if DateEditor}
640
+ <DateEditor
641
+ value={ctrl.editingCell?.value as string | number | Date | null}
642
+ formatString="yyyy-MM-dd"
643
+ dropDownDisplayMode="calendar"
644
+ autoOpen
645
+ block
646
+ onChange={(d) => updateEditingCellValue(d)}
647
+ onCommit={() => saveEditingCell()}
648
+ onCancel={() => {
649
+ ctrl.editingCell = null;
650
+ ctrl.gridRootEl?.focus({ preventScroll: true });
651
+ }}
652
+ />
653
+ {/if}
654
+ {:else if ctrl.editingCell?.editorType === "datetime"}
655
+ {#if DateEditor}
656
+ <DateEditor
657
+ value={ctrl.editingCell?.value as string | number | Date | null}
658
+ formatString="yyyy-MM-dd HH:mm"
659
+ dropDownDisplayMode="both"
660
+ autoOpen
661
+ block
662
+ onChange={(d) => updateEditingCellValue(d)}
663
+ onCommit={() => saveEditingCell()}
664
+ onCancel={() => {
665
+ ctrl.editingCell = null;
666
+ ctrl.gridRootEl?.focus({ preventScroll: true });
667
+ }}
668
+ />
669
+ {/if}
670
+ {:else if ctrl.editingCell?.editorType === "time"}
671
+ <!-- Rich time editor: SvTimePicker dial. Stores the 'HH:MM' string the
672
+ `time` parser expects; opts out via editorType 'time-native'. -->
673
+ {#if DateEditor}
674
+ <DateEditor
675
+ value={timeStringToDate(ctrl.editingCell?.value)}
676
+ formatString="HH:mm"
677
+ dropDownDisplayMode="time"
678
+ autoOpen
679
+ block
680
+ onChange={(d) => updateEditingCellValue(dateToTimeString(d))}
681
+ onCommit={() => saveEditingCell()}
682
+ onCancel={() => {
683
+ ctrl.editingCell = null;
684
+ ctrl.gridRootEl?.focus({ preventScroll: true });
685
+ }}
686
+ />
687
+ {/if}
688
+ {:else if ctrl.editingCell?.editorType === "color"}
689
+ <!-- Native <input type="color"> opens its picker in a separate OS
690
+ overlay; once the picker closes, focus stays on the input so
691
+ `blur` never fires on its own. Commit on `change` (which fires
692
+ exactly once when the picker is dismissed) so the chosen color
693
+ is saved without needing the user to click elsewhere. -->
694
+ <input
695
+ use:focusOnMount
696
+ class={getEditorClass("color")}
697
+ type="color"
698
+ value={getEditableInputValue("color", ctrl.editingCell?.value)}
699
+ oninput={(event) =>
700
+ updateEditingCellValue(
701
+ (event.currentTarget as HTMLInputElement).value,
702
+ )}
703
+ onchange={(event) => {
704
+ updateEditingCellValue(
705
+ (event.currentTarget as HTMLInputElement).value,
706
+ );
707
+ saveEditingCell();
708
+ }}
709
+ onblur={() => saveEditingCell()}
710
+ onkeydown={onEditorKeyDown}
711
+ />
712
+ {:else if hasCellEditor(String(ctrl.editingCell?.editorType ?? ""))}
713
+ <!-- Custom editor registered via `registerCellEditor(type, Component)`:
714
+ mount it and hand it the uniform change / commit / cancel context. -->
715
+ {@const _reg = getCellEditor(String(ctrl.editingCell?.editorType))!}
716
+ {@const CustomCellEditor = _reg.component}
717
+ <CustomCellEditor {...resolveEditorProps(_reg, buildRegisteredEditorContext())} />
718
+ {:else}
719
+ {@const editorType = ctrl.editingCell?.editorType ?? "text"}
720
+ {@const isNumberEditor = getCellEditorInputType(editorType) === "text" &&
721
+ getEditorInputType(editorType) === "number"}
722
+ <input
723
+ use:focusOnMount
724
+ class={getEditorClass(editorType)}
725
+ type={getCellEditorInputType(editorType)}
726
+ inputmode={isNumberEditor ? "decimal" : undefined}
727
+ value={getEditableInputValue(
728
+ editorType,
729
+ ctrl.editingCell?.value,
730
+ )}
731
+ oninput={(event) => {
732
+ const input = event.currentTarget as HTMLInputElement;
733
+ // The number editor is a TEXT input so intermediate values survive
734
+ // (see getCellEditorInputType). That means nothing stops a letter
735
+ // being typed, which type="number" used to - so reject it here and
736
+ // put the last good draft back, rather than letting "12x" reach the
737
+ // commit and coerce to null.
738
+ if (isNumberEditor && !isNumericEditorInput(input.value)) {
739
+ input.value = String(ctrl.editingCell?.value ?? "");
740
+ return;
741
+ }
742
+ updateEditingCellValue(input.value);
743
+ }}
744
+ onblur={() => saveEditingCell()}
745
+ onkeydown={onEditorKeyDown}
746
+ />
747
+ {/if}
748
+ {/if}