@svgrid/grid 3.0.3 → 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-D6XvA1Ij.js → src-BKhZ6eXd.js} +10227 -9787
  67. package/dist/cdn/{src-Bugf5XlI.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-CHOMnW-c.js +0 -602
  273. package/dist/cdn/GridMenus-DhdPF700.js +0 -603
  274. package/dist/cdn/SvDateTimePicker-CCbDZNZB.js +0 -816
  275. package/dist/cdn/SvGridChart-1PMSp2LZ.js +0 -1481
  276. package/dist/cdn/SvGridChart-D02avhyr.js +0 -1480
  277. package/dist/cdn/SvGridChartPanel-DJnViOv4.js +0 -834
  278. package/dist/cdn/SvGridChartPanel-vPTNerFZ.js +0 -810
  279. package/dist/cdn/SvGridChartView-C--HhdUo.js +0 -56
  280. package/dist/cdn/SvGridChartView-KyyFG-wY.js +0 -55
  281. package/dist/cdn/chart-DLg3_zTQ.js +0 -1652
@@ -0,0 +1,181 @@
1
+ /**
2
+ * shortcut-registry - the injection seam for keyboard commands the free grid
3
+ * does not implement. `@svgrid/grid` owns cell navigation, copy/paste, undo and
4
+ * find; the Excel command set (Ctrl+Arrow to the edge of a data region, Ctrl+D
5
+ * fill down, Ctrl+Shift+V paste special, Alt+= autosum) is a paid feature in
6
+ * `@svgrid/enterprise`. This holder lets enterprise register a handler at
7
+ * import time; the grid runs the chain before it interprets a key itself.
8
+ *
9
+ * Same shape as `selection-bar-view`, `scheduler-view` and `board-view`, with
10
+ * one difference: those hold a single renderer, this holds a prioritised chain,
11
+ * because several packages may want a key and the first claim has to win
12
+ * deterministically.
13
+ *
14
+ * The chain runs BEFORE `getKeyboardIntent`, which is the whole point. The
15
+ * intent union in `keyboard.ts` is public API and closed, so there is no member
16
+ * that could mean "jump to the edge of the data region" and no way to add one
17
+ * without breaking a consumer that switches on it exhaustively. Running first
18
+ * sidesteps that: a registered handler claims `Ctrl+ArrowDown` and returns
19
+ * true, and the grid never computes an intent for that event at all.
20
+ *
21
+ * ```ts
22
+ * // in @svgrid/enterprise, at module load:
23
+ * import { registerGridShortcuts } from '@svgrid/grid'
24
+ * registerGridShortcuts(handleSheetKey, { id: 'sheet', priority: 100 })
25
+ * ```
26
+ */
27
+ import type { SvGridApi } from './index'
28
+
29
+ /** What a handler gets to read and write. A getter object over controller
30
+ * state, built only when at least one handler is registered - a free grid
31
+ * never pays to construct it. Indices are DISPLAY indices, matching
32
+ * `SelectionPoint`, so they follow sort, filter and pagination. */
33
+ export type GridCommandContext = {
34
+ /** The grid's public api. Identity is stable for the life of the grid, so
35
+ * a handler can key per-grid state on it in a WeakMap. */
36
+ readonly api: SvGridApi<any, any>
37
+ /** True when the event came from the cell editor rather than the grid root.
38
+ * Handlers that only make sense mid-edit (Alt+Enter) check this. */
39
+ readonly editing: boolean
40
+ /**
41
+ * The open inline editor's element, or null. A binding registered for
42
+ * `editing: true` reads and rewrites the text through it: F4 in a
43
+ * spreadsheet pins the reference under the caret.
44
+ */
45
+ readonly editor: HTMLInputElement | HTMLTextAreaElement | null
46
+ readonly activeCell: { rowIndex: number; colIndex: number; columnId: string | null } | null
47
+ /**
48
+ * The corner the active range grows from: the far end of a Shift+Arrow,
49
+ * Shift+click or drag, which need not be the active cell. Null while
50
+ * nothing is anchored. A command that extends the selection steps from
51
+ * here, not from the active cell, or Ctrl+Shift+Down twice would land
52
+ * on the same edge twice.
53
+ */
54
+ readonly selectionFocus: { rowIndex: number; colIndex: number } | null
55
+ readonly rowCount: number
56
+ readonly colCount: number
57
+ /** Every selected rectangle as [minRow, minCol, maxRow, maxCol], active last. */
58
+ readonly ranges: ReadonlyArray<readonly [number, number, number, number]>
59
+ columnIdAt(colIndex: number): string | null
60
+ getCellValue(rowIndex: number, colIndex: number): unknown
61
+ /** Write a cell and record it in the grid's undo history. Inside `batch`
62
+ * every write joins one step, so a fill or a replace-all is one Ctrl+Z. */
63
+ setCellValue(rowIndex: number, colIndex: number, value: unknown): void
64
+ setActiveCell(rowIndex: number, colIndex: number): void
65
+ setSelection(rowIndex: number, colIndex: number): void
66
+ extendSelection(rowIndex: number, colIndex: number): void
67
+ scrollIntoView(rowIndex: number, colIndex: number): void
68
+ startEditing(rowIndex: number, colIndex: number, seed?: string): boolean
69
+ /** Run every write inside `fn` as ONE undo step. Returns what `fn` returns. */
70
+ batch<T>(fn: () => T): T
71
+ /**
72
+ * Put something that is not a cell write into the grid's undo history:
73
+ * a format kept in the caller's own store, a structural change the grid
74
+ * cannot see. `undo` and `redo` are what Ctrl+Z and Ctrl+Y will call.
75
+ * Inside `batch` the step joins the batch, so a command that writes cells
76
+ * AND formats is still one press.
77
+ */
78
+ recordUndo(undo: () => void, redo: () => void): void
79
+ /**
80
+ * Put keyboard focus back on the grid, without scrolling.
81
+ *
82
+ * A toolbar button takes focus when it is clicked and a dialog takes it
83
+ * when it opens, so the keystroke after either - Ctrl+Z to undo what the
84
+ * button did, typing into the cell the dialog changed - lands on the button
85
+ * or on `<body>` rather than on the grid. Excel returns focus to the sheet
86
+ * after every ribbon command and every dialog; a command that owns chrome
87
+ * calls this once it is done.
88
+ */
89
+ focus(): void
90
+ /**
91
+ * Paste the system clipboard at the active cell, the way Ctrl+V does:
92
+ * tab-separated text becomes a block of cells, one undo step. Resolves
93
+ * once the paste has been applied, or without doing anything when the
94
+ * clipboard cannot be read (an insecure context, or permission refused),
95
+ * which is the same silence Ctrl+V keeps there.
96
+ */
97
+ paste(): Promise<void>
98
+ /**
99
+ * Copy the selection the way Ctrl+C does: every selected rectangle as
100
+ * tab-separated text, each cell through `processCellForClipboard`. The
101
+ * api's `copyToClipboard` is the export: the displayed rows with their
102
+ * headers, whatever the selection.
103
+ */
104
+ copy(): void
105
+ /**
106
+ * Cut the selection the way Ctrl+X does: the copy above, then the cells
107
+ * cleared as one undo step.
108
+ */
109
+ cut(): Promise<void>
110
+ /**
111
+ * Whether a cell may be written: the column's `editable` predicate as
112
+ * the grid's own editor, fill handle and paste read it. `setCellValue`
113
+ * does not consult it (a command is trusted to know what it writes), so
114
+ * a command that fills or stamps a range asks here and skips the cells
115
+ * that say no. Optional for a context built by hand.
116
+ */
117
+ canEdit?(rowIndex: number, colIndex: number): boolean
118
+ }
119
+
120
+ /** Return true to consume the event. The grid stops there and does not
121
+ * interpret the key itself. Return false to let it fall through. */
122
+ export type GridShortcutHandler = (
123
+ event: KeyboardEvent,
124
+ cmd: GridCommandContext,
125
+ ) => boolean
126
+
127
+ type Entry = { handler: GridShortcutHandler; id: string | undefined; priority: number }
128
+
129
+ // Module-level, not $state: the chain is written at import time by whoever
130
+ // installs a feature pack and read inside an event handler. Nothing renders
131
+ // from it, so reactivity would cost a proxy for no benefit.
132
+ let entries: Entry[] = []
133
+
134
+ /**
135
+ * Register a keyboard handler. Higher `priority` runs first; ties keep
136
+ * registration order. Passing an `id` that is already registered replaces that
137
+ * entry rather than stacking a second copy, so an enable* function stays
138
+ * idempotent. Returns an unregister function.
139
+ */
140
+ export function registerGridShortcuts(
141
+ handler: GridShortcutHandler,
142
+ opts?: { id?: string; priority?: number },
143
+ ): () => void {
144
+ const entry: Entry = {
145
+ handler,
146
+ id: opts?.id,
147
+ priority: opts?.priority ?? 0,
148
+ }
149
+ const next = entry.id ? entries.filter((e) => e.id !== entry.id) : entries.slice()
150
+ next.push(entry)
151
+ // Stable sort by descending priority: Array.prototype.sort is stable per
152
+ // spec, so equal priorities keep the order they were pushed in.
153
+ next.sort((a, b) => b.priority - a.priority)
154
+ entries = next
155
+ return () => {
156
+ entries = entries.filter((e) => e !== entry)
157
+ }
158
+ }
159
+
160
+ /** Run the chain. Returns true when a handler consumed the event. */
161
+ export function runGridShortcuts(
162
+ event: KeyboardEvent,
163
+ cmd: GridCommandContext,
164
+ ): boolean {
165
+ for (const entry of entries) {
166
+ if (entry.handler(event, cmd)) return true
167
+ }
168
+ return false
169
+ }
170
+
171
+ /** Whether anything is registered. The grid checks this before building a
172
+ * command context, so an unregistered grid pays one array-length read. */
173
+ export function hasGridShortcuts(): boolean {
174
+ return entries.length > 0
175
+ }
176
+
177
+ /** Drop every handler. Tests use this; production code should keep the
178
+ * unregister function `registerGridShortcuts` returns. */
179
+ export function clearGridShortcuts(): void {
180
+ entries = []
181
+ }
@@ -3,8 +3,9 @@
3
3
  // type-level cycle. Backs the generic key/value typing of `setOption` / `getOption`.
4
4
  import type { FilterOperator, Props } from './SvGrid.types'
5
5
  import type { GridExportOptions, GridClipboardOptions } from './export-format'
6
- import type { ChartSpec, ChartType } from './chart'
6
+ import type { ChartFormatState, ChartReducer, ChartSpec, ChartTimeBucket, ChartType, ChartZoomWindow } from './chart'
7
7
  import type { GridPredicateExpr } from './filtering/predicate-expr'
8
+ import type { GridCommandContext } from './shortcut-registry'
8
9
 
9
10
  // Aliased to the core union rather than restated: the API surfaces below hand
10
11
  // back whatever the grid actually filtered with, so a hand-maintained subset
@@ -53,21 +54,70 @@ export type SvGridViewState = {
53
54
  reduce: 'sum' | 'avg' | 'count'
54
55
  stacked: boolean
55
56
  }
56
- /** All charts in the tab strip (multi-chart), plus the active index. */
57
- charts?: Array<{
57
+ /** All charts in the tab strip (multi-chart), plus the active index.
58
+ * Every field the panel can change is here so a saved view restores the
59
+ * chart exactly; `null` inherits the `charting` config. */
60
+ charts?: ChartTabSnapshot[]
61
+ chartActive?: number
62
+ /** The named charts kept with `api.saveChart`; present only when there is one. */
63
+ savedCharts?: SavedChart[]
64
+ }
65
+
66
+ /** One chart tab as the saved view carries it (`GridState.charts[i]`). */
67
+ export type ChartTabSnapshot = {
58
68
  title: string
59
69
  type: ChartType
60
70
  dimension: string | null
61
71
  series: string | null
62
72
  measure: string | null
63
- reduce: 'sum' | 'avg' | 'count'
73
+ /** Scatter's y measure, a range's high, a bullet's target. */
74
+ measure2?: string | null
75
+ reduce: ChartReducer
64
76
  stacked: boolean | null
77
+ stacked100?: boolean | null
78
+ orientation?: 'vertical' | 'horizontal' | null
79
+ donut?: boolean | null
80
+ palette?: string[] | null
65
81
  dataLabels: boolean | null
66
82
  logScale: boolean | null
67
83
  timeAxis: boolean | null
68
84
  valueFormat: 'number' | 'currency' | 'percent' | 'compact' | null
69
- }>
70
- chartActive?: number
85
+ bucket?: ChartTimeBucket | null
86
+ bins?: number | null
87
+ funnelShape?: 'trapezoid' | 'pyramid' | 'cone' | null
88
+ candleStyle?: 'classic' | 'hollow' | 'heikin-ashi' | null
89
+ /** The zoom window as category indices, or null for the whole chart. */
90
+ zoom?: ChartZoomWindow | null
91
+ ohlc?: { open: string | null; high: string | null; low: string | null; close: string | null; volume: string | null } | null
92
+ indicators?: string[]
93
+ format?: ChartFormatState | null
94
+ /** An unlinked chart carries the spec it was frozen with. */
95
+ frozen?: { spec: ChartSpec; at: string } | null
96
+ }
97
+
98
+ /** A chart configuration kept under a name (`api.saveChart`). */
99
+ export type SavedChart = {
100
+ name: string
101
+ /** ISO timestamp of the save. */
102
+ savedAt: string
103
+ /** The tab's state at the time; `applySavedChart` puts it on the active tab. */
104
+ tab: ChartTabSnapshot
105
+ }
106
+
107
+ /** One cell, in the 0-based display coordinates every selection method uses. */
108
+ export type SvGridCellCoords = { row: number; col: number }
109
+
110
+ /**
111
+ * A selected rectangle with its orientation kept: `from` is where the
112
+ * selection was started (the anchor) and `to` where it ended (the focus),
113
+ * so a range dragged upwards has `from.row > to.row`. `highlight` is the
114
+ * active cell inside it. The same three fields as Handsontable's CellRange,
115
+ * so code written against `getSelectedRange()` there reads the same here.
116
+ */
117
+ export type SvGridSelectedRange = {
118
+ from: SvGridCellCoords
119
+ to: SvGridCellCoords
120
+ highlight: SvGridCellCoords
71
121
  }
