@svgrid/grid 2.6.21 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (358) hide show
  1. package/CHANGELOG.md +150 -88
  2. package/README.md +221 -199
  3. package/dist/FlexRender.svelte +96 -96
  4. package/dist/GridFooter.svelte +181 -181
  5. package/dist/GridMenus.svelte +201 -18
  6. package/dist/SvAutoComplete.svelte +169 -165
  7. package/dist/SvAvatar.svelte +75 -75
  8. package/dist/SvCalendar.svelte +503 -503
  9. package/dist/SvCarousel.svelte +141 -141
  10. package/dist/SvCheckBox.svelte +102 -102
  11. package/dist/SvCircularProgress.svelte +109 -109
  12. package/dist/SvColorInput.svelte +181 -181
  13. package/dist/SvComboBox.svelte +279 -275
  14. package/dist/SvContextMenu.svelte +116 -116
  15. package/dist/SvCountryInput.svelte +163 -163
  16. package/dist/SvDateRangeInput.svelte +25 -3
  17. package/dist/SvDrawer.svelte +254 -254
  18. package/dist/SvDropDownList.svelte +378 -374
  19. package/dist/SvDurationInput.svelte +126 -126
  20. package/dist/SvField.svelte +293 -293
  21. package/dist/SvForm.svelte +437 -437
  22. package/dist/SvGrid.controller.svelte.d.ts +61 -9
  23. package/dist/SvGrid.controller.svelte.js +363 -123
  24. package/dist/SvGrid.css +222 -1
  25. package/dist/SvGrid.helpers.d.ts +55 -0
  26. package/dist/SvGrid.helpers.js +83 -0
  27. package/dist/SvGrid.svelte +255 -56
  28. package/dist/SvGrid.types.d.ts +146 -1
  29. package/dist/SvGridCellEditor.svelte +744 -662
  30. package/dist/SvGridChart.svelte +1716 -1716
  31. package/dist/SvGridChartPanel.svelte +485 -485
  32. package/dist/SvGridChartView.svelte +70 -70
  33. package/dist/SvGridDropdown.svelte +728 -728
  34. package/dist/SvGridSelect.svelte +270 -266
  35. package/dist/SvGridSelect.svelte.d.ts +1 -1
  36. package/dist/SvGroupCell.svelte +116 -116
  37. package/dist/SvListBox.svelte +334 -326
  38. package/dist/SvMaskedInput.svelte +122 -122
  39. package/dist/SvMenu.svelte +124 -124
  40. package/dist/SvMenuList.svelte +146 -146
  41. package/dist/SvMultiSelect.svelte +293 -289
  42. package/dist/SvMultiSelect.svelte.d.ts +1 -1
  43. package/dist/SvNumberInput.svelte +172 -168
  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/SvRichCell.svelte +125 -0
  53. package/dist/SvRichCell.svelte.d.ts +15 -0
  54. package/dist/SvRichText.svelte +211 -211
  55. package/dist/SvRowGroupPanel.svelte +170 -170
  56. package/dist/SvScrollArea.svelte +61 -61
  57. package/dist/SvSlider.svelte +203 -203
  58. package/dist/SvSwitchButton.svelte +108 -108
  59. package/dist/SvTagsInput.svelte +115 -115
  60. package/dist/SvTextArea.svelte +5 -1
  61. package/dist/SvTextInput.svelte +147 -147
  62. package/dist/SvTimePicker.svelte +245 -245
  63. package/dist/SvToaster.svelte +159 -159
  64. package/dist/SvToggleButton.svelte +85 -85
  65. package/dist/SvTooltip.svelte +161 -161
  66. package/dist/SvTour.svelte +208 -208
  67. package/dist/SvTree.svelte +444 -444
  68. package/dist/SvTreeSelect.svelte +239 -235
  69. package/dist/SvTreeSelect.svelte.d.ts +1 -1
  70. package/dist/builtin-editors.d.ts +2 -2
  71. package/dist/builtin-editors.js +19 -3
  72. package/dist/cdn/GridMenus-C4oLsJyj.js +600 -0
  73. package/dist/cdn/GridMenus-yEivjNwl.js +601 -0
  74. package/dist/cdn/SvDateRangeInput-Bh0A0JkF.js +199 -0
  75. package/dist/cdn/SvDateRangeInput-DflbiP7N.js +198 -0
  76. package/dist/cdn/SvDateTimePicker-BWwpfB_o.js +812 -0
  77. package/dist/cdn/SvDateTimePicker-Bivn8dAP.js +816 -0
  78. package/dist/cdn/SvGridCellEditor-AXL8cHGO.js +550 -0
  79. package/dist/cdn/SvGridCellEditor-Ba7rY3eu.js +549 -0
  80. package/dist/cdn/{SvGridChart-BwVos976.js → SvGridChart-Bs2GIR2Q.js} +1 -1
  81. package/dist/cdn/{SvGridChart-BEJmNNx9.js → SvGridChart-CwhFz7GV.js} +1 -1
  82. package/dist/cdn/{SvGridChartPanel-BOknOkrP.js → SvGridChartPanel-1WXuStzM.js} +1 -1
  83. package/dist/cdn/{SvGridChartPanel-D2PjII4O.js → SvGridChartPanel-e5_nqNP9.js} +1 -1
  84. package/dist/cdn/{SvGridChartView-00LPUHL1.js → SvGridChartView-SmPW10dI.js} +1 -1
  85. package/dist/cdn/{SvGridChartView-BhUEJ5ki.js → SvGridChartView-eSPuJE6g.js} +1 -1
  86. package/dist/cdn/{SvGridDropdown-D0VdjeR8.js → SvGridDropdown-B1ZcLpgs.js} +1 -1
  87. package/dist/cdn/{SvGridDropdown-D13MtJ2j.js → SvGridDropdown-CtdbHcIS.js} +1 -1
  88. package/dist/cdn/column-resize-DsfNXMom.js +102 -0
  89. package/dist/cdn/date-format-CcP1tafP.js +1395 -0
  90. package/dist/cdn/date-format-DZT7T1wf.js +1393 -0
  91. package/dist/cdn/{editor-registry-CiI0xlKg.js → editor-registry-BhgE3S84.js} +51 -25
  92. package/dist/cdn/row-resize-BRcimkUT.js +95 -0
  93. package/dist/cdn/{src-BYq-qyrp.js → src-COC0vnpg.js} +7846 -7317
  94. package/dist/cdn/{src-DBel9wRZ.js → src-pYGV2rMR.js} +10033 -9504
  95. package/dist/cdn/svgrid.js +15 -11
  96. package/dist/cdn/svgrid.svelte-external.js +15 -11
  97. package/dist/cell-values.d.ts +16 -0
  98. package/dist/cell-values.js +28 -0
  99. package/dist/chart-export.js +8 -8
  100. package/dist/clipboard.d.ts +22 -0
  101. package/dist/clipboard.js +405 -27
  102. package/dist/column-groups.js +1 -1
  103. package/dist/column-resize.d.ts +46 -0
  104. package/dist/column-resize.js +205 -0
  105. package/dist/columns.d.ts +0 -3
  106. package/dist/columns.js +0 -57
  107. package/dist/core.d.ts +19 -4
  108. package/dist/core.js +460 -119
  109. package/dist/editing.js +12 -7
  110. package/dist/editors/cell-editors.d.ts +11 -0
  111. package/dist/editors/cell-editors.js +18 -1
  112. package/dist/filtering/excel-filters.js +28 -0
  113. package/dist/grid-messages.d.ts +15 -0
  114. package/dist/grid-messages.js +15 -0
  115. package/dist/group-display.d.ts +1 -1
  116. package/dist/index.d.ts +6 -1
  117. package/dist/index.js +10 -0
  118. package/dist/list-option.d.ts +6 -0
  119. package/dist/menus.js +41 -8
  120. package/dist/row-resize.d.ts +11 -0
  121. package/dist/row-resize.js +7 -1
  122. package/dist/sanitize-html.d.ts +37 -0
  123. package/dist/sanitize-html.js +234 -0
  124. package/dist/selection-bar-view.svelte.d.ts +25 -0
  125. package/dist/selection-bar-view.svelte.js +13 -0
  126. package/dist/selection.d.ts +2 -1
  127. package/dist/selection.js +133 -8
  128. package/dist/spreadsheet.d.ts +1 -1
  129. package/dist/spreadsheet.js +1 -1
  130. package/package.json +11 -11
  131. package/src/FlexRender.svelte +96 -96
  132. package/src/GridFooter.svelte +181 -181
  133. package/src/GridMenus.svelte +201 -18
  134. package/src/SvAutoComplete.svelte +169 -165
  135. package/src/SvAvatar.svelte +75 -75
  136. package/src/SvCalendar.svelte +503 -503
  137. package/src/SvCalendar.test.ts +226 -226
  138. package/src/SvCarousel.svelte +141 -141
  139. package/src/SvCheckBox.svelte +102 -102
  140. package/src/SvCircularProgress.svelte +109 -109
  141. package/src/SvColorInput.svelte +181 -181
  142. package/src/SvComboBox.svelte +279 -275
  143. package/src/SvContextMenu.svelte +116 -116
  144. package/src/SvCountryInput.svelte +163 -163
  145. package/src/SvDateRangeInput.svelte +25 -3
  146. package/src/SvDrawer.svelte +254 -254
  147. package/src/SvDropDownList.svelte +378 -374
  148. package/src/SvDurationInput.svelte +126 -126
  149. package/src/SvField.svelte +293 -293
  150. package/src/SvForm.svelte +437 -437
  151. package/src/SvForm.test.ts +411 -411
  152. package/src/SvGrid.controller.svelte.ts +404 -133
  153. package/src/SvGrid.css +222 -1
  154. package/src/SvGrid.helpers.ts +87 -0
  155. package/src/SvGrid.svelte +255 -56
  156. package/src/SvGrid.types.ts +2092 -1940
  157. package/src/SvGridCellEditor.svelte +744 -662
  158. package/src/SvGridChart.svelte +1716 -1716
  159. package/src/SvGridChartPanel.svelte +485 -485
  160. package/src/SvGridChartView.svelte +70 -70
  161. package/src/SvGridDropdown.svelte +728 -728
  162. package/src/SvGridSelect.svelte +270 -266
  163. package/src/SvGroupCell.svelte +116 -116
  164. package/src/SvListBox.svelte +334 -326
  165. package/src/SvMaskedInput.svelte +122 -122
  166. package/src/SvMenu.svelte +124 -124
  167. package/src/SvMenu.test.ts +97 -97
  168. package/src/SvMenuList.svelte +146 -146
  169. package/src/SvMultiSelect.svelte +293 -289
  170. package/src/SvNumberInput.svelte +172 -168
  171. package/src/SvOtpInput.svelte +158 -158
  172. package/src/SvPasswordInput.svelte +151 -151
  173. package/src/SvPhoneInput.svelte +133 -133
  174. package/src/SvPopover.svelte +197 -197
  175. package/src/SvProgress.svelte +116 -116
  176. package/src/SvRadioGroup.svelte +107 -107
  177. package/src/SvRating.svelte +112 -112
  178. package/src/SvResult.svelte +73 -73
  179. package/src/SvRichCell.svelte +125 -0
  180. package/src/SvRichText.svelte +211 -211
  181. package/src/SvRowGroupPanel.svelte +170 -170
  182. package/src/SvScrollArea.svelte +61 -61
  183. package/src/SvSlider.svelte +203 -203
  184. package/src/SvSwitchButton.svelte +108 -108
  185. package/src/SvTagsInput.svelte +115 -115
  186. package/src/SvTextArea.svelte +5 -1
  187. package/src/SvTextInput.svelte +147 -147
  188. package/src/SvTimePicker.svelte +245 -245
  189. package/src/SvToaster.svelte +159 -159
  190. package/src/SvToaster.test.ts +95 -95
  191. package/src/SvToggleButton.svelte +85 -85
  192. package/src/SvTooltip.svelte +161 -161
  193. package/src/SvTour.svelte +208 -208
  194. package/src/SvTree.svelte +444 -444
  195. package/src/SvTreeSelect.svelte +239 -235
  196. package/src/a11y/dismissable.test.ts +119 -119
  197. package/src/a11y/dismissable.ts +114 -114
  198. package/src/a11y.contract.test.ts +49 -49
  199. package/src/a11y.test.ts +59 -59
  200. package/src/a11y.ts +61 -61
  201. package/src/ai.test.ts +502 -502
  202. package/src/ai.ts +1419 -1419
  203. package/src/build-api.coverage.test.ts +633 -633
  204. package/src/build-api.ts +846 -846
  205. package/src/builtin-editors.grid.test.ts +83 -83
  206. package/src/builtin-editors.test.ts +18 -1
  207. package/src/builtin-editors.ts +19 -3
  208. package/src/cell-formatting.ts +171 -171
  209. package/src/cell-render.test.ts +513 -513
  210. package/src/cell-render.ts +496 -496
  211. package/src/cell-values.ts +148 -118
  212. package/src/chart-export.ts +202 -202
  213. package/src/chart-view.svelte.ts +36 -36
  214. package/src/chart.ts +2321 -2321
  215. package/src/clipboard.test.ts +93 -20
  216. package/src/clipboard.ts +433 -33
  217. package/src/collaboration.test.ts +104 -104
  218. package/src/collaboration.ts +167 -167
  219. package/src/column-groups.ts +78 -78
  220. package/src/column-resize.test.ts +381 -0
  221. package/src/column-resize.ts +227 -0
  222. package/src/columns.test.ts +0 -103
  223. package/src/columns.ts +0 -58
  224. package/src/core.aggregate.test.ts +134 -0
  225. package/src/core.filter.test.ts +156 -0
  226. package/src/core.grouping.test.ts +146 -0
  227. package/src/core.performance.test.ts +30 -30
  228. package/src/core.row-shape.test.ts +119 -0
  229. package/src/core.rowmodel-cache.test.ts +121 -0
  230. package/src/core.sort.test.ts +293 -0
  231. package/src/core.ts +1865 -1468
  232. package/src/createAutocomplete.svelte.ts +132 -132
  233. package/src/createCombobox.svelte.ts +191 -191
  234. package/src/createCountryInput.svelte.ts +157 -157
  235. package/src/createDropdownList.svelte.ts +168 -168
  236. package/src/createForm.svelte.ts +386 -386
  237. package/src/createGrid.svelte.ts +42 -42
  238. package/src/createGrid.test.ts +10 -10
  239. package/src/createGridState.svelte.ts +17 -17
  240. package/src/createListbox.svelte.ts +250 -250
  241. package/src/createMenu.svelte.ts +224 -224
  242. package/src/createPopoverSelect.svelte.ts +213 -213
  243. package/src/createSlider.svelte.ts +191 -191
  244. package/src/createTooltip.svelte.ts +144 -144
  245. package/src/createTree.svelte.ts +322 -322
  246. package/src/date-string-column.test.ts +87 -0
  247. package/src/datetime/date-core.ts +206 -206
  248. package/src/datetime/date-restrict.ts +61 -61
  249. package/src/datetime/timezone.ts +135 -135
  250. package/src/dock-manager-model.ts +596 -596
  251. package/src/dock-model.ts +374 -374
  252. package/src/editing.test.ts +974 -949
  253. package/src/editing.ts +609 -606
  254. package/src/editor-block-prop.test.ts +132 -0
  255. package/src/editor-contract.ts +171 -171
  256. package/src/editor-registry.grid.test.ts +144 -144
  257. package/src/editor-registry.ts +122 -122
  258. package/src/editors/cell-editors.ts +27 -1
  259. package/src/export-data-api.test.ts +126 -126
  260. package/src/export-format.test.ts +107 -107
  261. package/src/export-format.ts +601 -601
  262. package/src/filter-operators.ts +160 -160
  263. package/src/filtering/excel-filters.ts +325 -295
  264. package/src/filtering/normalize-fast-path.test.ts +104 -0
  265. package/src/flex-render.ts +3 -3
  266. package/src/form-field.ts +127 -127
  267. package/src/grid-messages.ts +34 -0
  268. package/src/group-display.test.ts +167 -167
  269. package/src/group-display.ts +200 -200
  270. package/src/headless.ts +87 -87
  271. package/src/index.ts +25 -1
  272. package/src/js-scroller.svelte.ts +173 -173
  273. package/src/keyboard-handlers.ts +270 -270
  274. package/src/keyboard.test.ts +59 -59
  275. package/src/keyboard.ts +97 -97
  276. package/src/list-nav.test.ts +49 -49
  277. package/src/list-nav.ts +29 -29
  278. package/src/list-option.test.ts +56 -56
  279. package/src/list-option.ts +179 -173
  280. package/src/menus.test.ts +43 -0
  281. package/src/menus.ts +597 -567
  282. package/src/merge-objects.ts +48 -48
  283. package/src/move-cells.test.ts +850 -0
  284. package/src/number-editor-literal.test.ts +84 -0
  285. package/src/number-editor.grid.test.ts +160 -0
  286. package/src/overlays.test.ts +90 -90
  287. package/src/positioning.ts +268 -268
  288. package/src/render-component.ts +28 -28
  289. package/src/resize-props.test.ts +361 -0
  290. package/src/row-drag-touch.ts +211 -211
  291. package/src/row-drag.test.ts +353 -353
  292. package/src/row-drag.ts +415 -415
  293. package/src/row-resize.test.ts +524 -493
  294. package/src/row-resize.ts +228 -210
  295. package/src/sanitize-html.test.ts +182 -0
  296. package/src/sanitize-html.ts +235 -0
  297. package/src/scheduler-ical.ts +181 -181
  298. package/src/scheduler-model.test.ts +562 -562
  299. package/src/scheduler-model.ts +873 -873
  300. package/src/selection-bar-view.svelte.ts +36 -0
  301. package/src/selection.test.ts +900 -743
  302. package/src/selection.ts +135 -8
  303. package/src/server-data-source.test.ts +383 -383
  304. package/src/server-data-source.ts +469 -469
  305. package/src/sparkline.test.ts +68 -68
  306. package/src/sparkline.ts +169 -169
  307. package/src/spreadsheet.test.ts +488 -488
  308. package/src/spreadsheet.ts +312 -312
  309. package/src/static-functions.ts +11 -11
  310. package/src/subscribe.ts +38 -38
  311. package/src/summaries.ts +113 -113
  312. package/src/svgrid-wrapper.types.ts +563 -563
  313. package/src/svgrid.async-editor-options.test.ts +273 -273
  314. package/src/svgrid.auto-row-height.test.ts +204 -204
  315. package/src/svgrid.behavior.test.ts +910 -910
  316. package/src/svgrid.charting.test.ts +534 -534
  317. package/src/svgrid.comments-autocomplete.test.ts +127 -127
  318. package/src/svgrid.context-menu.test.ts +147 -147
  319. package/src/svgrid.features.test.ts +157 -157
  320. package/src/svgrid.filter-depth.test.ts +163 -163
  321. package/src/svgrid.filter-menu-listbox.svelte.test.ts +377 -331
  322. package/src/svgrid.filter-menu-scroll.test.ts +112 -112
  323. package/src/svgrid.grand-total.test.ts +188 -188
  324. package/src/svgrid.group-display-mode.test.ts +171 -171
  325. package/src/svgrid.group-footers.test.ts +121 -121
  326. package/src/svgrid.group-pagination.test.ts +153 -153
  327. package/src/svgrid.in-suggest-snapshot.svelte.test.ts +91 -0
  328. package/src/svgrid.menu-scroll-close.test.ts +204 -0
  329. package/src/svgrid.new-features.wrapper.test.ts +251 -251
  330. package/src/svgrid.selection-bar-seam.test.ts +185 -0
  331. package/src/svgrid.tree-data.test.ts +186 -186
  332. package/src/svgrid.upsell-license.test.ts +117 -0
  333. package/src/svgrid.wrapper.test.ts +63 -63
  334. package/src/svgriddropdown.async-panel.svelte.test.ts +195 -195
  335. package/src/test-setup.ts +62 -62
  336. package/src/themes/index.ts +288 -288
  337. package/src/toast-store.svelte.ts +250 -250
  338. package/src/toast-store.test.ts +147 -147
  339. package/src/tree-row-model.test.ts +168 -168
  340. package/src/ui-buttons.test.ts +144 -144
  341. package/src/ui-inputs.test.ts +118 -118
  342. package/src/ui-localization.test.ts +113 -113
  343. package/src/ui-range.test.ts +70 -70
  344. package/src/ui-selection.test.ts +155 -155
  345. package/src/ui-tier1.test.ts +142 -142
  346. package/src/virtual.test.ts +88 -88
  347. package/src/virtualization/column-virtualizer.test.ts +27 -27
  348. package/src/virtualization/column-virtualizer.ts +30 -30
  349. package/src/virtualization/svelte-virtualizer.svelte.ts +26 -26
  350. package/src/virtualization/types.ts +30 -30
  351. package/src/virtualization/virtualizer.test.ts +47 -47
  352. package/src/virtualization/virtualizer.ts +322 -322
  353. package/dist/cdn/GridMenus-BfTAKn84.js +0 -488
  354. package/dist/cdn/GridMenus-C3bJd7w8.js +0 -489
  355. package/dist/cdn/SvDateTimePicker-CHnUkWcH.js +0 -2206
  356. package/dist/cdn/SvDateTimePicker-CRQH_U7E.js +0 -2201
  357. package/dist/cdn/SvGridCellEditor-BGUCzuPB.js +0 -523
  358. package/dist/cdn/SvGridCellEditor-G8IMWIws.js +0 -522
