@svgrid/grid 2.7.0 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (264) 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 +181 -181
  5. package/dist/SvAutoComplete.svelte +169 -169
  6. package/dist/SvAvatar.svelte +75 -75
  7. package/dist/SvCalendar.svelte +503 -503
  8. package/dist/SvCarousel.svelte +141 -141
  9. package/dist/SvCheckBox.svelte +102 -102
  10. package/dist/SvCircularProgress.svelte +109 -109
  11. package/dist/SvColorInput.svelte +181 -181
  12. package/dist/SvComboBox.svelte +279 -279
  13. package/dist/SvContextMenu.svelte +116 -116
  14. package/dist/SvCountryInput.svelte +163 -163
  15. package/dist/SvDrawer.svelte +254 -254
  16. package/dist/SvDropDownList.svelte +378 -378
  17. package/dist/SvDurationInput.svelte +126 -126
  18. package/dist/SvField.svelte +293 -293
  19. package/dist/SvForm.svelte +437 -437
  20. package/dist/SvGridCellEditor.svelte +748 -744
  21. package/dist/SvGridChart.svelte +1716 -1716
  22. package/dist/SvGridChartPanel.svelte +485 -485
  23. package/dist/SvGridChartView.svelte +70 -70
  24. package/dist/SvGridDropdown.svelte +728 -728
  25. package/dist/SvGridSelect.svelte +270 -270
  26. package/dist/SvGroupCell.svelte +116 -116
  27. package/dist/SvListBox.svelte +334 -334
  28. package/dist/SvMaskedInput.svelte +122 -122
  29. package/dist/SvMenu.svelte +124 -124
  30. package/dist/SvMenuList.svelte +146 -146
  31. package/dist/SvMultiSelect.svelte +293 -293
  32. package/dist/SvNumberInput.svelte +172 -172
  33. package/dist/SvOtpInput.svelte +158 -158
  34. package/dist/SvPasswordInput.svelte +151 -151
  35. package/dist/SvPhoneInput.svelte +133 -133
  36. package/dist/SvPopover.svelte +197 -197
  37. package/dist/SvProgress.svelte +116 -116
  38. package/dist/SvRadioGroup.svelte +107 -107
  39. package/dist/SvRating.svelte +112 -112
  40. package/dist/SvResult.svelte +73 -73
  41. package/dist/SvRichText.svelte +211 -211
  42. package/dist/SvRowGroupPanel.svelte +170 -170
  43. package/dist/SvScrollArea.svelte +61 -61
  44. package/dist/SvSlider.svelte +203 -203
  45. package/dist/SvSwitchButton.svelte +108 -108
  46. package/dist/SvTagsInput.svelte +115 -115
  47. package/dist/SvTextInput.svelte +147 -147
  48. package/dist/SvTimePicker.svelte +245 -245
  49. package/dist/SvToaster.svelte +159 -159
  50. package/dist/SvToggleButton.svelte +85 -85
  51. package/dist/SvTooltip.svelte +161 -161
  52. package/dist/SvTour.svelte +208 -208
  53. package/dist/SvTree.svelte +444 -444
  54. package/dist/SvTreeSelect.svelte +239 -239
  55. package/dist/cdn/{GridMenus-C4oLsJyj.js → GridMenus-CYzOvmNQ.js} +3 -3
  56. package/dist/cdn/{GridMenus-yEivjNwl.js → GridMenus-DFB6iwF8.js} +3 -3
  57. package/dist/cdn/{SvDateRangeInput-DflbiP7N.js → SvDateRangeInput-CaOuMs8O.js} +1 -1
  58. package/dist/cdn/{SvDateRangeInput-Bh0A0JkF.js → SvDateRangeInput-DMLKmGEc.js} +1 -1
  59. package/dist/cdn/{SvDateTimePicker-Bivn8dAP.js → SvDateTimePicker-CCbDZNZB.js} +1 -1
  60. package/dist/cdn/{SvDateTimePicker-BWwpfB_o.js → SvDateTimePicker-sonaH0oh.js} +1 -1
  61. package/dist/cdn/{SvGridCellEditor-Ba7rY3eu.js → SvGridCellEditor-BWnTi2N7.js} +2 -2
  62. package/dist/cdn/{SvGridCellEditor-AXL8cHGO.js → SvGridCellEditor-CjEJMJKc.js} +2 -2
  63. package/dist/cdn/{SvGridChart-CwhFz7GV.js → SvGridChart-BEJmNNx9.js} +1 -1
  64. package/dist/cdn/{SvGridChart-Bs2GIR2Q.js → SvGridChart-BwVos976.js} +1 -1
  65. package/dist/cdn/{SvGridChartPanel-1WXuStzM.js → SvGridChartPanel-BOknOkrP.js} +1 -1
  66. package/dist/cdn/{SvGridChartPanel-e5_nqNP9.js → SvGridChartPanel-D2PjII4O.js} +1 -1
  67. package/dist/cdn/{SvGridChartView-SmPW10dI.js → SvGridChartView-00LPUHL1.js} +1 -1
  68. package/dist/cdn/{SvGridChartView-eSPuJE6g.js → SvGridChartView-BhUEJ5ki.js} +1 -1
  69. package/dist/cdn/{SvGridDropdown-B1ZcLpgs.js → SvGridDropdown-D0VdjeR8.js} +1 -1
  70. package/dist/cdn/{SvGridDropdown-CtdbHcIS.js → SvGridDropdown-D13MtJ2j.js} +1 -1
  71. package/dist/cdn/{date-format-DZT7T1wf.js → date-format-BNii4zeD.js} +2 -2
  72. package/dist/cdn/{date-format-CcP1tafP.js → date-format-BnnHlqGw.js} +2 -2
  73. package/dist/cdn/{row-drag-touch-Ddaa-QeB.js → row-drag-touch-CycRR65n.js} +12 -10
  74. package/dist/cdn/{src-COC0vnpg.js → src-C8QjSaq-.js} +53 -55
  75. package/dist/cdn/{src-pYGV2rMR.js → src-D0hO2eYN.js} +52 -54
  76. package/dist/cdn/svgrid.js +7 -7
  77. package/dist/cdn/svgrid.svelte-external.js +7 -7
  78. package/dist/chart-export.js +8 -8
  79. package/dist/editing.js +22 -13
  80. package/dist/row-drag-touch.d.ts +5 -1
  81. package/dist/row-drag-touch.js +15 -4
  82. package/dist/row-drag.js +7 -3
  83. package/package.json +11 -11
  84. package/src/FlexRender.svelte +96 -96
  85. package/src/GridFooter.svelte +181 -181
  86. package/src/SvAutoComplete.svelte +169 -169
  87. package/src/SvAvatar.svelte +75 -75
  88. package/src/SvCalendar.svelte +503 -503
  89. package/src/SvCalendar.test.ts +226 -226
  90. package/src/SvCarousel.svelte +141 -141
  91. package/src/SvCheckBox.svelte +102 -102
  92. package/src/SvCircularProgress.svelte +109 -109
  93. package/src/SvColorInput.svelte +181 -181
  94. package/src/SvComboBox.svelte +279 -279
  95. package/src/SvContextMenu.svelte +116 -116
  96. package/src/SvCountryInput.svelte +163 -163
  97. package/src/SvDrawer.svelte +254 -254
  98. package/src/SvDropDownList.svelte +378 -378
  99. package/src/SvDurationInput.svelte +126 -126
  100. package/src/SvField.svelte +293 -293
  101. package/src/SvForm.svelte +437 -437
  102. package/src/SvForm.test.ts +411 -411
  103. package/src/SvGrid.types.ts +2092 -2092
  104. package/src/SvGridCellEditor.svelte +748 -744
  105. package/src/SvGridChart.svelte +1716 -1716
  106. package/src/SvGridChartPanel.svelte +485 -485
  107. package/src/SvGridChartView.svelte +70 -70
  108. package/src/SvGridDropdown.svelte +728 -728
  109. package/src/SvGridSelect.svelte +270 -270
  110. package/src/SvGroupCell.svelte +116 -116
  111. package/src/SvListBox.svelte +334 -334
  112. package/src/SvMaskedInput.svelte +122 -122
  113. package/src/SvMenu.svelte +124 -124
  114. package/src/SvMenu.test.ts +97 -97
  115. package/src/SvMenuList.svelte +146 -146
  116. package/src/SvMultiSelect.svelte +293 -293
  117. package/src/SvNumberInput.svelte +172 -172
  118. package/src/SvOtpInput.svelte +158 -158
  119. package/src/SvPasswordInput.svelte +151 -151
  120. package/src/SvPhoneInput.svelte +133 -133
  121. package/src/SvPopover.svelte +197 -197
  122. package/src/SvProgress.svelte +116 -116
  123. package/src/SvRadioGroup.svelte +107 -107
  124. package/src/SvRating.svelte +112 -112
  125. package/src/SvResult.svelte +73 -73
  126. package/src/SvRichText.svelte +211 -211
  127. package/src/SvRowGroupPanel.svelte +170 -170
  128. package/src/SvScrollArea.svelte +61 -61
  129. package/src/SvSlider.svelte +203 -203
  130. package/src/SvSwitchButton.svelte +108 -108
  131. package/src/SvTagsInput.svelte +115 -115
  132. package/src/SvTextInput.svelte +147 -147
  133. package/src/SvTimePicker.svelte +245 -245
  134. package/src/SvToaster.svelte +159 -159
  135. package/src/SvToaster.test.ts +95 -95
  136. package/src/SvToggleButton.svelte +85 -85
  137. package/src/SvTooltip.svelte +161 -161
  138. package/src/SvTour.svelte +208 -208
  139. package/src/SvTree.svelte +444 -444
  140. package/src/SvTreeSelect.svelte +239 -239
  141. package/src/a11y/dismissable.test.ts +119 -119
  142. package/src/a11y/dismissable.ts +114 -114
  143. package/src/a11y.contract.test.ts +49 -49
  144. package/src/a11y.test.ts +59 -59
  145. package/src/a11y.ts +61 -61
  146. package/src/ai.test.ts +502 -502
  147. package/src/ai.ts +1419 -1419
  148. package/src/build-api.coverage.test.ts +633 -633
  149. package/src/build-api.ts +846 -846
  150. package/src/builtin-editors.grid.test.ts +83 -83
  151. package/src/cell-formatting.ts +171 -171
  152. package/src/cell-render.test.ts +513 -513
  153. package/src/cell-render.ts +496 -496
  154. package/src/cell-values.ts +148 -148
  155. package/src/chart-export.ts +202 -202
  156. package/src/chart-view.svelte.ts +36 -36
  157. package/src/chart.ts +2321 -2321
  158. package/src/collaboration.test.ts +104 -104
  159. package/src/collaboration.ts +167 -167
  160. package/src/column-groups.ts +78 -78
  161. package/src/core.performance.test.ts +30 -30
  162. package/src/core.ts +1865 -1865
  163. package/src/createAutocomplete.svelte.ts +132 -132
  164. package/src/createCombobox.svelte.ts +191 -191
  165. package/src/createCountryInput.svelte.ts +157 -157
  166. package/src/createDropdownList.svelte.ts +168 -168
  167. package/src/createForm.svelte.ts +386 -386
  168. package/src/createGrid.svelte.ts +42 -42
  169. package/src/createGrid.test.ts +10 -10
  170. package/src/createGridState.svelte.ts +17 -17
  171. package/src/createListbox.svelte.ts +250 -250
  172. package/src/createMenu.svelte.ts +224 -224
  173. package/src/createPopoverSelect.svelte.ts +213 -213
  174. package/src/createSlider.svelte.ts +191 -191
  175. package/src/createTooltip.svelte.ts +144 -144
  176. package/src/createTree.svelte.ts +322 -322
  177. package/src/datetime/date-core.ts +206 -206
  178. package/src/datetime/date-restrict.ts +61 -61
  179. package/src/datetime/timezone.ts +135 -135
  180. package/src/dock-manager-model.ts +596 -596
  181. package/src/dock-model.ts +374 -374
  182. package/src/editing.test.ts +974 -974
  183. package/src/editing.ts +618 -609
  184. package/src/editor-contract.ts +171 -171
  185. package/src/editor-registry.grid.test.ts +144 -144
  186. package/src/editor-registry.ts +122 -122
  187. package/src/export-data-api.test.ts +126 -126
  188. package/src/export-format.test.ts +107 -107
  189. package/src/export-format.ts +601 -601
  190. package/src/filter-operators.ts +160 -160
  191. package/src/filtering/excel-filters.ts +325 -325
  192. package/src/flex-render.ts +3 -3
  193. package/src/form-field.ts +127 -127
  194. package/src/group-display.test.ts +167 -167
  195. package/src/group-display.ts +200 -200
  196. package/src/headless.ts +87 -87
  197. package/src/js-scroller.svelte.ts +173 -173
  198. package/src/keyboard-handlers.ts +270 -270
  199. package/src/keyboard.test.ts +59 -59
  200. package/src/keyboard.ts +97 -97
  201. package/src/list-nav.test.ts +49 -49
  202. package/src/list-nav.ts +29 -29
  203. package/src/list-option.test.ts +56 -56
  204. package/src/list-option.ts +179 -179
  205. package/src/menus.ts +597 -597
  206. package/src/merge-objects.ts +48 -48
  207. package/src/overlays.test.ts +90 -90
  208. package/src/positioning.ts +268 -268
  209. package/src/render-component.ts +28 -28
  210. package/src/row-drag-touch.ts +226 -211
  211. package/src/row-drag.test.ts +401 -353
  212. package/src/row-drag.ts +419 -415
  213. package/src/row-resize.test.ts +524 -524
  214. package/src/row-resize.ts +228 -228
  215. package/src/scheduler-ical.ts +181 -181
  216. package/src/scheduler-model.test.ts +562 -562
  217. package/src/scheduler-model.ts +873 -873
  218. package/src/selection.test.ts +885 -885
  219. package/src/server-data-source.test.ts +383 -383
  220. package/src/server-data-source.ts +469 -469
  221. package/src/sparkline.test.ts +68 -68
  222. package/src/sparkline.ts +169 -169
  223. package/src/spreadsheet.test.ts +488 -488
  224. package/src/spreadsheet.ts +312 -312
  225. package/src/static-functions.ts +11 -11
  226. package/src/subscribe.ts +38 -38
  227. package/src/summaries.ts +113 -113
  228. package/src/svgrid-wrapper.types.ts +563 -563
  229. package/src/svgrid.async-editor-options.test.ts +273 -273
  230. package/src/svgrid.auto-row-height.test.ts +204 -204
  231. package/src/svgrid.behavior.test.ts +910 -910
  232. package/src/svgrid.charting.test.ts +534 -534
  233. package/src/svgrid.comments-autocomplete.test.ts +127 -127
  234. package/src/svgrid.context-menu.test.ts +147 -147
  235. package/src/svgrid.features.test.ts +157 -157
  236. package/src/svgrid.filter-depth.test.ts +163 -163
  237. package/src/svgrid.filter-menu-listbox.svelte.test.ts +382 -377
  238. package/src/svgrid.filter-menu-scroll.test.ts +112 -112
  239. package/src/svgrid.grand-total.test.ts +188 -188
  240. package/src/svgrid.group-display-mode.test.ts +171 -171
  241. package/src/svgrid.group-footers.test.ts +121 -121
  242. package/src/svgrid.group-pagination.test.ts +153 -153
  243. package/src/svgrid.new-features.wrapper.test.ts +251 -251
  244. package/src/svgrid.tree-data.test.ts +186 -186
  245. package/src/svgrid.wrapper.test.ts +63 -63
  246. package/src/svgriddropdown.async-panel.svelte.test.ts +195 -195
  247. package/src/test-setup.ts +62 -62
  248. package/src/themes/index.ts +288 -288
  249. package/src/toast-store.svelte.ts +250 -250
  250. package/src/toast-store.test.ts +147 -147
  251. package/src/tree-row-model.test.ts +168 -168
  252. package/src/ui-buttons.test.ts +144 -144
  253. package/src/ui-inputs.test.ts +118 -118
  254. package/src/ui-localization.test.ts +113 -113
  255. package/src/ui-range.test.ts +70 -70
  256. package/src/ui-selection.test.ts +155 -155
  257. package/src/ui-tier1.test.ts +142 -142
  258. package/src/virtual.test.ts +88 -88
  259. package/src/virtualization/column-virtualizer.test.ts +27 -27
  260. package/src/virtualization/column-virtualizer.ts +30 -30
  261. package/src/virtualization/svelte-virtualizer.svelte.ts +26 -26
  262. package/src/virtualization/types.ts +30 -30
  263. package/src/virtualization/virtualizer.test.ts +47 -47
  264. package/src/virtualization/virtualizer.ts +322 -322