72
122
 
73
123
  /**
@@ -118,17 +168,44 @@ export type SvGridApi<
118
168
  * range is `[rowStart, colStart, rowEnd, colEnd]` in 0-indexed grid
119
169
  * coordinates. Pass an empty array to clear the selection.
120
170
  *
121
- * The grid currently honours the FIRST range only (single-range
122
- * engine); subsequent ranges are accepted for API forward compat
123
- * but ignored. The grid's active cell jumps to the range's start
124
- * corner.
171
+ * Every range is honoured: the last one becomes the active range and
172
+ * the others stay highlighted beside it, as after a Ctrl+drag. The
173
+ * active cell goes to the last range's start corner.
125
174
  */
126
175
  selectCells(ranges: ReadonlyArray<readonly [number, number, number, number]>): void
127
176
  /**
128
- * Returns the current cell-selection rectangles in the same shape
129
- * `selectCells` accepts. Empty array when no range is active.
177
+ * The current cell-selection rectangles in the same shape `selectCells`
178
+ * accepts, each normalised to `[minRow, minCol, maxRow, maxCol]`, in the
179
+ * order they were selected with the active one last. Empty array when no
180
+ * range is active.
181
+ *
182
+ * Handsontable's method of the same name. Two differences, both on
183
+ * purpose: an empty array rather than `undefined`, so `getSelected()[0]`
184
+ * never throws; and normalised corners, so a loop from the first row to
185
+ * the last cannot run zero times because the range was dragged upwards.
186
+ * For the orientation, and the active cell inside a range, use
187
+ * `getSelectedRange()`.
188
+ *
189
+ * With cell selection off (`selectable={false}`) no rectangle is ever
190
+ * recorded and this stays `[]`; the focused cell is `getActiveCell()`.
130
191
  */