package/src/clipboard.ts CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  getCellKey,
14
14
  } from "./SvGrid.helpers";
15
15
  import {
16
+ createDataIndexLookup,
16
17
  getColumnBaseValue,
17
18
  isGroupRow,
18
19
  toolPanelHeaderLabel,
@@ -22,43 +23,61 @@ export function createClipboard<
22
23
  TFeatures extends TableFeatures = TableFeatures,
23
24
  TData extends RowData = RowData,
24
25
  >(ctx: any) {
26
+ /**
27
+ * `rowIndex` here is a DISPLAY index - a position in `allRows`, the space the
28
+ * selection rectangle, the fill drag and `isCellEditableAt` all work in.
29
+ * These two functions used to index `internalData` with it directly, which is
30
+ * the same row only while the grid is unsorted, unfiltered and unpaginated.
31
+ * Sort a column and the fill handle read its pattern from one row and wrote
32
+ * to another; enterprise bulk edit wrote to the wrong rows the same way.
33
+ *
34
+ * `row.original` IS the object in `data`, so identity resolves the slot no
35
+ * matter how the view is ordered.
36
+ */
37
+ function rowDataAt(rowIndex: number): Record<string, unknown> | undefined {
38
+ return ctx.allRows[rowIndex]?.original as Record<string, unknown> | undefined;
39
+ }
40
+
25
41
  /** Read the raw underlying value for the cell at (rowIndex, columnId)
26
42
  * for pattern extraction. */
27
43
  function readCellRaw(rowIndex: number, columnId: string): unknown {
28
- const row = ctx.internalData[rowIndex];
44
+ const row = rowDataAt(rowIndex);
29
45
  const column = ctx.findColumnById(columnId);
30
46
  if (!row || !column?.columnDef.field) return undefined;
31
- return (row as Record<string, unknown>)[column.columnDef.field];
47
+ return row[column.columnDef.field];
32
48
  }
33
49
 
34
50
  /** Write a value into (rowIndex, columnId) without going through the
35
51
  * edit lifecycle. Fires `onCellValueChange` per write so consumers
36
52
  * can react (formula recompute, autosave, etc.). */
37
53
  function writeCellRaw(rowIndex: number, columnId: string, value: unknown) {
38
- const row = ctx.internalData[rowIndex];
54
+ const row = rowDataAt(rowIndex);
39
55
  const column = ctx.findColumnById(columnId);
40
56
  if (!row || !column?.columnDef.field) return;
41
57
  const field = column.columnDef.field;
42
- const oldValue = (row as Record<string, unknown>)[field];
58
+ const oldValue = row[field];
43
59
  if (oldValue === value) return;
44
- // Resolve the row's id BEFORE swapping internalData - otherwise the
45
- // recomputed `allRows` references the new row object and the
46
- // `r.original === row` lookup fails, dropping our edit out of the
47
- // `editedCellValues` map (which getCellDisplayValue consults first).
48
- const rowId = ctx.allRows.find((r: any) => r.original === row)?.id;
60
+ // The slot in `data` backing this displayed row. Also read BEFORE swapping
61
+ // internalData: afterwards the recomputed `allRows` references the new row
62
+ // object, so an `original`-based lookup would miss and drop the edit out of
63
+ // `editedCellValues` (which getCellDisplayValue consults first).
64
+ const dataIndex = ctx.internalData.indexOf(row as TData);
65
+ if (dataIndex < 0) return;
66
+ const rowId = ctx.allRows[rowIndex]?.id;
49
67
  const next = ctx.internalData.slice() as Array<TData>;
50
- next[rowIndex] = { ...row, [field]: value } as TData;
68
+ next[dataIndex] = { ...row, [field]: value } as TData;
51
69
  ctx.internalData = next;
52
70
  if (rowId) {
53
71
  const key = getCellKey(rowId, columnId);
54
72
  ctx.editedCellValues = { ...ctx.editedCellValues, [key]: value };
55
73
  }
56
74
  ctx.props.onCellValueChange?.({
57
- rowIndex,
75
+ // The position in `props.data`, matching what the inline editor reports.
76
+ rowIndex: dataIndex,
58
77
  columnId,
59
78
  oldValue,
60
79
  newValue: value,
61
- row: next[rowIndex] as TData,
80
+ row: next[dataIndex] as TData,
62
81
  });
63
82
  }
64
83
 
@@ -71,6 +90,7 @@ export function createClipboard<
71
90
  // Clear fillDrag FIRST so a thrown error doesn't leave the grid
72
91
  // stuck tracking the pointer.
73
92
  ctx.fillDrag = null;
93
+ stopEdgeScroll();
74
94
  const newMinRow = Math.min(d.sourceMinRow, d.targetRow);
75
95
  const newMaxRow = Math.max(d.sourceMaxRow, d.targetRow);
76
96
  const newMinCol = Math.min(d.sourceMinCol, d.targetCol);
@@ -222,24 +242,121 @@ export function createClipboard<
222
242
  // (`onWindowPointerMove` / `endDragSelection`) keep tracking.
223
243
  }
224
244
 
225
- /** Pointermove during fill-drag. We don't get cell coords from the
226
- * event directly - find the td under the pointer via elementFromPoint
227
- * and read its data-svgrid-row / data-svgrid-col attributes. */
228
- function onFillPointerMove(event: PointerEvent) {
229
- if (!ctx.fillDrag) return;
230
- const el = document.elementFromPoint(
231
- event.clientX,
232
- event.clientY,
233
- ) as HTMLElement | null;
245
+ /** The grid cell under a viewport point, or null. Drags cannot read their
246
+ * target off the event: the pointer is over whatever `<td>` happens to be
247
+ * there, and during a fill/move drag the event target is the cell the drag
248
+ * STARTED on. */
249
+ function cellAtPoint(x: number, y: number): { row: number; col: number } | null {
250
+ const el = document.elementFromPoint(x, y) as HTMLElement | null;
234
251
  const cell = el?.closest(
235
252
  "td[data-svgrid-row][data-svgrid-col]",
236
253
  ) as HTMLElement | null;
237
- if (!cell) return;
238
- const r = Number(cell.dataset.svgridRow);
239
- const c = Number(cell.dataset.svgridCol);
240
- if (!Number.isFinite(r) || !Number.isFinite(c)) return;
241
- if (r === ctx.fillDrag.targetRow && c === ctx.fillDrag.targetCol) return;
242
- ctx.fillDrag = { ...ctx.fillDrag, targetRow: r, targetCol: c };
254
+ if (!cell) return null;
255
+ const row = Number(cell.dataset.svgridRow);
256
+ const col = Number(cell.dataset.svgridCol);
257
+ if (!Number.isFinite(row) || !Number.isFinite(col)) return null;
258
+ return { row, col };
259
+ }
260
+
261
+ // ---- Edge auto-scroll -------------------------------------------------
262
+ // Shared by all three drags that extend a rectangle - fill, range move, and
263
+ // plain drag-select. Without it a drag can only reach cells that are already
264
+ // on screen, which on a grid the size this one is built for means most of
265
+ // them are unreachable.
266
+
267
+ /** How far from the viewport edge the scroll starts, in CSS pixels. */
268
+ const EDGE_BAND_PX = 40;
269
+ /** Fastest scroll, in pixels per frame, reached at the very edge. */
270
+ const EDGE_MAX_PX = 20;
271
+
272
+ let edgeFrame: number | null = null;
273
+ let edgePointer: { x: number; y: number } | null = null;
274
+
275
+ function stopEdgeScroll() {
276
+ if (edgeFrame != null) cancelAnimationFrame(edgeFrame);
277
+ edgeFrame = null;
278
+ edgePointer = null;
279
+ }
280
+
281
+ /**
282
+ * Pixels to scroll this frame along one axis. Zero in the middle, ramping to
283
+ * EDGE_MAX_PX at the edge and staying there once the pointer leaves the
284
+ * container entirely - a linear ramp rather than a constant step, so nudging
285
+ * one row past the edge creeps instead of lurching.
286
+ */
287
+ function edgeVelocity(pos: number, min: number, max: number) {
288
+ if (pos < min + EDGE_BAND_PX) {
289
+ const depth = Math.min(EDGE_BAND_PX, min + EDGE_BAND_PX - pos);
290
+ return -Math.ceil((depth / EDGE_BAND_PX) * EDGE_MAX_PX);
291
+ }
292
+ if (pos > max - EDGE_BAND_PX) {
293
+ const depth = Math.min(EDGE_BAND_PX, pos - (max - EDGE_BAND_PX));
294
+ return Math.ceil((depth / EDGE_BAND_PX) * EDGE_MAX_PX);
295
+ }
296
+ return 0;
297
+ }
298
+
299
+ /** Point the in-flight drag at whatever cell is under (x, y) now. */
300
+ function retargetDrag(x: number, y: number) {
301
+ const hit = cellAtPoint(x, y);
302
+ if (!hit) return;
303
+ if (ctx.moveDrag) {
304
+ if (hit.row === ctx.moveDrag.targetRow && hit.col === ctx.moveDrag.targetCol) return;
305
+ ctx.moveDrag = { ...ctx.moveDrag, targetRow: hit.row, targetCol: hit.col };
306
+ } else if (ctx.fillDrag) {
307
+ if (hit.row === ctx.fillDrag.targetRow && hit.col === ctx.fillDrag.targetCol) return;
308
+ ctx.fillDrag = { ...ctx.fillDrag, targetRow: hit.row, targetCol: hit.col };
309
+ } else if (ctx.isDraggingSelection) {
310
+ ctx.extendSelection(hit.row, hit.col);
311
+ ctx.setActiveCell(hit.row, hit.col);
312
+ }
313
+ }
314
+
315
+ function stepEdgeScroll() {
316
+ edgeFrame = null;
317
+ const scroller = ctx.scrollContainer as HTMLElement | null;
318
+ const p = edgePointer;
319
+ const dragging = ctx.moveDrag || ctx.fillDrag || ctx.isDraggingSelection;
320
+ if (!scroller || !p || !dragging) {
321
+ stopEdgeScroll();
322
+ return;
323
+ }
324
+ const rect = scroller.getBoundingClientRect();
325
+ const dx = edgeVelocity(p.x, rect.left, rect.right);
326
+ const dy = edgeVelocity(p.y, rect.top, rect.bottom);
327
+ if (dx || dy) {
328
+ const fromX = scroller.scrollLeft;
329
+ const fromY = scroller.scrollTop;
330
+ scroller.scrollLeft += dx;
331
+ scroller.scrollTop += dy;
332
+ // Only re-target when the view actually moved. At either end the scroll
333
+ // clamps, and re-targeting then would fight a pointer that is parked
334
+ // still - the selection would flicker between two cells every frame.
335
+ if (scroller.scrollLeft !== fromX || scroller.scrollTop !== fromY) {
336
+ retargetDrag(p.x, p.y);
337
+ }
338
+ }
339
+ edgeFrame = requestAnimationFrame(stepEdgeScroll);
340
+ }
341
+
342
+ /**
343
+ * Record where the pointer is and make sure the scroll loop is running. The
344
+ * loop keeps going after the pointer stops moving, which is the whole point:
345
+ * a user parks the cursor at the edge and expects the grid to keep coming.
346
+ */
347
+ function trackEdgeScroll(event: PointerEvent) {
348
+ edgePointer = { x: event.clientX, y: event.clientY };
349
+ if (edgeFrame == null) edgeFrame = requestAnimationFrame(stepEdgeScroll);
350
+ }
351
+
352
+ /** Pointermove during fill-drag. */
353
+ function onFillPointerMove(event: PointerEvent) {
354
+ if (!ctx.fillDrag) return;
355
+ trackEdgeScroll(event);
356
+ const hit = cellAtPoint(event.clientX, event.clientY);
357
+ if (!hit) return;
358
+ if (hit.row === ctx.fillDrag.targetRow && hit.col === ctx.fillDrag.targetCol) return;
359
+ ctx.fillDrag = { ...ctx.fillDrag, targetRow: hit.row, targetCol: hit.col };
243
360
  }
244
361
 
245
362
  function onFillPointerUp() {
@@ -247,6 +364,282 @@ export function createClipboard<
247
364
  applyFillPattern();
248
365
  }
249
366
 
367
+ // ---- Range move / copy (drag the selection border) --------------------
368
+
369
+ /** How thick the draggable strip on a range border is, in CSS pixels.
370
+ * Anything further inside the cell keeps its old behaviour of starting a
371
+ * fresh selection, so this only claims the outer sliver. */
372
+ const MOVE_GRAB_PX = 4;
373
+
374
+ /** Is (clientX, clientY) on the grab strip of the range border that the
375
+ * cell at (rowIndex, colIndex) sits on? Returns false for any cell that
376
+ * is not on a range edge, so the common case costs one call to
377
+ * `getCellRangeEdges` - which itself bails immediately with no selection. */
378
+ function isOnMoveGrabStrip(
379
+ cell: HTMLElement,
380
+ rowIndex: number,
381
+ colIndex: number,
382
+ clientX: number,
383
+ clientY: number,
384
+ ): boolean {
385
+ if (!ctx.moveCellsEffective) return false;
386
+ const edges = ctx.getCellRangeEdges(rowIndex, colIndex);
387
+ if (!edges) return false;
388
+ // One layout read per pointermove, and only once the pointer is over a
389
+ // cell that IS on a range edge. Reading a single rect is cheap unless
390
+ // the DOM is dirty, and the only write this path makes is a boolean flag
391
+ // that changes at most twice per border crossing.
392
+ const box = cell.getBoundingClientRect();
393
+ const x = clientX - box.left;
394
+ const y = clientY - box.top;
395
+ return (
396
+ (edges.top && y >= 0 && y <= MOVE_GRAB_PX) ||
397
+ (edges.bottom && box.height - y >= 0 && box.height - y <= MOVE_GRAB_PX) ||
398
+ (edges.left && x >= 0 && x <= MOVE_GRAB_PX) ||
399
+ (edges.right && box.width - x >= 0 && box.width - x <= MOVE_GRAB_PX)
400
+ );
401
+ }
402
+
403
+ /** Border pointerdown - seed a range move. Returns false when the pointer
404
+ * was not on the grab strip, which is the caller's signal to fall through
405
+ * to normal selection behaviour. */
406
+ function startMoveDrag(
407
+ event: PointerEvent,
408
+ rowIndex: number,
409
+ colIndex: number,
410
+ ): boolean {
411
+ if (event.button !== 0) return false;
412
+ // Mouse and pen only, the same policy drag-select uses (issue #23) and for
413
+ // a worse reason: this path DOES preventDefault, so a finger that happened
414
+ // to land within 4px of a selection border would start a move and kill the
415
+ // browser's native scroll for that gesture. There is no hover on touch
416
+ // either, so the border carries no affordance to find in the first place.
417
+ // Touch already has cut/paste for the same job.
418
+ if (event.pointerType === "touch") return false;
419
+ // `event.target`, not `currentTarget`: Svelte 5 delegates pointerdown to
420
+ // the root, and the strip test needs the cell's own rect - the target can
421
+ // be a span inside the cell, so walk up to the td.
422
+ const cell = (event.target as HTMLElement | null)?.closest?.(
423
+ "td[data-svgrid-row][data-svgrid-col]",
424
+ ) as HTMLElement | null;
425
+ if (!cell) return false;
426
+ if (!isOnMoveGrabStrip(cell, rowIndex, colIndex, event.clientX, event.clientY))
427
+ return false;
428
+ const rect = moveSourceRect(rowIndex, colIndex);
429
+ if (!rect) return false;
430
+ event.stopPropagation();
431
+ // preventDefault stops the drag from rubber-banding a text selection over
432
+ // the rows it passes, which also means the grid never gets focus from the
433
+ // gesture - so take it explicitly, or the keyboard stops working after a
434
+ // move.
435
+ event.preventDefault();
436
+ ctx.gridRootEl?.focus({ preventScroll: true });
437
+ ctx.moveDrag = {
438
+ ...rect,
439
+ grabRow: rowIndex,
440
+ grabCol: colIndex,
441
+ targetRow: rowIndex,
442
+ targetCol: colIndex,
443
+ copy: event.ctrlKey || event.metaKey,
444
+ };
445
+ // No `setPointerCapture`, for the same reason the fill handle skips it:
446
+ // the drag finds its target cell with `elementFromPoint`, and capture
447
+ // would route every move back to the cell the drag started on.
448
+ return true;
449
+ }
450
+
451
+ /** The selection rectangle that CONTAINS (rowIndex, colIndex), which is the
452
+ * one whose border the user grabbed. With several committed ranges the
453
+ * grabbed one is the one that moves, not whichever happens to be active. */
454
+ function moveSourceRect(rowIndex: number, colIndex: number) {
455
+ for (const rect of ctx.getSelectionRects()) {
456
+ if (
457
+ rowIndex >= rect.minRow &&
458
+ rowIndex <= rect.maxRow &&
459
+ colIndex >= rect.minCol &&
460
+ colIndex <= rect.maxCol
461
+ ) {
462
+ return {
463
+ sourceMinRow: rect.minRow,
464
+ sourceMaxRow: rect.maxRow,
465
+ sourceMinCol: rect.minCol,
466
+ sourceMaxCol: rect.maxCol,
467
+ };
468
+ }
469
+ }
470
+ return null;
471
+ }
472
+
473
+ function onMovePointerMove(event: PointerEvent) {
474
+ const d = ctx.moveDrag;
475
+ if (!d) return;
476
+ trackEdgeScroll(event);
477
+ const hit = cellAtPoint(event.clientX, event.clientY);
478
+ // Track the modifier even when the pointer is between cells, so releasing
479
+ // Ctrl over a gap still flips a copy back to a move.
480
+ const copy = event.ctrlKey || event.metaKey;
481
+ if (!hit) {
482
+ if (copy !== d.copy) ctx.moveDrag = { ...d, copy };
483
+ return;
484
+ }
485
+ if (hit.row === d.targetRow && hit.col === d.targetCol && copy === d.copy) return;
486
+ ctx.moveDrag = { ...d, targetRow: hit.row, targetCol: hit.col, copy };
487
+ }
488
+
489
+ function onMovePointerUp(event?: PointerEvent) {
490
+ if (!ctx.moveDrag) return;
491
+ // Take the modifier from the RELEASE. Reading it only on pointermove made
492
+ // "drag, then press Ctrl, then let go" a move - which is the natural way
493
+ // to do it, and is a copy in every spreadsheet.
494
+ if (event) {
495
+ const copy = event.ctrlKey || event.metaKey;
496
+ if (copy !== ctx.moveDrag.copy) ctx.moveDrag = { ...ctx.moveDrag, copy };
497
+ }
498
+ applyMoveRange();
499
+ }
500
+
501
+ /** Where a move would land, or null when the drag has not left the spot it
502
+ * started on. Shared by the drop and by the preview marquee so the outline
503
+ * can never promise a rectangle the drop would refuse. */
504
+ function moveDestRect(d: {
505
+ sourceMinRow: number;
506
+ sourceMaxRow: number;
507
+ sourceMinCol: number;
508
+ sourceMaxCol: number;
509
+ grabRow: number;
510
+ grabCol: number;
511
+ targetRow: number;
512
+ targetCol: number;
513
+ }) {
514
+ const dr = d.targetRow - d.grabRow;
515
+ const dc = d.targetCol - d.grabCol;
516
+ if (dr === 0 && dc === 0) return null;
517
+ const minRow = d.sourceMinRow + dr;
518
+ const maxRow = d.sourceMaxRow + dr;
519
+ const minCol = d.sourceMinCol + dc;
520
+ const maxCol = d.sourceMaxCol + dc;
521
+ if (minRow < 0 || minCol < 0) return null;
522
+ if (maxRow >= ctx.allRows.length) return null;
523
+ if (maxCol >= ctx.allColumns.length) return null;
524
+ return { minRow, maxRow, minCol, maxCol };
525
+ }
526
+
527
+ /**
528
+ * Commit a range move (or copy). Refuses the whole operation rather than
529
+ * applying the part that fits: a move that silently skipped a read-only or
530
+ * grouped destination cell would leave the range split across two places
531
+ * with no way to tell which half landed.
532
+ */
533
+ function applyMoveRange() {
534
+ const d = ctx.moveDrag;
535
+ if (!d) return;
536
+ // Clear the drag FIRST so a thrown error can't leave the grid tracking
537
+ // the pointer forever, exactly as applyFillPattern does.
538
+ ctx.moveDrag = null;
539
+ stopEdgeScroll();
540
+ const dest = moveDestRect(d);
541
+ if (!dest) return;
542
+
543
+ const height = d.sourceMaxRow - d.sourceMinRow + 1;
544
+ const width = d.sourceMaxCol - d.sourceMinCol + 1;
545
+
546
+ // Every column the move touches must be field-backed on BOTH sides: a
547
+ // computed / accessor column has nowhere to write the value back to.
548
+ for (let c = 0; c < width; c += 1) {
549
+ if (!ctx.allColumns[d.sourceMinCol + c]?.columnDef.field) return;
550
+ if (!ctx.allColumns[dest.minCol + c]?.columnDef.field) return;
551
+ }
552
+ // Destination must be writable, and so must the source when the values
553
+ // are leaving it. Group rows fail this through isCellEditableAt.
554
+ for (let r = 0; r < height; r += 1) {
555
+ for (let c = 0; c < width; c += 1) {
556
+ if (!ctx.isCellEditableAt(dest.minRow + r, dest.minCol + c)) return;
557
+ if (
558
+ !d.copy &&
559
+ !ctx.isCellEditableAt(d.sourceMinRow + r, d.sourceMinCol + c)
560
+ )
561
+ return;
562
+ }
563
+ }
564
+
565
+ // Snapshot the source before touching anything - source and destination
566
+ // overlap on any short drag, so reading lazily would move already-moved
567
+ // values a second time.
568
+ const values: unknown[][] = [];
569
+ for (let r = 0; r < height; r += 1) {
570
+ const rowValues: unknown[] = [];
571
+ for (let c = 0; c < width; c += 1) {
572
+ const column = ctx.allColumns[d.sourceMinCol + c];
573
+ rowValues.push(readCellRaw(d.sourceMinRow + r, column.id));
574
+ }
575
+ values.push(rowValues);
576
+ }
577
+
578
+ const steps: Array<{
579
+ rowId: string;
580
+ columnId: string;
581
+ field: string;
582
+ before: unknown;
583
+ after: unknown;
584
+ }> = [];
585
+ const write = (rowIndex: number, colIndex: number, value: unknown) => {
586
+ const column = ctx.allColumns[colIndex];
587
+ if (!column?.columnDef.field) return;
588
+ const before = readCellRaw(rowIndex, column.id);
589
+ if (before === value) return;
590
+ // Resolve the row id BEFORE the write: writeCellRaw swaps in a fresh
591
+ // row object, so an `original`-based lookup afterwards would miss.
592
+ const rowId = ctx.allRows[rowIndex]?.id;
593
+ writeCellRaw(rowIndex, column.id, value);
594
+ if (rowId) {
595
+ steps.push({
596
+ rowId,
597
+ columnId: column.id,
598
+ field: column.columnDef.field as string,
599
+ before,
600
+ after: value,
601
+ });
602
+ }
603
+ };
604
+
605
+ // Clear the whole source first, then write the destination. With the
606
+ // values already snapshotted this is correct even when the rectangles
607
+ // overlap: the destination write always lands after the clear.
608
+ if (!d.copy) {
609
+ for (let r = 0; r < height; r += 1) {
610
+ for (let c = 0; c < width; c += 1) {
611
+ write(d.sourceMinRow + r, d.sourceMinCol + c, null);
612
+ }
613
+ }
614
+ }
615
+ for (let r = 0; r < height; r += 1) {
616
+ for (let c = 0; c < width; c += 1) {
617
+ write(dest.minRow + r, dest.minCol + c, values[r]![c]);
618
+ }
619
+ }
620
+
621
+ // One undo entry per changed cell, appended together so a single Ctrl+Z
622
+ // sequence walks the whole move back. Same shape and truncation rule as
623
+ // a whole-row commit.
624
+ if (steps.length) {
625
+ let hist = ctx.history.slice(0, ctx.historyPtr + 1);
626
+ for (const step of steps) hist.push(step);
627
+ if (hist.length > ctx.UNDO_LIMIT) hist = hist.slice(hist.length - ctx.UNDO_LIMIT);
628
+ ctx.history = hist;
629
+ ctx.historyPtr = ctx.history.length - 1;
630
+ ctx.historyVersion += 1;
631
+ }
632
+
633
+ // Follow the values: the range stays selected where it landed, so a
634
+ // second drag can chain off the first the way it does in a spreadsheet.
635
+ ctx.selectionRanges = [];
636
+ ctx.selectionRange = {
637
+ anchor: { rowIndex: dest.minRow, colIndex: dest.minCol },
638
+ focus: { rowIndex: dest.maxRow, colIndex: dest.maxCol },
639
+ };
640
+ ctx.setActiveCell(dest.minRow, dest.minCol);
641
+ }
642
+
250
643
  function toggleBooleanCell(rowIndex: number, colIndex: number) {
251
644
  const row = ctx.allRows[rowIndex];
252
645
  const column = ctx.allColumns[colIndex];
@@ -393,15 +786,14 @@ export function createClipboard<
393
786
  newValue: unknown
394
787
  row: TData
395
788
  }> = [];
789
+ const dataIndexOf = createDataIndexLookup(next);
396
790
  for (let r = startRow; r <= endRow; r += 1) {
397
791
  const row = ctx.allRows[r];
398
792
  if (!row || isGroupRow(row)) continue;
399
- const dataIndex = Number(row.id);
400
- if (
401
- !Number.isInteger(dataIndex) ||
402
- dataIndex < 0 ||
403
- dataIndex >= next.length
404
- ) continue;
793
+ // By identity, not `Number(row.id)` - that is the array index only under
794
+ // the default getRowId, so a custom one cleared the wrong row.
795
+ const dataIndex = dataIndexOf(row);
796
+ if (dataIndex < 0) continue;
405
797
  const originalRow = next[dataIndex];
406
798
  if (!originalRow) continue;
407
799
  const updated: Record<string, unknown> = {
@@ -465,6 +857,14 @@ export function createClipboard<
465
857
  startFillDrag,
466
858
  onFillPointerMove,
467
859
  onFillPointerUp,
860
+ isOnMoveGrabStrip,
861
+ startMoveDrag,
862
+ onMovePointerMove,
863
+ onMovePointerUp,
864
+ trackEdgeScroll,
865
+ stopEdgeScroll,
866
+ applyMoveRange,
867
+ moveDestRect,
468
868
  toggleBooleanCell,
469
869
  copySelectionToClipboard,
470
870
  clearSelectedCells,