@svgrid/grid 3.0.2 → 3.0.4

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 (281) hide show
  1. package/README.md +2 -1
  2. package/dist/GridMenus.svelte +44 -2
  3. package/dist/SvChartMenu.svelte +69 -0
  4. package/dist/SvChartMenu.svelte.d.ts +11 -0
  5. package/dist/SvChartPanes.svelte +113 -0
  6. package/dist/SvChartPanes.svelte.d.ts +20 -0
  7. package/dist/SvGrid.controller.svelte.d.ts +110 -39
  8. package/dist/SvGrid.controller.svelte.js +655 -200
  9. package/dist/SvGrid.css +105 -2
  10. package/dist/SvGrid.svelte +386 -514
  11. package/dist/SvGrid.types.d.ts +210 -9
  12. package/dist/SvGridCellEditor.svelte +17 -0
  13. package/dist/SvGridChart.svelte +2257 -329
  14. package/dist/SvGridChart.svelte.d.ts +6 -106
  15. package/dist/SvGridChart.types.d.ts +338 -0
  16. package/dist/SvGridChart.types.js +1 -0
  17. package/dist/SvGridChartBuilder.svelte +323 -0
  18. package/dist/SvGridChartBuilder.svelte.d.ts +34 -0
  19. package/dist/SvGridChartPanel.svelte +221 -275
  20. package/dist/SvGridChartPickers.svelte +417 -0
  21. package/dist/SvGridChartPickers.svelte.d.ts +70 -0
  22. package/dist/SvGridChartView.svelte +13 -14
  23. package/dist/SvModal.svelte +7 -1
  24. package/dist/SvModal.svelte.d.ts +5 -0
  25. package/dist/ai.d.ts +40 -8
  26. package/dist/ai.js +135 -24
  27. package/dist/aria.d.ts +11 -0
  28. package/dist/build-api.js +108 -22
  29. package/dist/cdn/GridMenus-7kbpnnBW.js +635 -0
  30. package/dist/cdn/GridMenus-BL7ZgQvU.js +636 -0
  31. package/dist/cdn/SvChartMenu-Bl6PBbkT.js +58 -0
  32. package/dist/cdn/SvChartMenu-FBSMINA6.js +59 -0
  33. package/dist/cdn/{SvDateRangeInput-DMLKmGEc.js → SvDateRangeInput-BeU_TkbQ.js} +15 -14
  34. package/dist/cdn/{SvDateRangeInput-CaOuMs8O.js → SvDateRangeInput-yX8vzleW.js} +38 -37
  35. package/dist/cdn/{SvDateTimePicker-sonaH0oh.js → SvDateTimePicker-DQwt4UAs.js} +139 -180
  36. package/dist/cdn/SvDateTimePicker-vNU6bZ-q.js +775 -0
  37. package/dist/cdn/{SvGridCellEditor-CjEJMJKc.js → SvGridCellEditor-B1p-vCK5.js} +188 -178
  38. package/dist/cdn/{SvGridCellEditor-BWnTi2N7.js → SvGridCellEditor-D_0q4xAu.js} +98 -88
  39. package/dist/cdn/SvGridChart-C3EWAZaM.js +3574 -0
  40. package/dist/cdn/SvGridChart-DzLkSwCH.js +3572 -0
  41. package/dist/cdn/SvGridChartBuilder-BE2T1ykB.js +783 -0
  42. package/dist/cdn/SvGridChartBuilder-CfII62sZ.js +784 -0
  43. package/dist/cdn/SvGridChartPanel-CCX5_Wgd.js +675 -0
  44. package/dist/cdn/SvGridChartPanel-xf4sWVTo.js +699 -0
  45. package/dist/cdn/SvGridChartView-CfuXmY5I.js +48 -0
  46. package/dist/cdn/SvGridChartView-DYwabQWj.js +47 -0
  47. package/dist/cdn/SvMenuList-CLN8OuIK.js +452 -0
  48. package/dist/cdn/SvMenuList-DkGhNKjf.js +451 -0
  49. package/dist/cdn/SvModal-BAE-pjZX.js +396 -0
  50. package/dist/cdn/SvModal-CIZWCcad.js +397 -0
  51. package/dist/cdn/chart-Dz7SqMXH.js +4341 -0
  52. package/dist/cdn/chart-export-BXqueeWJ.js +135 -0
  53. package/dist/cdn/chart-export-pdf-DONFUnNY.js +110 -0
  54. package/dist/cdn/chart-panel-messages-CUbf2R4i.js +1052 -0
  55. package/dist/cdn/chart-panel-messages-CmNrMdsr.js +1053 -0
  56. package/dist/cdn/chart-summary-BJW_tg_X.js +138 -0
  57. package/dist/cdn/chart-trend-CaN9mDEV.js +50 -0
  58. package/dist/cdn/chart-validate-HcOVFAfJ.js +188 -0
  59. package/dist/cdn/{column-resize-DsfNXMom.js → column-resize-DpmOLfRp.js} +3 -3
  60. package/dist/cdn/{date-format-BnnHlqGw.js → date-format-CtquV-p3.js} +583 -603
  61. package/dist/cdn/{date-format-BNii4zeD.js → date-format-D6KFzU_W.js} +347 -367
  62. package/dist/cdn/dismissable-DAHetSNk.js +44 -0
  63. package/dist/cdn/editor-contract-LXgQJAgd.js +22 -0
  64. package/dist/cdn/focus-trap-BBOQYBma.js +65 -0
  65. package/dist/cdn/{row-resize-BRcimkUT.js → row-resize-niQCp040.js} +44 -36
  66. package/dist/cdn/{src-DNbz94Ct.js → src-BKhZ6eXd.js} +10227 -9787
  67. package/dist/cdn/{src-BJ2opd0j.js → src-V1uu8iE9.js} +6568 -6128
  68. package/dist/cdn/svgrid.js +26 -15
  69. package/dist/cdn/svgrid.svelte-external.js +26 -15
  70. package/dist/chart-axes.d.ts +53 -0
  71. package/dist/chart-axes.js +351 -0
  72. package/dist/chart-cartesian.d.ts +88 -0
  73. package/dist/chart-cartesian.js +1862 -0
  74. package/dist/chart-decimate.d.ts +51 -0
  75. package/dist/chart-decimate.js +199 -0
  76. package/dist/chart-export-pdf.d.ts +47 -0
  77. package/dist/chart-export-pdf.js +187 -0
  78. package/dist/chart-export.d.ts +2 -1
  79. package/dist/chart-export.js +18 -3
  80. package/dist/chart-financial.d.ts +91 -0
  81. package/dist/chart-financial.js +175 -0
  82. package/dist/chart-flow.d.ts +10 -0
  83. package/dist/chart-flow.js +298 -0
  84. package/dist/chart-format.d.ts +18 -0
  85. package/dist/chart-format.js +65 -0
  86. package/dist/chart-grid.d.ts +8 -0
  87. package/dist/chart-grid.js +190 -0
  88. package/dist/chart-hierarchy.d.ts +17 -0
  89. package/dist/chart-hierarchy.js +146 -0
  90. package/dist/chart-indicators.d.ts +115 -0
  91. package/dist/chart-indicators.js +389 -0
  92. package/dist/chart-messages.d.ts +95 -0
  93. package/dist/chart-messages.js +92 -0
  94. package/dist/chart-motion.d.ts +16 -0
  95. package/dist/chart-motion.js +141 -0
  96. package/dist/chart-panel-messages.d.ts +265 -0
  97. package/dist/chart-panel-messages.js +285 -0
  98. package/dist/chart-pivot.d.ts +87 -0
  99. package/dist/chart-pivot.js +156 -0
  100. package/dist/chart-polar.d.ts +25 -0
  101. package/dist/chart-polar.js +700 -0
  102. package/dist/chart-samples.d.ts +16 -0
  103. package/dist/chart-samples.js +153 -0
  104. package/dist/chart-scale.d.ts +113 -0
  105. package/dist/chart-scale.js +405 -0
  106. package/dist/chart-stats.d.ts +111 -0
  107. package/dist/chart-stats.js +419 -0
  108. package/dist/chart-stream.d.ts +45 -0
  109. package/dist/chart-stream.js +91 -0
  110. package/dist/chart-summary.d.ts +27 -0
  111. package/dist/chart-summary.js +205 -0
  112. package/dist/chart-sync.svelte.d.ts +32 -0
  113. package/dist/chart-sync.svelte.js +23 -0
  114. package/dist/chart-table.d.ts +35 -0
  115. package/dist/chart-table.js +165 -0
  116. package/dist/chart-trend.d.ts +19 -0
  117. package/dist/chart-trend.js +61 -0
  118. package/dist/chart-types.d.ts +1456 -0
  119. package/dist/chart-types.js +6 -0
  120. package/dist/chart-validate.d.ts +43 -0
  121. package/dist/chart-validate.js +272 -0
  122. package/dist/chart-zoom.d.ts +40 -0
  123. package/dist/chart-zoom.js +145 -0
  124. package/dist/chart.d.ts +186 -865
  125. package/dist/chart.js +460 -2204
  126. package/dist/clipboard.d.ts +1 -0
  127. package/dist/clipboard.js +137 -18
  128. package/dist/column-resize.d.ts +3 -0
  129. package/dist/column-resize.js +4 -1
  130. package/dist/columns.js +3 -0
  131. package/dist/command-context.d.ts +19 -0
  132. package/dist/command-context.js +116 -0
  133. package/dist/core.d.ts +7 -0
  134. package/dist/createPopoverSelect.svelte.d.ts +2 -2
  135. package/dist/editing.d.ts +2 -8
  136. package/dist/editing.js +262 -33
  137. package/dist/fill-patterns.d.ts +0 -5
  138. package/dist/fill-patterns.js +56 -2
  139. package/dist/grid-messages.d.ts +10 -3
  140. package/dist/grid-messages.js +5 -0
  141. package/dist/history.d.ts +83 -0
  142. package/dist/history.js +133 -0
  143. package/dist/index.d.ts +24 -6
  144. package/dist/index.js +20 -2
  145. package/dist/keyboard-handlers.js +88 -31
  146. package/dist/keyboard.d.ts +55 -0
  147. package/dist/keyboard.js +132 -0
  148. package/dist/menus.d.ts +3 -1
  149. package/dist/menus.js +7 -2
  150. package/dist/merges.d.ts +97 -0
  151. package/dist/merges.js +147 -0
  152. package/dist/row-resize.d.ts +3 -0
  153. package/dist/row-resize.js +14 -0
  154. package/dist/scroll-sync.d.ts +1 -0
  155. package/dist/scroll-sync.js +24 -0
  156. package/dist/selection.d.ts +16 -0
  157. package/dist/selection.js +108 -16
  158. package/dist/shortcut-registry.d.ts +145 -0
  159. package/dist/shortcut-registry.js +44 -0
  160. package/dist/svgrid-wrapper.types.d.ts +213 -22
  161. package/dist/virtualization/virtualizer.js +38 -17
  162. package/package.json +6 -1
  163. package/src/GridMenus.svelte +44 -2
  164. package/src/SvChartMenu.svelte +69 -0
  165. package/src/SvChartPanes.svelte +113 -0
  166. package/src/SvChartPanes.test.ts +66 -0
  167. package/src/SvGrid.controller.svelte.ts +629 -172
  168. package/src/SvGrid.css +105 -2
  169. package/src/SvGrid.svelte +386 -514
  170. package/src/SvGrid.types.ts +188 -8
  171. package/src/SvGridCellEditor.svelte +17 -0
  172. package/src/SvGridChart.svelte +2257 -329
  173. package/src/SvGridChart.test.ts +1419 -1
  174. package/src/SvGridChart.types.ts +336 -0
  175. package/src/SvGridChartBuilder.svelte +323 -0
  176. package/src/SvGridChartPanel.svelte +221 -275
  177. package/src/SvGridChartPickers.svelte +417 -0
  178. package/src/SvGridChartView.svelte +13 -14
  179. package/src/SvModal.svelte +7 -1
  180. package/src/SvModal.test.ts +9 -0
  181. package/src/ai.test.ts +57 -2
  182. package/src/ai.ts +166 -22
  183. package/src/aria.d.ts +11 -0
  184. package/src/build-api.coverage.test.ts +184 -0
  185. package/src/build-api.ts +107 -18
  186. package/src/chart-axes.test.ts +573 -0
  187. package/src/chart-axes.ts +342 -0
  188. package/src/chart-cartesian.ts +1863 -0
  189. package/src/chart-decimate.test.ts +249 -0
  190. package/src/chart-decimate.ts +186 -0
  191. package/src/chart-export-pdf.test.ts +91 -0
  192. package/src/chart-export-pdf.ts +207 -0
  193. package/src/chart-export.test.ts +14 -0
  194. package/src/chart-export.ts +13 -3
  195. package/src/chart-financial.ts +208 -0
  196. package/src/chart-flow.ts +286 -0
  197. package/src/chart-format.ts +67 -0
  198. package/src/chart-grid.ts +179 -0
  199. package/src/chart-hierarchy.ts +135 -0
  200. package/src/chart-indicators.test.ts +443 -0
  201. package/src/chart-indicators.ts +384 -0
  202. package/src/chart-messages.test.ts +21 -0
  203. package/src/chart-messages.ts +184 -0
  204. package/src/chart-motion.test.ts +71 -0
  205. package/src/chart-motion.ts +148 -0
  206. package/src/chart-panel-messages.test.ts +29 -0
  207. package/src/chart-panel-messages.ts +550 -0
  208. package/src/chart-pivot.test.ts +54 -0
  209. package/src/chart-pivot.ts +225 -0
  210. package/src/chart-polar.ts +674 -0
  211. package/src/chart-samples.ts +106 -0
  212. package/src/chart-scale.ts +394 -0
  213. package/src/chart-series-types.test.ts +714 -0
  214. package/src/chart-stats.ts +409 -0
  215. package/src/chart-stream.test.ts +75 -0
  216. package/src/chart-stream.ts +114 -0
  217. package/src/chart-summary.test.ts +90 -0
  218. package/src/chart-summary.ts +197 -0
  219. package/src/chart-sync.svelte.ts +41 -0
  220. package/src/chart-table.test.ts +76 -0
  221. package/src/chart-table.ts +178 -0
  222. package/src/chart-trend.ts +47 -0
  223. package/src/chart-types.ts +1309 -0
  224. package/src/chart-validate.test.ts +100 -0
  225. package/src/chart-validate.ts +247 -0
  226. package/src/chart-zoom.test.ts +137 -0
  227. package/src/chart-zoom.ts +124 -0
  228. package/src/chart.coverage.test.ts +81 -0
  229. package/src/chart.test.ts +56 -3
  230. package/src/chart.ts +557 -2784
  231. package/src/clipboard.test.ts +151 -0
  232. package/src/clipboard.ts +145 -16
  233. package/src/column-resize.ts +7 -1
  234. package/src/columns.ts +2 -0
  235. package/src/command-context.test.ts +200 -0
  236. package/src/command-context.ts +137 -0
  237. package/src/core.ts +7 -0
  238. package/src/editing.test.ts +281 -3
  239. package/src/editing.ts +256 -38
  240. package/src/fill-patterns.test.ts +35 -0
  241. package/src/fill-patterns.ts +56 -2
  242. package/src/grid-messages.ts +16 -3
  243. package/src/history.test.ts +196 -0
  244. package/src/history.ts +162 -0
  245. package/src/icon-seam.test.ts +3 -2
  246. package/src/index.ts +150 -2
  247. package/src/keyboard-handlers.coverage.test.ts +126 -6
  248. package/src/keyboard-handlers.ts +91 -30
  249. package/src/keyboard-shortcuts.seam.test.ts +200 -0
  250. package/src/keyboard.test.ts +124 -1
  251. package/src/keyboard.ts +142 -0
  252. package/src/menus.test.ts +26 -0
  253. package/src/menus.ts +10 -3
  254. package/src/merges.test.ts +112 -0
  255. package/src/merges.ts +168 -0
  256. package/src/pivot.grid.test.ts +40 -5
  257. package/src/row-resize.test.ts +44 -0
  258. package/src/row-resize.ts +17 -0
  259. package/src/scroll-sync.test.ts +42 -1
  260. package/src/scroll-sync.ts +25 -0
  261. package/src/selection.multi-range.test.ts +34 -0
  262. package/src/selection.test.ts +30 -2
  263. package/src/selection.ts +111 -19
  264. package/src/shortcut-registry.test.ts +97 -0
  265. package/src/shortcut-registry.ts +181 -0
  266. package/src/svgrid-wrapper.types.ts +184 -13
  267. package/src/svgrid.behavior.test.ts +135 -0
  268. package/src/svgrid.charting.test.ts +513 -3
  269. package/src/svgrid.context-menu.test.ts +31 -0
  270. package/src/svgrid.new-features.wrapper.test.ts +10 -3
  271. package/src/virtualization/virtualizer.ts +39 -17
  272. package/dist/cdn/GridMenus-DyFA-QxA.js +0 -602
  273. package/dist/cdn/GridMenus-M2XP9t0_.js +0 -603
  274. package/dist/cdn/SvDateTimePicker-CCbDZNZB.js +0 -816
  275. package/dist/cdn/SvGridChart-DAb9RbRs.js +0 -1480
  276. package/dist/cdn/SvGridChart-Yl-IXZFO.js +0 -1481
  277. package/dist/cdn/SvGridChartPanel-DayQpDsR.js +0 -834
  278. package/dist/cdn/SvGridChartPanel-aqf8n-vJ.js +0 -810
  279. package/dist/cdn/SvGridChartView-CxfakxR0.js +0 -56
  280. package/dist/cdn/SvGridChartView-Dm_YEx9L.js +0 -55
  281. package/dist/cdn/chart-CKJRCPOk.js +0 -1652
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Merged cells: a rectangle of cells drawn as one, the way a spreadsheet's
3
+ * Merge & Center does. The grid takes them as `mergedCells`, a list of
4
+ * origins with their spans in display indices, and this module is the
5
+ * pure part: an index over them, the questions the renderer, the
6
+ * selection and the keyboard ask, and nothing that touches the DOM.
7
+ *
8
+ * A merge is drawn by its origin td, which takes `rowspan` and `colspan`;
9
+ * the cells it covers are not emitted. Selection rectangles grow to whole
10
+ * merges (a merge is selected as one), the active cell inside a merge is
11
+ * its origin, and an arrow key moving into a merge lands on the origin
12
+ * and moving out of one steps past its far edge.
13
+ */
14
+ export type MergedCell = {
15
+ /** Top-left cell of the merge, display indices. */
16
+ rowIndex: number;
17
+ colIndex: number;
18
+ /** How many rows and columns it covers, 1 or more. */
19
+ rowSpan: number;
20
+ colSpan: number;
21
+ };
22
+ export type MergeIndex = {
23
+ readonly list: ReadonlyArray<MergedCell>;
24
+ /** Every cell a merge covers (origin included) -> its merge. */
25
+ readonly byCell: ReadonlyMap<string, MergedCell>;
26
+ };
27
+ export type MergeRect = {
28
+ minRow: number;
29
+ maxRow: number;
30
+ minCol: number;
31
+ maxCol: number;
32
+ };
33
+ /** An index over the merges, or null when there are none (the common case). */
34
+ export declare function buildMergeIndex(merges: ReadonlyArray<MergedCell> | undefined | null): MergeIndex | null;
35
+ /** The merge covering (r, c), origin included, or undefined. */
36
+ export declare function mergeAt(index: MergeIndex | null, r: number, c: number): MergedCell | undefined;
37
+ /** Whether (r, c) is inside a merge without being its origin. */
38
+ export declare function isCovered(index: MergeIndex | null, r: number, c: number): boolean;
39
+ /** The origin of the merge (r, c) sits in, or (r, c) itself. */
40
+ export declare function originOf(index: MergeIndex | null, r: number, c: number): {
41
+ rowIndex: number;
42
+ colIndex: number;
43
+ };
44
+ /** The far corner of the merge (r, c) sits in, or (r, c) itself. */
45
+ export declare function endOf(index: MergeIndex | null, r: number, c: number): {
46
+ rowIndex: number;
47
+ colIndex: number;
48
+ };
49
+ /**
50
+ * The rectangle grown to hold every merge it touches, to a fixed point: a
51
+ * merge that pokes out of the rectangle pulls the edge out, which can
52
+ * reach another merge, and so on, as a spreadsheet's selection does.
53
+ */
54
+ export declare function expandRectToMerges(index: MergeIndex | null, rect: MergeRect): MergeRect;
55
+ /**
56
+ * Where a move from `from` to `next` really lands. A step into a merge
57
+ * lands on its origin; a step that stays inside the merge the move started
58
+ * in carries on past its far edge (right from a three-wide merge reaches
59
+ * the column after it), clamped to the bounds; a step that then lands in
60
+ * another merge takes that one's origin.
61
+ */
62
+ export declare function stepPastMerge(index: MergeIndex | null, from: {
63
+ rowIndex: number;
64
+ colIndex: number;
65
+ }, next: {
66
+ rowIndex: number;
67
+ colIndex: number;
68
+ }, bounds: {
69
+ maxRow: number;
70
+ maxCol: number;
71
+ }): {
72
+ rowIndex: number;
73
+ colIndex: number;
74
+ };
75
+ /**
76
+ * How a td at (r, c) draws inside a rendered window of rows and columns.
77
+ * A merge is drawn once per window, by the top-left cell of the part of it
78
+ * the window holds: the origin when the origin is in the window (`origin`),
79
+ * else the first covered cell the window shows (`continuation`, which
80
+ * draws the origin's content itself), with the spans clamped to the window
81
+ * so a table never spans into rows it has not drawn. Every other cell of
82
+ * the merge is `skip`: no td, the drawn one covers its slot.
83
+ */
84
+ export type MergeDraw = {
85
+ kind: 'skip';
86
+ } | {
87
+ kind: 'origin' | 'continuation';
88
+ merge: MergedCell;
89
+ rowSpan: number;
90
+ colSpan: number;
91
+ };
92
+ export declare function mergeDrawAt(index: MergeIndex | null, r: number, c: number, window: {
93
+ firstRow: number;
94
+ lastRow: number;
95
+ firstCol: number;
96
+ lastCol: number;
97
+ }): MergeDraw | null;
package/dist/merges.js ADDED
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Merged cells: a rectangle of cells drawn as one, the way a spreadsheet's
3
+ * Merge & Center does. The grid takes them as `mergedCells`, a list of
4
+ * origins with their spans in display indices, and this module is the
5
+ * pure part: an index over them, the questions the renderer, the
6
+ * selection and the keyboard ask, and nothing that touches the DOM.
7
+ *
8
+ * A merge is drawn by its origin td, which takes `rowspan` and `colspan`;
9
+ * the cells it covers are not emitted. Selection rectangles grow to whole
10
+ * merges (a merge is selected as one), the active cell inside a merge is
11
+ * its origin, and an arrow key moving into a merge lands on the origin
12
+ * and moving out of one steps past its far edge.
13
+ */
14
+ const key = (r, c) => `${r}:${c}`;
15
+ /** An index over the merges, or null when there are none (the common case). */
16
+ export function buildMergeIndex(merges) {
17
+ if (!merges || merges.length === 0)
18
+ return null;
19
+ const byCell = new Map();
20
+ const list = [];
21
+ for (const raw of merges) {
22
+ const rowSpan = Math.max(1, Math.floor(raw.rowSpan));
23
+ const colSpan = Math.max(1, Math.floor(raw.colSpan));
24
+ if (rowSpan === 1 && colSpan === 1)
25
+ continue;
26
+ const merge = { rowIndex: raw.rowIndex, colIndex: raw.colIndex, rowSpan, colSpan };
27
+ list.push(merge);
28
+ for (let r = merge.rowIndex; r < merge.rowIndex + rowSpan; r += 1) {
29
+ for (let c = merge.colIndex; c < merge.colIndex + colSpan; c += 1)
30
+ byCell.set(key(r, c), merge);
31
+ }
32
+ }
33
+ return list.length ? { list, byCell } : null;
34
+ }
35
+ /** The merge covering (r, c), origin included, or undefined. */
36
+ export function mergeAt(index, r, c) {
37
+ return index?.byCell.get(key(r, c));
38
+ }
39
+ /** Whether (r, c) is inside a merge without being its origin. */
40
+ export function isCovered(index, r, c) {
41
+ const m = mergeAt(index, r, c);
42
+ return !!m && (m.rowIndex !== r || m.colIndex !== c);
43
+ }
44
+ /** The origin of the merge (r, c) sits in, or (r, c) itself. */
45
+ export function originOf(index, r, c) {
46
+ const m = mergeAt(index, r, c);
47
+ return m ? { rowIndex: m.rowIndex, colIndex: m.colIndex } : { rowIndex: r, colIndex: c };
48
+ }
49
+ /** The far corner of the merge (r, c) sits in, or (r, c) itself. */
50
+ export function endOf(index, r, c) {
51
+ const m = mergeAt(index, r, c);
52
+ return m
53
+ ? { rowIndex: m.rowIndex + m.rowSpan - 1, colIndex: m.colIndex + m.colSpan - 1 }
54
+ : { rowIndex: r, colIndex: c };
55
+ }
56
+ /**
57
+ * The rectangle grown to hold every merge it touches, to a fixed point: a
58
+ * merge that pokes out of the rectangle pulls the edge out, which can
59
+ * reach another merge, and so on, as a spreadsheet's selection does.
60
+ */
61
+ export function expandRectToMerges(index, rect) {
62
+ if (!index)
63
+ return rect;
64
+ let { minRow, maxRow, minCol, maxCol } = rect;
65
+ for (let guard = 0; guard < 64; guard += 1) {
66
+ let grew = false;
67
+ for (const m of index.list) {
68
+ const r2 = m.rowIndex + m.rowSpan - 1;
69
+ const c2 = m.colIndex + m.colSpan - 1;
70
+ const touches = m.rowIndex <= maxRow && r2 >= minRow && m.colIndex <= maxCol && c2 >= minCol;
71
+ if (!touches)
72
+ continue;
73
+ if (m.rowIndex < minRow) {
74
+ minRow = m.rowIndex;
75
+ grew = true;
76
+ }
77
+ if (r2 > maxRow) {
78
+ maxRow = r2;
79
+ grew = true;
80
+ }
81
+ if (m.colIndex < minCol) {
82
+ minCol = m.colIndex;
83
+ grew = true;
84
+ }
85
+ if (c2 > maxCol) {
86
+ maxCol = c2;
87
+ grew = true;
88
+ }
89
+ }
90
+ if (!grew)
91
+ break;
92
+ }
93
+ return minRow === rect.minRow && maxRow === rect.maxRow && minCol === rect.minCol && maxCol === rect.maxCol
94
+ ? rect
95
+ : { minRow, maxRow, minCol, maxCol };
96
+ }
97
+ /**
98
+ * Where a move from `from` to `next` really lands. A step into a merge
99
+ * lands on its origin; a step that stays inside the merge the move started
100
+ * in carries on past its far edge (right from a three-wide merge reaches
101
+ * the column after it), clamped to the bounds; a step that then lands in
102
+ * another merge takes that one's origin.
103
+ */
104
+ export function stepPastMerge(index, from, next, bounds) {
105
+ if (!index)
106
+ return next;
107
+ const target = mergeAt(index, next.rowIndex, next.colIndex);
108
+ if (!target)
109
+ return next;
110
+ const source = mergeAt(index, from.rowIndex, from.colIndex);
111
+ if (source !== target)
112
+ return { rowIndex: target.rowIndex, colIndex: target.colIndex };
113
+ // Still inside the merge the move began in: keep going in that direction.
114
+ const dr = Math.sign(next.rowIndex - from.rowIndex);
115
+ const dc = Math.sign(next.colIndex - from.colIndex);
116
+ if (dr === 0 && dc === 0)
117
+ return { rowIndex: target.rowIndex, colIndex: target.colIndex };
118
+ let rowIndex = next.rowIndex;
119
+ let colIndex = next.colIndex;
120
+ if (dr > 0)
121
+ rowIndex = target.rowIndex + target.rowSpan;
122
+ else if (dr < 0)
123
+ rowIndex = target.rowIndex - 1;
124
+ if (dc > 0)
125
+ colIndex = target.colIndex + target.colSpan;
126
+ else if (dc < 0)
127
+ colIndex = target.colIndex - 1;
128
+ if (rowIndex < 0 || rowIndex > bounds.maxRow || colIndex < 0 || colIndex > bounds.maxCol) {
129
+ // Nowhere further to go: the merge itself stays current.
130
+ return { rowIndex: target.rowIndex, colIndex: target.colIndex };
131
+ }
132
+ const landed = mergeAt(index, rowIndex, colIndex);
133
+ return landed ? { rowIndex: landed.rowIndex, colIndex: landed.colIndex } : { rowIndex, colIndex };
134
+ }
135
+ export function mergeDrawAt(index, r, c, window) {
136
+ const m = mergeAt(index, r, c);
137
+ if (!m)
138
+ return null;
139
+ const anchorRow = Math.max(m.rowIndex, window.firstRow);
140
+ const anchorCol = Math.max(m.colIndex, window.firstCol);
141
+ if (r !== anchorRow || c !== anchorCol)
142
+ return { kind: 'skip' };
143
+ const isOrigin = anchorRow === m.rowIndex && anchorCol === m.colIndex;
144
+ const rowSpan = Math.max(1, Math.min(m.rowIndex + m.rowSpan - r, window.lastRow - r + 1));
145
+ const colSpan = Math.max(1, Math.min(m.colIndex + m.colSpan - c, window.lastCol - c + 1));
146
+ return { kind: isOrigin ? 'origin' : 'continuation', merge: m, rowSpan, colSpan };
147
+ }
@@ -30,6 +30,9 @@ export type RowResizeOptions = {
30
30
  onResize: (rowIndex: number, height: number) => void;
31
31
  /** Optional callback fired during the drag (every pointermove). */
32
32
  onResizeMove?: (rowIndex: number, height: number) => void;
33
+ /** Double-click on the strip: size the row to its content, the
34
+ * spreadsheet gesture. Without it a double-click does nothing. */
35
+ onAutosize?: (rowIndex: number) => void;
33
36
  /** Min row height in px. Default 20. */
34
37
  min?: number;
35
38
  /** Max row height in px. Default 320. */
@@ -29,6 +29,8 @@ const STRIP_CLASS = 'sv-grid-row-resize-handle';
29
29
  export function rowResize(node, opts) {
30
30
  let current = opts;
31
31
  let drag = null;
32
+ /** The last press on a strip, for telling a double-click from two drags. */
33
+ let lastPress = null;
32
34
  function rowIndexOf(tr) {
33
35
  const dataRow = tr.querySelector('[data-svgrid-row]');
34
36
  const val = dataRow?.dataset.svgridRow;
@@ -109,6 +111,18 @@ export function rowResize(node, opts) {
109
111
  return;
110
112
  e.preventDefault();
111
113
  e.stopPropagation();
114
+ // The second press of a double-click: size the row to its content
115
+ // instead of starting another drag. Timed here rather than read from
116
+ // a dblclick event, whose target is the row once the first press's
117
+ // resize has re-rendered the strip; pointerdown carries no count.
118
+ const now = e.timeStamp;
119
+ const again = lastPress !== null && lastPress.rowIndex === rowIndex && now - lastPress.at < 400;
120
+ lastPress = { rowIndex, at: now };
121
+ if (again && current.onAutosize) {
122
+ lastPress = null;
123
+ current.onAutosize(rowIndex);
124
+ return;
125
+ }
112
126
  t.classList.add('is-resizing');
113
127
  drag = {
114
128
  rowIndex,
@@ -5,5 +5,6 @@ export declare function createScrollSync<TFeatures extends TableFeatures = Table
5
5
  hideTooltip: () => void;
6
6
  flushScheduledScrollSync: () => void;
7
7
  scheduleScrollSync: (scrollTop: number, scrollLeft: number) => void;
8
+ syncScrollNow: (scrollTop: number, scrollLeft: number) => void;
8
9
  onBodyScroll: (event: Event) => void;
9
10
  };
@@ -57,6 +57,29 @@ export function createScrollSync(ctx) {
57
57
  return;
58
58
  ctx.scrollSyncRaf = requestAnimationFrame(flushScheduledScrollSync);
59
59
  }
60
+ /**
61
+ * Sync the virtualizers to a scroll position NOW instead of on the next
62
+ * animation frame.
63
+ *
64
+ * For a scroll the grid made itself - keyboard navigation bringing the
65
+ * active cell into view - the position is already known, and going through
66
+ * the browser's `scroll` event and then the rAF above put the row-window
67
+ * update one frame behind the key press: the keydown's own flush painted
68
+ * the active cell on the OLD rows, and the rows only caught up on the next
69
+ * frame. Two painted frames per arrow key. Syncing inline folds the window
70
+ * update into the same flush as the active-cell change, so the key press
71
+ * paints once. The `scroll` event still fires afterwards and re-syncs to
72
+ * the same offset, which the virtualizers treat as a no-op.
73
+ */
74
+ function syncScrollNow(scrollTop, scrollLeft) {
75
+ if (ctx.scrollSyncRaf !== null) {
76
+ cancelAnimationFrame(ctx.scrollSyncRaf);
77
+ ctx.scrollSyncRaf = null;
78
+ }
79
+ ctx.pendingScrollTop = scrollTop;
80
+ ctx.pendingScrollLeft = scrollLeft;
81
+ flushScheduledScrollSync();
82
+ }
60
83
  function onBodyScroll(event) {
61
84
  const container = event.currentTarget;
62
85
  if (!container)
@@ -84,6 +107,7 @@ export function createScrollSync(ctx) {
84
107
  hideTooltip,
85
108
  flushScheduledScrollSync,
86
109
  scheduleScrollSync,
110
+ syncScrollNow,
87
111
  onBodyScroll,
88
112
  };
89
113
  }
@@ -15,6 +15,22 @@ export declare function createSelection<TFeatures extends TableFeatures = TableF
15
15
  left: boolean;
16
16
  right: boolean;
17
17
  } | null;
18
+ activeRangeRect: () => {
19
+ minRow: number;
20
+ maxRow: number;
21
+ minCol: number;
22
+ maxCol: number;
23
+ } | null;
24
+ cellSelectionState: (rowIndex: number, colIndex: number) => {
25
+ active: boolean;
26
+ edges: {
27
+ top: boolean;
28
+ bottom: boolean;
29
+ left: boolean;
30
+ right: boolean;
31
+ } | null;
32
+ fillHandle: boolean;
33
+ } | null;
18
34
  fillMarqueeEdges: (rowIndex: number, colIndex: number) => {
19
35
  top: boolean;
20
36
  bottom: boolean;
package/dist/selection.js CHANGED
@@ -4,6 +4,7 @@
4
4
  import { getGridCellDomId, } from "./index.js";
5
5
  import "./sv-grid-scrollbar.js";
6
6
  import { getColumnBaseValue, isGroupRow, } from "./cell-values.js";
7
+ import { expandRectToMerges, originOf, endOf } from "./merges.js";
7
8
  export function createSelection(ctx) {
8
9
  function isRowSelected(rowId) {
9
10
  return Boolean(ctx.rowSelectionState[rowId]);
@@ -26,7 +27,18 @@ export function createSelection(ctx) {
26
27
  });
27
28
  }
28
29
  function setActiveCell(rowIndex, colIndex) {
30
+ // The active cell inside a merge is its origin: the one that draws, the
31
+ // one the formula bar shows, the one an edit writes.
32
+ ({ rowIndex, colIndex } = originOf(ctx.mergeIndex, rowIndex, colIndex));
29
33
  ctx.userHasActivatedCell = true;
34
+ // A click, an arrow, a jump: the Tab run is over. The Tab and Enter
35
+ // paths put the origin back after this call when it should live on.
36
+ // Activating the cell the cursor is already on (typing into it, F2, a
37
+ // double-click) is not a move and keeps the run.
38
+ const prev = ctx.grid.getState().activeCell;
39
+ if (!prev || prev.rowIndex !== rowIndex || prev.colIndex !== colIndex) {
40
+ ctx.tabRunOrigin = null;
41
+ }
30
42
  ctx.grid.setActiveCell({
31
43
  rowIndex,
32
44
  colIndex,
@@ -51,7 +63,16 @@ export function createSelection(ctx) {
51
63
  return;
52
64
  if (colIndex < 0 || colIndex >= ctx.allColumns.length)
53
65
  return;
54
- if (ctx.rowVirtualizationEnabled) {
66
+ const container = ctx.scrollContainer;
67
+ const topBefore = container.scrollTop;
68
+ const leftBefore = container.scrollLeft;
69
+ // A frozen row is always in view; only the column may need to move.
70
+ const frozen = ctx.frozenRowCount ?? 0;
71
+ const frozenBand = ctx.frozenBandHeight ?? 0;
72
+ if (rowIndex < frozen) {
73
+ // Always in view; only the column below may need to move.
74
+ }
75
+ else if (ctx.rowVirtualizationEnabled) {
55
76
  // Prefer the browser's native `scrollIntoView({ block: 'nearest' })`
56
77
  // when the target row is already mounted. It does Excel-style
57
78
  // minimum-scroll, respects the sticky thead via the
@@ -99,8 +120,10 @@ export function createSelection(ctx) {
99
120
  const rowHeight = ctx.virtualizer.getSizeForIndex(rowIndex);
100
121
  const rowBottom = rowTopScroll + rowHeight;
101
122
  let nextTop = currentTop;
102
- if (rowTopScroll < currentTop + headerHeight) {
103
- nextTop = rowTopScroll - headerHeight;
123
+ // The frozen band sits under the header, so a row is hidden until
124
+ // it clears both.
125
+ if (rowTopScroll < currentTop + headerHeight + frozenBand) {
126
+ nextTop = rowTopScroll - headerHeight - frozenBand;
104
127
  }
105
128
  else if (rowBottom > currentTop + clientHeight) {
106
129
  nextTop = rowBottom - clientHeight;
@@ -154,34 +177,65 @@ export function createSelection(ctx) {
154
177
  else if (cellEnd > viewEnd) {
155
178
  ctx.scrollContainer.scrollLeft = cellEnd - ctx.scrollContainer.clientWidth;
156
179
  }
157
- // No inline scrollVersion bump - the `scroll` event triggers
158
- // onBodyScroll which flushes via rAF, doing one batched update.
180
+ // The scroll position is known right here, so hand it to the
181
+ // virtualizers now rather than waiting for the `scroll` event + rAF:
182
+ // that put the new row window one painted frame behind the active-cell
183
+ // move (see `syncScrollNow`). Reading the position back is cheap - the
184
+ // scrollIntoView / scrollTop writes above already laid the tree out.
185
+ const topAfter = container.scrollTop;
186
+ const leftAfter = container.scrollLeft;
187
+ if (topAfter !== topBefore || leftAfter !== leftBefore) {
188
+ ctx.syncScrollNow(topAfter, leftAfter);
189
+ }
159
190
  }
160
- // Normalize a range to a rectangle, or null when incomplete.
191
+ // Normalize a range to a rectangle, or null when incomplete. A merge
192
+ // the rectangle touches is in it whole, as a spreadsheet's selection
193
+ // has it: a merged cell is one cell.
161
194
  function rangeRect(range) {
162
195
  const a = range?.anchor;
163
196
  const f = range?.focus;
164
197
  if (!a || !f)
165
198
  return null;
166
- return {
199
+ return expandRectToMerges(ctx.mergeIndex, {
167
200
  minRow: Math.min(a.rowIndex, f.rowIndex),
168
201
  maxRow: Math.max(a.rowIndex, f.rowIndex),
169
202
  minCol: Math.min(a.colIndex, f.colIndex),
170
203
  maxCol: Math.max(a.colIndex, f.colIndex),
171
- };
204
+ });
205
+ }
206
+ /** The active range as a rectangle, or null while nothing is anchored. */
207
+ function activeRangeRect() {
208
+ return rangeRect(ctx.selectionRange);
172
209
  }
210
+ // Memo for `getSelectionRects`, keyed on the identity of its two inputs.
211
+ // Both are replaced wholesale on every change and never mutated in place,
212
+ // so identity is a complete change check. Without it, every rendered
213
+ // cell's outline derived rebuilt this array on every arrow key - ~300
214
+ // arrays plus their rect objects per key press, for a result that is the
215
+ // same until the selection moves.
216
+ let rectsForRange = null;
217
+ let rectsForRanges = null;
218
+ let rectsMemo = [];
173
219
  // Every selected rectangle: the committed extra ranges plus the active one.
174
220
  // Order matters for copy (added-order); the active range comes last.
221
+ // Callers must treat the result as read-only.
175
222
  function getSelectionRects() {
223
+ const range = ctx.selectionRange;
224
+ const ranges = ctx.selectionRanges ?? [];
225
+ if (range === rectsForRange && ranges === rectsForRanges)
226
+ return rectsMemo;
176
227
  const rects = [];
177
- for (const r of ctx.selectionRanges ?? []) {
228
+ for (const r of ranges) {
178
229
  const rect = rangeRect(r);
179
230
  if (rect)
180
231
  rects.push(rect);
181
232
  }
182
- const active = rangeRect(ctx.selectionRange);
233
+ const active = rangeRect(range);
183
234
  if (active)
184
235
  rects.push(active);
236
+ rectsForRange = range;
237
+ rectsForRanges = ranges;
238
+ rectsMemo = rects;
185
239
  return rects;
186
240
  }
187
241
  function setSelection(rowIndex, colIndex, additive = false) {
@@ -240,6 +294,9 @@ export function createSelection(ctx) {
240
294
  // that work to return null every time.
241
295
  if (!hasAnySelectionRect())
242
296
  return null;
297
+ // A td inside a merge draws to the merge's far corner, so its bottom and
298
+ // right edges are the merge's, not the cell's.
299
+ const end = endOf(ctx.mergeIndex, rowIndex, colIndex);
243
300
  for (const rect of getSelectionRects()) {
244
301
  if (rowIndex < rect.minRow ||
245
302
  rowIndex > rect.maxRow ||
@@ -249,13 +306,39 @@ export function createSelection(ctx) {
249
306
  }
250
307
  return {
251
308
  top: rowIndex === rect.minRow,
252
- bottom: rowIndex === rect.maxRow,
309
+ bottom: end.rowIndex === rect.maxRow,
253
310
  left: colIndex === rect.minCol,
254
- right: colIndex === rect.maxCol,
311
+ right: end.colIndex === rect.maxCol,
255
312
  };
256
313
  }
257
314
  return null;
258
315
  }
316
+ /**
317
+ * Everything a cell's outline needs from the selection model, in ONE value:
318
+ * whether it is the active cell, which range edges it sits on (`edges` is
319
+ * null when it is in no range) and whether it carries the fill handle.
320
+ * `null` for a cell that is none of those - nearly every cell.
321
+ *
322
+ * One value on purpose. The view reads it through a single `{@const}`, so
323
+ * an active-cell move re-marks one derived per rendered cell instead of
324
+ * three (active flag, range edges, fill handle) plus the fill handle's
325
+ * `{#if}`. With ~300 cells rendered, that marking was the largest single
326
+ * cost of an arrow key that does not scroll. A plain cell returns the same
327
+ * `null` it returned before the move, so its attribute effect stays put.
328
+ */
329
+ function cellSelectionState(rowIndex, colIndex) {
330
+ const a = ctx.activeCell;
331
+ const active = !!a && a.rowIndex === rowIndex && a.colIndex === colIndex;
332
+ const edges = getCellRangeEdges(rowIndex, colIndex);
333
+ const fh = ctx.fillHandleCell;
334
+ // The handle sits on the range's bottom-right cell; when that is inside
335
+ // a merge, the td that draws the merge carries it.
336
+ const end = fh ? endOf(ctx.mergeIndex, rowIndex, colIndex) : null;
337
+ const fillHandle = !!fh && !!end && fh.rowIndex === end.rowIndex && fh.colIndex === end.colIndex;
338
+ if (!active && !edges && !fillHandle)
339
+ return null;
340
+ return { active, edges, fillHandle };
341
+ }
259
342
  /**
260
343
  * The rectangle a drag is currently promising, or null when no drag is in
261
344
  * flight. A fill grows the source rectangle out to the pointer; a move
@@ -368,8 +451,13 @@ export function createSelection(ctx) {
368
451
  ? { rowIndex: active.rowIndex, colIndex: active.colIndex }
369
452
  : null;
370
453
  if (event.shiftKey) {
454
+ // The range grows to the clicked cell and the active cell stays where
455
+ // it is, as in Excel: it is the cell the formula bar shows and the
456
+ // one the next keystroke edits. Used to jump to the clicked corner,
457
+ // so a Shift+click to select B2:D5 then typing wrote into D5.
371
458
  extendSelection(rowIndex, colIndex);
372
- setActiveCell(rowIndex, colIndex);
459
+ if (!active)
460
+ setActiveCell(rowIndex, colIndex);
373
461
  }
374
462
  else {
375
463
  setActiveCell(rowIndex, colIndex);
@@ -390,8 +478,9 @@ export function createSelection(ctx) {
390
478
  const row = ctx.allRows[rowIndex];
391
479
  if (!row || isGroupRow(row))
392
480
  return;
481
+ // The active cell is where the drag began; only the range's far corner
482
+ // follows the pointer.
393
483
  extendSelection(rowIndex, colIndex);
394
- setActiveCell(rowIndex, colIndex);
395
484
  }
396
485
  /** `event` is the window `pointerup` when this comes from the real gesture,
397
486
  * and absent when `onWindowPointerMove` calls it to recover from a release
@@ -516,11 +605,12 @@ export function createSelection(ctx) {
516
605
  return;
517
606
  }
518
607
  // onCellPointerDown already set active+selection for data cells. Only
519
- // decide whether to enter edit mode (click on the previously-active cell).
608
+ // decide whether to enter edit mode (click on the previously-active cell,
609
+ // unless the grid was told a second click is just a click).
520
610
  const wasActive = ctx.activeAtPointerDown !== null &&
521
611
  ctx.activeAtPointerDown.rowIndex === rowIndex &&
522
612
  ctx.activeAtPointerDown.colIndex === colIndex;
523
- if (wasActive && ctx.editingEnabled) {
613
+ if (wasActive && ctx.editingEnabled && ctx.props.editOnSecondClick !== false) {
524
614
  ctx.onCellDoubleClick(rowIndex, colIndex);
525
615
  return;
526
616
  }
@@ -562,6 +652,8 @@ export function createSelection(ctx) {
562
652
  extendSelection,
563
653
  isCellInSelectedRange,
564
654
  getCellRangeEdges,
655
+ activeRangeRect,
656
+ cellSelectionState,
565
657
  fillMarqueeEdges,
566
658
  getSelectionRects,
567
659
  isInFillPreview,