131
192
  getSelected(): Array<[number, number, number, number]>
193
+ /**
194
+ * The most recently selected rectangle as `[startRow, startCol, endRow,
195
+ * endCol]`, start being where the selection began, or `undefined` when
196
+ * nothing is selected. Handsontable's `getSelectedLast()`, orientation
197
+ * included: a range dragged upwards has `startRow > endRow`.
198
+ */
199
+ getSelectedLast(): [number, number, number, number] | undefined
200
+ /**
201
+ * Every selected rectangle with its orientation and active cell, oldest
202
+ * first, or `undefined` when nothing is selected. Handsontable's
203
+ * `getSelectedRange()`.
204
+ */
205
+ getSelectedRange(): SvGridSelectedRange[] | undefined
206
+ /** The most recently selected rectangle, or `undefined`. Handsontable's
207
+ * `getSelectedRangeLast()`. */
208
+ getSelectedRangeLast(): SvGridSelectedRange | undefined
132
209
 
133
210
  // ----- Integrated charting (requires the `charting` prop) -----
134
211
  /** Open the built-in chart panel. */
@@ -149,13 +226,47 @@ export type SvGridApi<
149
226
  dimension?: string | null
150
227
  series?: string | null
151
228
  measure?: string | null
152
- reduce?: 'sum' | 'avg' | 'count'
229
+ reduce?: ChartReducer
153
230
  stacked?: boolean