@@ -1,211 +1,226 @@
1
- // Touch / pointer row dragging (#66).
2
- //
3
- // HTML5 drag events do not exist for touch input on iOS Safari or Android
4
- // Chrome - `dragstart` never fires - so the drag path in `row-drag.ts` is dead
5
- // on a phone. This is a second INPUT path onto the same model: it populates the
6
- // same cross-grid `bus` and the same `rowDropIndex` / `rowDropSide`, and commits
7
- // through the same `commitDropOnRow`, so reorder, cross-grid hand-off and the
8
- // `onRowDragEnd` payload cannot drift between mouse and touch.
9
- //
10
- // Loaded with `import()` on the first touch that lands on a draggable row, so
11
- // desktop users never download it. The 350 ms long press below more than covers
12
- // the fetch, and the drag cannot begin before it elapses anyway.
13
- //
14
- // A touch that starts on a row must still be able to SCROLL the grid, so the
15
- // drag only begins after a long press; moving before that threshold hands the
16
- // gesture back to the browser.
17
-
18
- /** Everything the touch path needs from the `createRowDrag` closure. */
19
- export type TouchDragDeps = {
20
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
21
- ctx: any;
22
- managed: () => boolean;
23
- originalAt: (rowIndex: number) => unknown;
24
- commitDropOnRow: (rowIndex: number, side: "before" | "after") => void;
25
- clearIndicators: () => void;
26
- cancelPendingLeave: () => void;
27
- /** Publish the dragged row on the shared cross-grid bus. */
28
- openBus: (row: unknown) => void;
29
- closeBus: () => void;
30
- };
31
-
32
- const LONG_PRESS_MS = 350;
33
- /** Movement (px) before the long press is treated as a scroll instead. */
34
- const SCROLL_SLOP_PX = 10;
35
- /** Distance from an edge (px) at which the grid starts auto-scrolling. */
36
- const EDGE_PX = 48;
37
- const EDGE_SPEED_PX = 12;
38
-
39
- type TouchState = {
40
- pointerId: number;
41
- rowIndex: number;
42
- startX: number;
43
- startY: number;
44
- started: boolean;
45
- timer: ReturnType<typeof setTimeout> | null;
46
- scrollFrame: number | null;
47
- scroller: HTMLElement | null;
48
- root: HTMLElement | null;
49
- };
50
-
51
- /** What {@link createTouchDrag} returns. */
52
- export type TouchDragHandle = {
53
- start: (e: PointerEvent, rowIndex: number) => void;
54
- destroy: () => void;
55
- };
56
-
57
- export function createTouchDrag(deps: TouchDragDeps): TouchDragHandle {
58
- const { ctx } = deps;
59
- let touch: TouchState | null = null;
60
-
61
- /** Resolve the row under a viewport point, and which half of it. */
62
- function rowAtPoint(x: number, y: number): { index: number; side: "before" | "after" } | null {
63
- const el = document.elementFromPoint(x, y) as HTMLElement | null;
64
- const tr = el?.closest?.("tr.sv-grid-row") as HTMLElement | null;
65
- if (!tr) return null;
66
- const marker = tr.querySelector<HTMLElement>("[data-svgrid-row]");
67
- const raw = marker?.dataset.svgridRow;
68
- if (raw == null) return null;
69
- const index = Number(raw);
70
- if (!Number.isFinite(index)) return null;
71
- const rect = tr.getBoundingClientRect();
72
- return { index, side: y < rect.top + rect.height / 2 ? "before" : "after" };
73
- }
74
-
75
- function stopEdgeScroll() {
76
- if (touch?.scrollFrame != null) {
77
- cancelAnimationFrame(touch.scrollFrame);
78
- touch.scrollFrame = null;
79
- }
80
- }
81
-
82
- /** Keep scrolling while the finger is parked near the top or bottom edge. */
83
- function edgeScroll(clientY: number) {
84
- const scroller = touch?.scroller;
85
- if (!touch || !scroller) return;
86
- const rect = scroller.getBoundingClientRect();
87
- let dy = 0;
88
- if (clientY < rect.top + EDGE_PX) dy = -EDGE_SPEED_PX;
89
- else if (clientY > rect.bottom - EDGE_PX) dy = EDGE_SPEED_PX;
90
- stopEdgeScroll();
91
- if (dy === 0) return;
92
- const step = () => {
93
- if (!touch?.started) return;
94
- scroller.scrollTop += dy;
95
- // Re-hit-test as rows move under a stationary finger, otherwise the
96
- // indicator freezes on whichever row happened to be there at touch time.
97
- const hit = rowAtPoint(touch.startX, clientY);
98
- if (hit) {
99
- ctx.rowDropIndex = hit.index;
100
- ctx.rowDropSide = hit.side;
101
- }
102
- touch.scrollFrame = requestAnimationFrame(step);
103
- };
104
- touch.scrollFrame = requestAnimationFrame(step);
105
- }
106
-
107
- // `pointermove.preventDefault()` does NOT stop iOS scrolling; only a
108
- // non-passive `touchmove` does. Registered for the life of the drag.
109
- function blockTouchScroll(e: TouchEvent) {
110
- if (touch?.started) e.preventDefault();
111
- }
112
-
113
- function detach() {
114
- window.removeEventListener("pointermove", onPointerMove);
115
- window.removeEventListener("pointerup", onPointerUp);
116
- window.removeEventListener("pointercancel", onPointerCancel);
117
- window.removeEventListener("touchmove", blockTouchScroll);
118
- }
119
-
120
- function end(commit: boolean) {
121
- if (!touch) return;
122
- const t = touch;
123
- if (t.timer) clearTimeout(t.timer);
124
- stopEdgeScroll();
125
- touch = null;
126
- detach();
127
- t.root?.classList.remove("sv-grid-row-drag-active");
128
- if (!t.started) return;
129
- if (commit && ctx.rowDropIndex != null) {
130
- deps.commitDropOnRow(
131
- ctx.rowDropIndex as number,
132
- (ctx.rowDropSide ?? "before") as "before" | "after",
133
- );
134
- } else {
135
- deps.clearIndicators();
136
- deps.closeBus();
137
- }
138
- }
139
-
140
- function onPointerMove(e: PointerEvent) {
141
- if (!touch || e.pointerId !== touch.pointerId) return;
142
- if (!touch.started) {
143
- // Still inside the long press: a real move means the user is scrolling.
144
- const moved = Math.abs(e.clientY - touch.startY) + Math.abs(e.clientX - touch.startX);
145
- if (moved > SCROLL_SLOP_PX) end(false);
146
- return;
147
- }
148
- e.preventDefault();
149
- const hit = rowAtPoint(e.clientX, e.clientY);
150
- if (hit) {
151
- deps.cancelPendingLeave();
152
- ctx.rowDropIndex = hit.index;
153
- ctx.rowDropSide = hit.side;
154
- }
155
- touch.startX = e.clientX;
156
- edgeScroll(e.clientY);
157
- }
158
-
159
- function onPointerUp(e: PointerEvent) {
160
- if (!touch || e.pointerId !== touch.pointerId) return;
161
- end(true);
162
- }
163
-
164
- function onPointerCancel(e: PointerEvent) {
165
- if (!touch || e.pointerId !== touch.pointerId) return;
166
- end(false);
167
- }
168
-
169
- return {
170
- /** Begin a press that may become a drag. Caller has already checked touch. */
171
- start(e: PointerEvent, rowIndex: number) {
172
- if (!deps.managed() || touch) return;
173
- const row = deps.originalAt(rowIndex);
174
- if (row == null) return;
175
- const target = e.currentTarget as HTMLElement | null;
176
- touch = {
177
- pointerId: e.pointerId,
178
- rowIndex,
179
- startX: e.clientX,
180
- startY: e.clientY,
181
- started: false,
182
- timer: null,
183
- scrollFrame: null,
184
- scroller: target?.closest?.<HTMLElement>(".sv-grid-container") ?? null,
185
- root: target?.closest?.<HTMLElement>(".sv-grid-root") ?? null,
186
- };
187
- window.addEventListener("pointermove", onPointerMove, { passive: false });
188
- window.addEventListener("pointerup", onPointerUp);
189
- window.addEventListener("pointercancel", onPointerCancel);
190
- window.addEventListener("touchmove", blockTouchScroll, { passive: false });
191
- touch.timer = setTimeout(() => {
192
- if (!touch) return;
193
- touch.started = true;
194
- touch.timer = null;
195
- touch.root?.classList.add("sv-grid-row-drag-active");
196
- try {
197
- target?.setPointerCapture?.(e.pointerId);
198
- } catch {
199
- // Not every environment implements pointer capture (tests, old WebViews).
200
- }
201
- deps.openBus(row);
202
- ctx.rowDragActive = true;
203
- ctx.rowDropIndex = rowIndex;
204
- ctx.rowDropSide = "before";
205
- }, LONG_PRESS_MS);
206
- },
207
- destroy() {
208
- end(false);
209
- },
210
- };
211
- }
1
+ // Touch / pointer row dragging (#66).
2
+ //
3
+ // HTML5 drag events do not exist for touch input on iOS Safari or Android
4
+ // Chrome - `dragstart` never fires - so the drag path in `row-drag.ts` is dead
5
+ // on a phone. This is a second INPUT path onto the same model: it populates the
6
+ // same cross-grid `bus` and the same `rowDropIndex` / `rowDropSide`, and commits
7
+ // through the same `commitDropOnRow`, so reorder, cross-grid hand-off and the
8
+ // `onRowDragEnd` payload cannot drift between mouse and touch.
9
+ //
10
+ // Loaded with `import()` on the first touch that lands on a draggable row, so
11
+ // desktop users never download it. The 350 ms long press below absorbs the
12
+ // fetch rather than following it: `start` takes the pointerdown timestamp and
13
+ // subtracts the time already spent waiting, so a fetch that finishes inside the
14
+ // press costs the user nothing.
15
+ //
16
+ // A touch that starts on a row must still be able to SCROLL the grid, so the
17
+ // drag only begins after a long press; moving before that threshold hands the
18
+ // gesture back to the browser.
19
+
20
+ /** Everything the touch path needs from the `createRowDrag` closure. */
21
+ export type TouchDragDeps = {
22
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
23
+ ctx: any;
24
+ managed: () => boolean;
25
+ originalAt: (rowIndex: number) => unknown;
26
+ commitDropOnRow: (rowIndex: number, side: "before" | "after") => void;
27
+ clearIndicators: () => void;
28
+ cancelPendingLeave: () => void;
29
+ /** Publish the dragged row on the shared cross-grid bus. */
30
+ openBus: (row: unknown) => void;
31
+ closeBus: () => void;
32
+ };
33
+
34
+ const LONG_PRESS_MS = 350;
35
+ /** Movement (px) before the long press is treated as a scroll instead. */
36
+ const SCROLL_SLOP_PX = 10;
37
+ /** Distance from an edge (px) at which the grid starts auto-scrolling. */
38
+ const EDGE_PX = 48;
39
+ const EDGE_SPEED_PX = 12;
40
+
41
+ type TouchState = {
42
+ pointerId: number;
43
+ rowIndex: number;
44
+ startX: number;
45
+ startY: number;
46
+ started: boolean;
47
+ timer: ReturnType<typeof setTimeout> | null;
48
+ scrollFrame: number | null;
49
+ scroller: HTMLElement | null;
50
+ root: HTMLElement | null;
51
+ };
52
+
53
+ /** What {@link createTouchDrag} returns. */
54
+ export type TouchDragHandle = {
55
+ /**
56
+ * @param pressStartedAt `performance.now()` at the pointerdown that began
57
+ * this press. Defaults to now. See the long-press comment in `start`.
58
+ */
59
+ start: (e: PointerEvent, rowIndex: number, pressStartedAt?: number) => void;
60
+ destroy: () => void;
61
+ };
62
+
63
+ export function createTouchDrag(deps: TouchDragDeps): TouchDragHandle {
64
+ const { ctx } = deps;
65
+ let touch: TouchState | null = null;
66
+
67
+ /** Resolve the row under a viewport point, and which half of it. */
68
+ function rowAtPoint(x: number, y: number): { index: number; side: "before" | "after" } | null {
69
+ const el = document.elementFromPoint(x, y) as HTMLElement | null;
70
+ const tr = el?.closest?.("tr.sv-grid-row") as HTMLElement | null;
71
+ if (!tr) return null;
72
+ const marker = tr.querySelector<HTMLElement>("[data-svgrid-row]");
73
+ const raw = marker?.dataset.svgridRow;
74
+ if (raw == null) return null;
75
+ const index = Number(raw);
76
+ if (!Number.isFinite(index)) return null;
77
+ const rect = tr.getBoundingClientRect();
78
+ return { index, side: y < rect.top + rect.height / 2 ? "before" : "after" };
79
+ }
80
+
81
+ function stopEdgeScroll() {
82
+ if (touch?.scrollFrame != null) {
83
+ cancelAnimationFrame(touch.scrollFrame);
84
+ touch.scrollFrame = null;
85
+ }
86
+ }
87
+
88
+ /** Keep scrolling while the finger is parked near the top or bottom edge. */
89
+ function edgeScroll(clientY: number) {
90
+ const scroller = touch?.scroller;
91
+ if (!touch || !scroller) return;
92
+ const rect = scroller.getBoundingClientRect();
93
+ let dy = 0;
94
+ if (clientY < rect.top + EDGE_PX) dy = -EDGE_SPEED_PX;
95
+ else if (clientY > rect.bottom - EDGE_PX) dy = EDGE_SPEED_PX;
96
+ stopEdgeScroll();
97
+ if (dy === 0) return;
98
+ const step = () => {
99
+ if (!touch?.started) return;
100
+ scroller.scrollTop += dy;
101
+ // Re-hit-test as rows move under a stationary finger, otherwise the
102
+ // indicator freezes on whichever row happened to be there at touch time.
103
+ const hit = rowAtPoint(touch.startX, clientY);
104
+ if (hit) {
105
+ ctx.rowDropIndex = hit.index;
106
+ ctx.rowDropSide = hit.side;
107
+ }
108
+ touch.scrollFrame = requestAnimationFrame(step);
109
+ };
110
+ touch.scrollFrame = requestAnimationFrame(step);
111
+ }
112
+
113
+ // `pointermove.preventDefault()` does NOT stop iOS scrolling; only a
114
+ // non-passive `touchmove` does. Registered for the life of the drag.
115
+ function blockTouchScroll(e: TouchEvent) {
116
+ if (touch?.started) e.preventDefault();
117
+ }
118
+
119
+ function detach() {
120
+ window.removeEventListener("pointermove", onPointerMove);
121
+ window.removeEventListener("pointerup", onPointerUp);
122
+ window.removeEventListener("pointercancel", onPointerCancel);
123
+ window.removeEventListener("touchmove", blockTouchScroll);
124
+ }
125
+
126
+ function end(commit: boolean) {
127
+ if (!touch) return;
128
+ const t = touch;
129
+ if (t.timer) clearTimeout(t.timer);
130
+ stopEdgeScroll();
131
+ touch = null;
132
+ detach();
133
+ t.root?.classList.remove("sv-grid-row-drag-active");
134
+ if (!t.started) return;
135
+ if (commit && ctx.rowDropIndex != null) {
136
+ deps.commitDropOnRow(
137
+ ctx.rowDropIndex as number,
138
+ (ctx.rowDropSide ?? "before") as "before" | "after",
139
+ );
140
+ } else {
141
+ deps.clearIndicators();
142
+ deps.closeBus();
143
+ }
144
+ }
145
+
146
+ function onPointerMove(e: PointerEvent) {
147
+ if (!touch || e.pointerId !== touch.pointerId) return;
148
+ if (!touch.started) {
149
+ // Still inside the long press: a real move means the user is scrolling.
150
+ const moved = Math.abs(e.clientY - touch.startY) + Math.abs(e.clientX - touch.startX);
151
+ if (moved > SCROLL_SLOP_PX) end(false);
152
+ return;
153
+ }
154
+ e.preventDefault();
155
+ const hit = rowAtPoint(e.clientX, e.clientY);
156
+ if (hit) {
157
+ deps.cancelPendingLeave();
158
+ ctx.rowDropIndex = hit.index;
159
+ ctx.rowDropSide = hit.side;
160
+ }
161
+ touch.startX = e.clientX;
162
+ edgeScroll(e.clientY);
163
+ }
164
+
165
+ function onPointerUp(e: PointerEvent) {
166
+ if (!touch || e.pointerId !== touch.pointerId) return;
167
+ end(true);
168
+ }
169
+
170
+ function onPointerCancel(e: PointerEvent) {
171
+ if (!touch || e.pointerId !== touch.pointerId) return;
172
+ end(false);
173
+ }
174
+
175
+ return {
176
+ /** Begin a press that may become a drag. Caller has already checked touch. */
177
+ start(e: PointerEvent, rowIndex: number, pressStartedAt?: number) {
178
+ if (!deps.managed() || touch) return;
179
+ const row = deps.originalAt(rowIndex);
180
+ if (row == null) return;
181
+ const target = e.currentTarget as HTMLElement | null;
182
+ touch = {
183
+ pointerId: e.pointerId,
184
+ rowIndex,
185
+ startX: e.clientX,
186
+ startY: e.clientY,
187
+ started: false,
188
+ timer: null,
189
+ scrollFrame: null,
190
+ scroller: target?.closest?.<HTMLElement>(".sv-grid-container") ?? null,
191
+ root: target?.closest?.<HTMLElement>(".sv-grid-root") ?? null,
192
+ };
193
+ window.addEventListener("pointermove", onPointerMove, { passive: false });
194
+ window.addEventListener("pointerup", onPointerUp);
195
+ window.addEventListener("pointercancel", onPointerCancel);
196
+ window.addEventListener("touchmove", blockTouchScroll, { passive: false });
197
+ // The long press is measured from when the finger LANDED, not from when
198
+ // this module finished loading. `row-drag.ts` fetches it with import() on
199
+ // the first touch, and that fetch is not free - measured at ~190 ms cold.
200
+ // Starting the clock here would make the real cost `fetch + 350 ms`, so a
201
+ // user on a slow connection would have to hold noticeably longer for
202
+ // their first drag than for every one after it. Subtracting the elapsed
203
+ // time keeps it at 350 ms from the finger's point of view, which is what
204
+ // the comment at the top of this file has always claimed.
205
+ const waited = pressStartedAt == null ? 0 : Math.max(0, performance.now() - pressStartedAt);
206
+ touch.timer = setTimeout(() => {
207
+ if (!touch) return;
208
+ touch.started = true;
209
+ touch.timer = null;
210
+ touch.root?.classList.add("sv-grid-row-drag-active");
211
+ try {
212
+ target?.setPointerCapture?.(e.pointerId);
213
+ } catch {
214
+ // Not every environment implements pointer capture (tests, old WebViews).
215
+ }
216
+ deps.openBus(row);
217
+ ctx.rowDragActive = true;
218
+ ctx.rowDropIndex = rowIndex;
219
+ ctx.rowDropSide = "before";
220
+ }, Math.max(0, LONG_PRESS_MS - waited));
221
+ },
222
+ destroy() {
223
+ end(false);
224
+ },
225
+ };
226
+ }