154
231
  dataLabels?: boolean
155
232
  logScale?: boolean
156
233
  timeAxis?: boolean
157
234
  valueFormat?: 'number' | 'currency' | 'percent' | 'compact'
235
+ /** Group a date dimension by calendar unit; `null` returns to exact values. */
236
+ bucket?: ChartTimeBucket | null
237
+ /** Second measure: a scatter's Y, a range's high end, a bullet's target. */
238
+ measure2?: string | null
239
+ /** Histogram bin count; `null` for Sturges' rule. */
240
+ bins?: number | null
241
+ funnelShape?: 'trapezoid' | 'pyramid' | 'cone' | null
242
+ candleStyle?: 'classic' | 'hollow' | 'heikin-ashi' | null
243
+ /** Candlestick / OHLC: the price columns (ids or fields); `null` goes
244
+ * back to guessing them from the column names. */
245
+ ohlc?: { open?: string | null; high?: string | null; low?: string | null; close?: string | null; volume?: string | null } | null
246
+ /** Indicator panes under a price chart (`volume`, `rsi`, `macd`,
247
+ * `stochastic`, `atr`, `obv`) and overlays on it (`sma`, `ema`,
248
+ * `bb`, `vwap`). */
249
+ indicators?: Array<'volume' | 'rsi' | 'macd' | 'stochastic' | 'atr' | 'obv' | 'sma' | 'ema' | 'bb' | 'vwap'>
250
+ /** The builder's format state (titles, axes, legend, per-series style);
251
+ * `null` resets it. */
252
+ format?: ChartFormatState | null
253
+ /** `true` unlinks the chart from the grid (it keeps the spec it has),
254
+ * `false` links it back. */
255
+ frozen?: boolean
256
+ /** Apply a saved chart (by name) to the active tab before the other keys. */
257
+ saved?: string
158
258
  }): void
259
+ /**
260
+ * Keep the active chart's whole configuration (type, columns, aggregate,
261
+ * switches, indicators, format, zoom) under a name. A same-named entry is
262
+ * replaced. Saved charts travel with `getState()` / `setState()` and show
263
+ * in the panel's Saved charts popover.
264
+ */
265
+ saveChart(name: string): void
266
+ /** Apply a saved chart to the active tab, which keeps its title. `false` when no chart has that name. */
267
+ applySavedChart(name: string): boolean
268
+ removeSavedChart(name: string): void
269
+ getSavedCharts(): SavedChart[]
159
270
  /**
160
271
  * Register a natural-language "chart this" handler. When set, the chart
161
272
  * panel shows an AI button. `@svgrid/enterprise`'s `enableAiCharting(api)`
@@ -164,6 +275,13 @@ export type SvGridApi<
164
275
  setChartAiHandler(
165
276
  handler: ((prompt: string) => Promise<Record<string, unknown> | null>) | null,
166
277
  ): void
278
+ /**
279
+ * Register an "explain this chart" handler. When set, the chart panel's AI
280
+ * row shows an Explain button that reads the active chart and shows the
281
+ * summary and insights it returns. `enableAiCharting(api)` fills this with
282
+ * `aiExplainChart`. Pass `null` to remove it.
283
+ */
284
+ setChartExplainHandler(handler: (() => Promise<{ summary: string; insights: string[] } | null>) | null): void
167
285
 
168
286
  // ----- Rows -----
169
287
  /** Add one row. `position` defaults to `'bottom'`. */
@@ -259,6 +377,13 @@ export type SvGridApi<
259
377
  * `editorOptions` returns a Promise - static lists are never refetched.
260
378
  */
261
379
  refreshEditorOptions(columnId?: string): void
380
+ /**
381
+ * Open the `contextMenu` at the pointer, for the cell at (rowIndex,
382
+ * colIndex), from an element of your own: a spreadsheet's column letters
383
+ * and row numbers offer the cell menu for the column or row they select.
384
+ * Does nothing without a `contextMenu` prop.
385
+ */
386
+ openContextMenu(event: MouseEvent, rowIndex: number, colIndex: number): void
262
387
  /**
263
388
  * Clear every active column filter (menu, filter-row, set-list, global, and
264
389
  * the advanced filter). Resets the grid to "no filtering" in a single call.
@@ -370,6 +495,38 @@ export type SvGridApi<
370
495
  * "Autosize" item.
371
496
  */
372
497
  autosizeColumn(columnId: string): void
498
+ /**
499
+ * The height of a row in px: the one it was dragged or set to, else the
500
+ * declared `rowHeight` (30 when none). `rowIndex` is a display index.
501
+ */
502
+ getRowHeight(rowIndex: number): number
503
+ /**
504
+ * Give a row its own height, the way dragging its grip does with
505
+ * `rowResize`; `null` takes it back to the declared height. The height
506
+ * belongs to the row (its id), so it follows the row through a sort and
507
+ * survives the data being replaced. Ignored under `autoRowHeight`, where
508
+ * the content decides.
509
+ */
510
+ setRowHeight(rowIndex: number, height: number | null): void
511
+ /**
512
+ * Fold a column to nothing, the way a spreadsheet hides one: it keeps
513
+ * its index, its cells and its width, and takes no room until it is
514
+ * unfolded. Unlike `setColumnVisible(id, false)`, which takes the
515
+ * column out of the model and shifts every index after it, references
516
+ * by index stay valid, so a sheet's formulas and formats are untouched.
517
+ * Arrow keys, Tab and Enter step over a collapsed column.
518
+ */
519
+ setColumnCollapsed(columnId: string, collapsed: boolean): void
520
+ isColumnCollapsed(columnId: string): boolean
521
+ /**
522
+ * Fold a row to nothing, the row-side twin of `setColumnCollapsed`.
523
+ * `rowIndex` is a display index; the fold belongs to the row (its id)
524
+ * and follows it through a sort.
525
+ */
526
+ setRowCollapsed(rowIndex: number, collapsed: boolean): void
527
+ isRowCollapsed(rowIndex: number): boolean
528
+ /** The `mergedCells` in force, copied: origins with their spans. */
529
+ getMergedCells(): Array<{ rowIndex: number; colIndex: number; rowSpan: number; colSpan: number }>
373
530
  /** Run `autosizeColumn` on every column. */
374
531
  autosizeAllColumns(): void
375
532
  /**
@@ -424,6 +581,20 @@ export type SvGridApi<
424
581
  /** Wipe both stacks (e.g. after a server save commits the buffer). */
425
582
  clearHistory(): void
426
583
 
584
+ // ----- Keyboard commands -----
585
+ /**
586
+ * The same `GridCommandContext` a registered shortcut handler receives.
587
+ *
588
+ * Keyboard commands are handed one on every keystroke, but a button in a
589
+ * ribbon or a toolbar has no keystroke to ride in on, and re-implementing
590
+ * "bold the selection" against the api would give the button and the key
591
+ * two code paths that drift. Both go through this instead.
592
+ *
593
+ * Live, not a snapshot: the object reads through to the grid, so one built
594
+ * once at mount still reports the current selection.
595
+ */
596
+ getCommandContext(): GridCommandContext
597
+
427
598
  // ----- Find in grid -----
428
599
  /** Open the built-in find overlay (Ctrl+F also opens it). */
429
600
  openFind(): void
@@ -704,6 +704,72 @@ describe('SvGrid - fitColumns', () => {
704
704
  })
705
705
  })
706
706
 
707
+ describe('SvGrid - frozenRows', () => {
708
+ const frozen = (target: HTMLElement) => [...target.querySelectorAll<HTMLElement>('tr.sv-grid-row-frozen')]
709
+
710
+ it('marks the first N rows frozen with a sticky top under the header, and keeps them as the rows they are', async () => {
711
+ const { api, target, destroy } = await mountGrid({ frozenRows: 2, virtualization: true })
712
+ try {
713
+ await tick()
714
+ const rows = frozen(target)
715
+ expect(rows).toHaveLength(2)
716
+ expect(rows[0]!.querySelector('[data-svgrid-row]')?.getAttribute('data-svgrid-row')).toBe('0')
717
+ expect(rows[1]!.querySelector('[data-svgrid-row]')?.getAttribute('data-svgrid-row')).toBe('1')
718
+ // Under the header, then under each other: the second sits a row lower.
719
+ expect(rows[1]!.style.top).not.toBe(rows[0]!.style.top)
720
+ expect(rows[1]!.classList.contains('sv-grid-row-frozen-last')).toBe(true)
721
+ // Still numbered and still data rows: the body renders the rest once.
722
+ const bodyRows = [...target.querySelectorAll('tbody.sv-grid-body tr.sv-grid-row:not(.sv-grid-row-spacer)')]
723
+ const indices = bodyRows.map((tr) => tr.querySelector('[data-svgrid-row]')?.getAttribute('data-svgrid-row')).filter(Boolean)
724
+ expect(new Set(indices).size).toBe(indices.length)
725
+ expect(indices.slice(0, 3)).toEqual(['0', '1', '2'])
726
+ expect(api.getRowHeight(0)).toBe(36)
727
+ } finally {
728
+ destroy()
729
+ }
730
+ })
731
+
732
+ it('a row below the frozen band keeps its own height, not the height of the row N above it', async () => {
733
+ // The virtual loop used to draw item i as row i + N while the
734
+ // virtualizer had sized item i as row i, so under a freeze of two rows
735
+ // a taller row 3 was drawn 36px and row 1 got the 60px.
736
+ const { api, target, destroy } = await mountGrid({ frozenRows: 2, virtualization: true })
737
+ try {
738
+ await tick()
739
+ api.setRowHeight(3, 60)
740
+ await tick()
741
+ const tr = (index: number) => target.querySelector(`[data-svgrid-row="${index}"]`)?.closest('tr') as HTMLElement | null
742
+ expect(tr(3)?.style.height).toBe('60px')
743
+ expect(tr(1)?.style.height).toBe('36px')
744
+ expect(tr(2)?.style.height).toBe('36px')
745
+ // The last rows are still reachable: every row index renders once.
746
+ const indices = [...target.querySelectorAll('tbody.sv-grid-body tr.sv-grid-row:not(.sv-grid-row-spacer)')]
747
+ .map((row) => row.querySelector('[data-svgrid-row]')?.getAttribute('data-svgrid-row'))
748
+ .filter(Boolean)
749
+ expect(new Set(indices).size).toBe(indices.length)
750
+ expect(indices).toContain(String(api.getData().length - 1))
751
+ } finally {
752
+ destroy()
753
+ }
754
+ })
755
+
756
+ it('setOption frozenRows freezes and unfreezes at runtime', async () => {
757
+ const { api, target, destroy } = await mountGrid()
758
+ try {
759
+ await tick()
760
+ expect(frozen(target)).toHaveLength(0)
761
+ api.setOption('frozenRows', 1)
762
+ await tick()
763
+ expect(frozen(target)).toHaveLength(1)
764
+ api.setOption('frozenRows', undefined)
765
+ await tick()
766
+ expect(frozen(target)).toHaveLength(0)
767
+ } finally {
768
+ destroy()
769
+ }
770
+ })
771
+ })
772
+
707
773
  describe('SvGrid - selectable shortcut (cell selection)', () => {
708
774
  // The gated path a user actually takes: pointerdown on a cell runs through
709
775
  // createSelection.setSelection, which bails on !enableCellSelectionEffective.
@@ -911,3 +977,72 @@ describe('SvGrid - homepage snippet (shortcuts only, no features import)', () =>
911
977
  }
912
978
  })
913
979
  })
980
+
981
+ describe('SvGrid - mergedCells', () => {
982
+ const td = (target: HTMLElement, r: number, c: number) =>
983
+ target.querySelector(`td[data-svgrid-row="${r}"][data-svgrid-col="${c}"]`) as HTMLElement | null
984
+
985
+ it('draws a merge as one td with its spans and leaves the covered cells out', async () => {
986
+ const { api, target, destroy } = await mountGrid({
987
+ mergedCells: [
988
+ { rowIndex: 0, colIndex: 1, rowSpan: 1, colSpan: 2 },
989
+ { rowIndex: 2, colIndex: 0, rowSpan: 2, colSpan: 1 },
990
+ ],
991
+ })
992
+ try {
993
+ await tick()
994
+ const wide = td(target, 0, 1)
995
+ expect(wide?.getAttribute('colspan')).toBe('2')
996
+ expect(wide?.dataset.mergeOrigin).toBe('0:1')
997
+ expect(wide?.classList.contains('sv-grid-cell-merged')).toBe(true)
998
+ expect(td(target, 0, 2)).toBeNull()
999
+ const tall = td(target, 2, 0)
1000
+ expect(tall?.getAttribute('rowspan')).toBe('2')
1001
+ expect(td(target, 3, 0)).toBeNull()
1002
+ // The wide td is as wide as the two columns it covers, read off a plain row.
1003
+ const px = (el: HTMLElement | null) => parseFloat(el?.style.width ?? '0')
1004
+ expect(px(wide)).toBe(px(td(target, 1, 1)) + px(td(target, 1, 2)))
1005
+ // A plain cell is untouched.
1006
+ expect(td(target, 1, 1)?.hasAttribute('colspan')).toBe(false)
1007
+ expect(api.getMergedCells()).toEqual([
1008
+ { rowIndex: 0, colIndex: 1, rowSpan: 1, colSpan: 2 },
1009
+ { rowIndex: 2, colIndex: 0, rowSpan: 2, colSpan: 1 },
1010
+ ])
1011
+ } finally {
1012
+ destroy()
1013
+ }
1014
+ })
1015
+
1016
+ it('the active cell inside a merge is its origin, a range grows to whole merges, and the arrows step over it', async () => {
1017
+ const { api, target, destroy } = await mountGrid({
1018
+ mergedCells: [{ rowIndex: 0, colIndex: 1, rowSpan: 1, colSpan: 2 }],
1019
+ })
1020
+ try {
1021
+ await tick()
1022
+ const ctx = api.getCommandContext()
1023
+ ctx.setActiveCell(0, 2)
1024
+ expect(ctx.activeCell).toMatchObject({ rowIndex: 0, colIndex: 1 })
1025
+ ctx.setSelection(0, 2)
1026
+ expect(api.getSelected()).toEqual([[0, 1, 0, 2]])
1027
+ // A range from B2 up to C1 pulls in the whole of B1:C1.
1028
+ ctx.setSelection(1, 1)
1029
+ ctx.extendSelection(0, 1)
1030
+ expect(api.getSelected()).toEqual([[0, 1, 1, 2]])
1031
+ // Right from the merge lands past it; left from D1 lands on the origin.
1032
+ ctx.setActiveCell(0, 1)
1033
+ ctx.setSelection(0, 1)
1034
+ // The grid listens on its table, and only for keys aimed at it.
1035
+ const root = target.querySelector('table.sv-grid-table') as HTMLElement
1036
+ root.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true }))
1037
+ await tick()
1038
+ expect(ctx.activeCell).toMatchObject({ rowIndex: 0, colIndex: 3 })
1039
+ root.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowLeft', bubbles: true }))
1040
+ await tick()
1041
+ expect(ctx.activeCell).toMatchObject({ rowIndex: 0, colIndex: 1 })
1042
+ // The td that draws the merge carries the selection's right edge.
1043
+ expect(td(target, 0, 1)?.dataset.rangeRight).toBe('true')
1044
+ } finally {
1045
+ destroy()
1046
+ }
1047
+ })
1048
+ })