@svgrid/grid 2.1.22 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (295) hide show
  1. package/README.md +9 -2
  2. package/dist/DockNodeView.svelte +306 -0
  3. package/dist/DockNodeView.svelte.d.ts +7 -0
  4. package/dist/DockPopoutHost.svelte +17 -0
  5. package/dist/DockPopoutHost.svelte.d.ts +14 -0
  6. package/dist/FlexRender.svelte +96 -96
  7. package/dist/GridFooter.svelte +178 -178
  8. package/dist/GridMenus.svelte +83 -3
  9. package/dist/SvAutoComplete.svelte +5 -3
  10. package/dist/SvCarousel.svelte +141 -163
  11. package/dist/SvColorInput.svelte +187 -186
  12. package/dist/SvColorInput.svelte.d.ts +1 -1
  13. package/dist/SvComboBox.svelte +7 -3
  14. package/dist/SvCommand.svelte +28 -85
  15. package/dist/SvContextMenu.svelte +116 -116
  16. package/dist/SvCountryInput.svelte +1 -1
  17. package/dist/SvDateRangeInput.svelte +1 -1
  18. package/dist/SvDateTimePicker.svelte +9 -3
  19. package/dist/SvDateTimePicker.svelte.d.ts +3 -0
  20. package/dist/SvDockLayout.svelte +244 -0
  21. package/dist/SvDockLayout.svelte.d.ts +77 -0
  22. package/dist/SvDockManager.svelte +922 -0
  23. package/dist/SvDockManager.svelte.d.ts +106 -0
  24. package/dist/SvDrawer.svelte +250 -228
  25. package/dist/SvDrawer.svelte.d.ts +4 -0
  26. package/dist/SvDropDownList.svelte +85 -13
  27. package/dist/SvDropDownList.svelte.d.ts +3 -0
  28. package/dist/SvField.svelte +21 -4
  29. package/dist/SvField.svelte.d.ts +1 -0
  30. package/dist/SvForm.svelte +201 -91
  31. package/dist/SvForm.svelte.d.ts +15 -22
  32. package/dist/SvGrid.controller.svelte.d.ts +32 -1
  33. package/dist/SvGrid.controller.svelte.js +156 -27
  34. package/dist/SvGrid.css +193 -5
  35. package/dist/SvGrid.svelte +3131 -2786
  36. package/dist/SvGrid.types.d.ts +452 -1
  37. package/dist/SvGridBoard.svelte +2339 -2339
  38. package/dist/SvGridChart.svelte +1724 -1724
  39. package/dist/SvGridChartPanel.svelte +485 -485
  40. package/dist/SvGridDropdown.svelte +689 -682
  41. package/dist/SvGridSelect.svelte +1 -0
  42. package/dist/SvGroupCell.svelte +109 -109
  43. package/dist/SvListBox.svelte +36 -14
  44. package/dist/SvMaskedInput.svelte +4 -3
  45. package/dist/SvMaskedInput.svelte.d.ts +1 -1
  46. package/dist/SvMenu.svelte +124 -124
  47. package/dist/SvMenuList.svelte +117 -165
  48. package/dist/SvMenuList.svelte.d.ts +2 -13
  49. package/dist/SvModal.svelte +12 -29
  50. package/dist/SvMultiSelect.svelte +1 -0
  51. package/dist/SvNumberInput.svelte +131 -130
  52. package/dist/SvNumberInput.svelte.d.ts +1 -1
  53. package/dist/SvOtpInput.svelte +158 -158
  54. package/dist/SvPagination.svelte +18 -18
  55. package/dist/SvPasswordInput.svelte +3 -2
  56. package/dist/SvPasswordInput.svelte.d.ts +1 -1
  57. package/dist/SvPhoneInput.svelte +3 -2
  58. package/dist/SvPhoneInput.svelte.d.ts +1 -1
  59. package/dist/SvPopover.svelte +139 -139
  60. package/dist/SvRowGroupPanel.svelte +170 -170
  61. package/dist/SvScrollArea.svelte +61 -61
  62. package/dist/SvStepper.svelte +6 -9
  63. package/dist/SvTagsInput.svelte +3 -2
  64. package/dist/SvTagsInput.svelte.d.ts +1 -1
  65. package/dist/SvTooltip.svelte +127 -138
  66. package/dist/SvTour.svelte +204 -176
  67. package/dist/SvTree.svelte +377 -377
  68. package/dist/SvTreeSelect.svelte +230 -229
  69. package/dist/build-api.js +76 -54
  70. package/dist/cdn/svgrid.js +10913 -6450
  71. package/dist/cdn/svgrid.svelte-external.js +9628 -5481
  72. package/dist/chart-export.js +8 -8
  73. package/dist/columns.js +6 -0
  74. package/dist/conditional-formatting.d.ts +67 -3
  75. package/dist/conditional-formatting.js +124 -17
  76. package/dist/createCarousel.svelte.d.ts +72 -0
  77. package/dist/createCarousel.svelte.js +80 -0
  78. package/dist/createCommand.svelte.d.ts +62 -0
  79. package/dist/createCommand.svelte.js +109 -0
  80. package/dist/createForm.svelte.d.ts +62 -0
  81. package/dist/createForm.svelte.js +374 -0
  82. package/dist/createMenu.svelte.d.ts +88 -0
  83. package/dist/createMenu.svelte.js +153 -0
  84. package/dist/createOverlay.svelte.d.ts +38 -0
  85. package/dist/createOverlay.svelte.js +68 -0
  86. package/dist/createPagination.d.ts +74 -0
  87. package/dist/createPagination.js +71 -0
  88. package/dist/createPopoverSelect.svelte.js +6 -12
  89. package/dist/createStepper.d.ts +44 -0
  90. package/dist/createStepper.js +26 -0
  91. package/dist/createTooltip.svelte.d.ts +32 -0
  92. package/dist/createTooltip.svelte.js +61 -0
  93. package/dist/datetime/timezone.d.ts +36 -0
  94. package/dist/datetime/timezone.js +110 -0
  95. package/dist/dock-context.d.ts +52 -0
  96. package/dist/dock-context.js +1 -0
  97. package/dist/dock-manager-model.d.ts +136 -0
  98. package/dist/dock-manager-model.js +514 -0
  99. package/dist/dock-model.d.ts +105 -0
  100. package/dist/dock-model.js +288 -0
  101. package/dist/dock-popout.d.ts +30 -0
  102. package/dist/dock-popout.js +81 -0
  103. package/dist/editor-contract.d.ts +2 -0
  104. package/dist/filter-operators.d.ts +6 -0
  105. package/dist/filter-operators.js +35 -2
  106. package/dist/filtering/excel-filters.d.ts +18 -1
  107. package/dist/filtering/excel-filters.js +56 -0
  108. package/dist/form-field.d.ts +73 -0
  109. package/dist/form-field.js +15 -0
  110. package/dist/index.d.ts +25 -5
  111. package/dist/index.js +40 -4
  112. package/dist/list-nav.d.ts +18 -0
  113. package/dist/list-nav.js +30 -0
  114. package/dist/list-option.d.ts +2 -0
  115. package/dist/menu-item.d.ts +20 -0
  116. package/dist/menu-item.js +1 -0
  117. package/dist/menus.d.ts +5 -0
  118. package/dist/menus.js +68 -16
  119. package/dist/panel-resize.d.ts +25 -0
  120. package/dist/panel-resize.js +40 -0
  121. package/dist/popover.d.ts +29 -3
  122. package/dist/popover.js +22 -6
  123. package/dist/recurrence.d.ts +33 -7
  124. package/dist/recurrence.js +151 -13
  125. package/dist/row-drag.d.ts +8 -0
  126. package/dist/row-drag.js +11 -0
  127. package/dist/scheduler-ical.d.ts +30 -0
  128. package/dist/scheduler-ical.js +180 -0
  129. package/dist/scheduler-model.d.ts +269 -0
  130. package/dist/scheduler-model.js +602 -0
  131. package/dist/scheduler-view.svelte.d.ts +22 -0
  132. package/dist/scheduler-view.svelte.js +13 -0
  133. package/dist/spreadsheet.js +20 -12
  134. package/dist/ui-app.types.d.ts +3 -0
  135. package/package.json +14 -10
  136. package/src/DockNodeView.svelte +306 -0
  137. package/src/DockPopoutHost.svelte +17 -0
  138. package/src/FlexRender.svelte +96 -96
  139. package/src/GridFooter.svelte +178 -178
  140. package/src/GridMenus.svelte +83 -3
  141. package/src/SvAutoComplete.svelte +5 -3
  142. package/src/SvCarousel.svelte +141 -163
  143. package/src/SvCarousel.test.ts +68 -0
  144. package/src/SvColorInput.svelte +187 -186
  145. package/src/SvComboBox.svelte +7 -3
  146. package/src/SvCommand.svelte +28 -85
  147. package/src/SvCommand.test.ts +78 -0
  148. package/src/SvContextMenu.svelte +116 -116
  149. package/src/SvCountryInput.svelte +1 -1
  150. package/src/SvDateRangeInput.svelte +1 -1
  151. package/src/SvDateTimePicker.svelte +9 -3
  152. package/src/SvDockLayout.svelte +244 -0
  153. package/src/SvDockManager.svelte +922 -0
  154. package/src/SvDrawer.svelte +250 -228
  155. package/src/SvDropDownList.svelte +85 -13
  156. package/src/SvField.svelte +21 -4
  157. package/src/SvForm.svelte +201 -91
  158. package/src/SvForm.test.ts +299 -0
  159. package/src/SvGrid.controller.svelte.ts +3158 -3034
  160. package/src/SvGrid.css +193 -5
  161. package/src/SvGrid.svelte +3131 -2786
  162. package/src/SvGrid.types.ts +1489 -1027
  163. package/src/SvGridBoard.svelte +2339 -2339
  164. package/src/SvGridChart.svelte +1724 -1724
  165. package/src/SvGridChartPanel.svelte +485 -485
  166. package/src/SvGridDropdown.svelte +689 -682
  167. package/src/SvGridSelect.svelte +1 -0
  168. package/src/SvGroupCell.svelte +109 -109
  169. package/src/SvListBox.svelte +36 -14
  170. package/src/SvMaskedInput.svelte +4 -3
  171. package/src/SvMenu.svelte +124 -124
  172. package/src/SvMenu.test.ts +97 -73
  173. package/src/SvMenuList.svelte +117 -165
  174. package/src/SvModal.svelte +12 -29
  175. package/src/SvModal.test.ts +82 -0
  176. package/src/SvMultiSelect.svelte +1 -0
  177. package/src/SvNumberInput.svelte +131 -130
  178. package/src/SvOtpInput.svelte +158 -158
  179. package/src/SvPagination.svelte +18 -18
  180. package/src/SvPasswordInput.svelte +3 -2
  181. package/src/SvPhoneInput.svelte +3 -2
  182. package/src/SvPopover.svelte +139 -139
  183. package/src/SvRowGroupPanel.svelte +170 -170
  184. package/src/SvScrollArea.svelte +61 -61
  185. package/src/SvStepper.svelte +6 -9
  186. package/src/SvTagsInput.svelte +3 -2
  187. package/src/SvTooltip.svelte +127 -138
  188. package/src/SvTooltip.test.ts +73 -0
  189. package/src/SvTour.svelte +204 -176
  190. package/src/SvTree.svelte +377 -377
  191. package/src/SvTreeSelect.svelte +230 -229
  192. package/src/a11y.contract.test.ts +49 -49
  193. package/src/a11y.test.ts +59 -59
  194. package/src/a11y.ts +61 -61
  195. package/src/build-api.ts +870 -847
  196. package/src/cell-formatting.ts +169 -169
  197. package/src/cell-render.ts +469 -469
  198. package/src/chart-export.ts +201 -201
  199. package/src/chart.ts +2296 -2296
  200. package/src/collaboration.test.ts +104 -104
  201. package/src/collaboration.ts +167 -167
  202. package/src/columns.ts +5 -0
  203. package/src/conditional-formatting.test.ts +104 -0
  204. package/src/conditional-formatting.ts +176 -19
  205. package/src/core.performance.test.ts +30 -30
  206. package/src/core.ts +1111 -1111
  207. package/src/createAutocomplete.svelte.ts +132 -132
  208. package/src/createCarousel.svelte.ts +104 -0
  209. package/src/createCombobox.svelte.ts +179 -179
  210. package/src/createCommand.svelte.ts +125 -0
  211. package/src/createCountryInput.svelte.ts +157 -157
  212. package/src/createDropdownList.svelte.ts +162 -162
  213. package/src/createForm.svelte.ts +333 -0
  214. package/src/createGrid.svelte.ts +42 -42
  215. package/src/createGrid.test.ts +10 -10
  216. package/src/createGridState.svelte.ts +17 -17
  217. package/src/createMenu.svelte.ts +202 -0
  218. package/src/createOverlay.svelte.ts +101 -0
  219. package/src/createPagination.test.ts +65 -0
  220. package/src/createPagination.ts +84 -0
  221. package/src/createPopoverSelect.svelte.ts +206 -210
  222. package/src/createStepper.test.ts +62 -0
  223. package/src/createStepper.ts +61 -0
  224. package/src/createTooltip.svelte.ts +86 -0
  225. package/src/createTree.svelte.ts +319 -319
  226. package/src/datetime/timezone.test.ts +69 -0
  227. package/src/datetime/timezone.ts +134 -0
  228. package/src/dock-context.ts +47 -0
  229. package/src/dock-manager-model.test.ts +275 -0
  230. package/src/dock-manager-model.ts +593 -0
  231. package/src/dock-model.test.ts +163 -0
  232. package/src/dock-model.ts +368 -0
  233. package/src/dock-popout.ts +94 -0
  234. package/src/editing.test.ts +859 -859
  235. package/src/editing.ts +675 -675
  236. package/src/editor-contract.ts +2 -0
  237. package/src/export-data-api.test.ts +126 -126
  238. package/src/export-format.test.ts +107 -107
  239. package/src/export-format.ts +598 -598
  240. package/src/filter-operators.test.ts +59 -0
  241. package/src/filter-operators.ts +35 -2
  242. package/src/filtering/excel-filters.test.ts +86 -1
  243. package/src/filtering/excel-filters.ts +65 -0
  244. package/src/flex-render.ts +3 -3
  245. package/src/form-field.ts +87 -0
  246. package/src/index.ts +623 -469
  247. package/src/keyboard.test.ts +59 -59
  248. package/src/keyboard.ts +97 -97
  249. package/src/list-nav.test.ts +49 -0
  250. package/src/list-nav.ts +29 -0
  251. package/src/list-option.ts +2 -0
  252. package/src/menu-item.ts +21 -0
  253. package/src/menus.test.ts +48 -0
  254. package/src/menus.ts +639 -582
  255. package/src/merge-objects.ts +48 -48
  256. package/src/overlays.test.ts +90 -90
  257. package/src/panel-resize.ts +52 -0
  258. package/src/popover.bounds.test.ts +93 -0
  259. package/src/popover.ts +51 -7
  260. package/src/recurrence.test.ts +88 -1
  261. package/src/recurrence.ts +166 -15
  262. package/src/render-component.ts +28 -28
  263. package/src/row-drag.ts +10 -0
  264. package/src/scheduler-ical.test.ts +68 -0
  265. package/src/scheduler-ical.ts +180 -0
  266. package/src/scheduler-model.test.ts +546 -0
  267. package/src/scheduler-model.ts +853 -0
  268. package/src/scheduler-view.svelte.ts +33 -0
  269. package/src/selection.test.ts +754 -754
  270. package/src/selection.ts +600 -600
  271. package/src/server-data-source.test.ts +289 -289
  272. package/src/server-data-source.ts +413 -413
  273. package/src/sparkline.test.ts +68 -68
  274. package/src/sparkline.ts +169 -169
  275. package/src/spreadsheet.test.ts +489 -489
  276. package/src/spreadsheet.ts +312 -304
  277. package/src/static-functions.ts +11 -11
  278. package/src/subscribe.ts +38 -38
  279. package/src/svgrid-wrapper.types.ts +501 -501
  280. package/src/svgrid.behavior.test.ts +706 -706
  281. package/src/svgrid.charting.test.ts +527 -527
  282. package/src/svgrid.effect-driven-filter.svelte.test.ts +116 -0
  283. package/src/svgrid.features.test.ts +157 -157
  284. package/src/svgrid.new-features.wrapper.test.ts +251 -251
  285. package/src/svgrid.wrapper.test.ts +40 -40
  286. package/src/test-setup.ts +62 -62
  287. package/src/themes/index.ts +159 -159
  288. package/src/ui-app.types.ts +3 -0
  289. package/src/virtual.test.ts +6 -4
  290. package/src/virtualization/column-virtualizer.test.ts +27 -27
  291. package/src/virtualization/column-virtualizer.ts +30 -30
  292. package/src/virtualization/svelte-virtualizer.svelte.ts +26 -26
  293. package/src/virtualization/types.ts +30 -30
  294. package/src/virtualization/virtualizer.test.ts +47 -47
  295. package/src/virtualization/virtualizer.ts +296 -296
package/src/build-api.ts CHANGED
@@ -1,847 +1,870 @@
1
- // build-api handlers extracted from the controller. Imperative event handlers
2
- // reading/writing controller state via the `ctx` handle; the reactive core
3
- // ($state/$derived/$effect) stays in the controller.
4
- import {
5
- applyExcelFilter,
6
- normalizeForFilter,
7
- createColumnVirtualizer,
8
- createCoreRowModel,
9
- createExpandedRowModel,
10
- createFilteredRowModel,
11
- createGroupedRowModel,
12
- createPaginatedRowModel,
13
- createSvelteVirtualizer,
14
- createSortedRowModel,
15
- createSvGrid,
16
- filterFns,
17
- getGridCellA11yProps,
18
- getGridCellDomId,
19
- getGridHeaderA11yProps,
20
- getGridRootA11yProps,
21
- getGridRowA11yProps,
22
- parseEditorValue,
23
- normalizeEditorOptions,
24
- sortFns,
25
- tableFeatures,
26
- rowSortingFeature,
27
- columnFilteringFeature,
28
- columnGroupingFeature,
29
- type CellContext,
30
- type EditorContext,
31
- type CellEditorOption,
32
- type CellEditorType,
33
- type CellFormatter,
34
- type CellFormatConfig,
35
- type Column,
36
- type ColumnDef,
37
- type Row,
38
- type RowData,
39
- type SvGridApi,
40
- type TableFeatures,
41
- } from "./index";
42
- import "./sv-grid-scrollbar";
43
- import type { Snippet } from "svelte";
44
- import { getKeyboardIntent, getNextActiveCell } from "./keyboard";
45
- import {
46
- formatNumericWithConfig,
47
- getDateFormatter,
48
- resolveDatePattern,
49
- } from "./cell-formatting";
50
- import {
51
- projectGridRows,
52
- serializeDelimited,
53
- serializeJson,
54
- serializeMarkdown,
55
- downloadTextFile,
56
- copyTextToClipboard,
57
- type GridExportColumn,
58
- type GridExportScope,
59
- type GridExportOptions,
60
- type GridClipboardOptions,
61
- } from "./export-format";
62
- import {
63
- RenderSnippetConfig,
64
- RenderComponentConfig,
65
- } from "./render-component";
66
- import { buildFillPattern } from "./fill-patterns";
67
- import { buildSparkline, toSparklineValues } from "./sparkline";
68
- import {
69
- resolveCellFormat,
70
- computeColumnStat,
71
- formatsNeedingStats,
72
- type ColumnStat,
73
- type ResolvedCellFormat,
74
- } from "./conditional-formatting";
75
- import SvGridDropdown from "./SvGridDropdown.svelte";
76
- import type {
77
- Props,
78
- SelectionPoint,
79
- SelectionRange,
80
- CellEditState,
81
- FilterOperator,
82
- FilterOption,
83
- MenuPosition,
84
- } from "./SvGrid.types";
85
- import {
86
- cfTextStyle,
87
- fmtStat,
88
- getCellKey,
89
- resolveClassList,
90
- toDateInputValue,
91
- toDateTimeLocalInputValue,
92
- getEditableInputValue,
93
- getEditorInputType,
94
- toValueArray,
95
- getOptionLabel,
96
- getOptionColor,
97
- colorfulChipStyle,
98
- getEditorClass,
99
- asDate,
100
- clampMenuX,
101
- cssEscape,
102
- rawToNumber,
103
- formatFacetNumber,
104
- formatFacetDate,
105
- } from "./SvGrid.helpers";
106
- import { createClipboard } from "./clipboard";
107
- import {
108
- filterOperatorOptions,
109
- fallbackOperatorOption,
110
- TEXT_OPERATORS,
111
- NUMBER_OPERATORS,
112
- DATE_OPERATORS,
113
- CHECKBOX_OPERATORS,
114
- operatorOption,
115
- operatorsForColumn,
116
- defaultOperatorFor,
117
- operatorLabelFor,
118
- } from "./filter-operators";
119
- import {
120
- type FacetBucket,
121
- isBucketableColumn,
122
- buildBuckets,
123
- isInBucket,
124
- } from "./facet-buckets";
125
- import {
126
- getColumnBaseValue,
127
- isGroupRow,
128
- toolPanelHeaderLabel,
129
- formatSummaryNumeric,
130
- getColumnAlign,
131
- getPinnedCellValue,
132
- getColumnAccessorValue,
133
- columnDefMatchesId,
134
- } from "./cell-values";
135
-
136
- export function createGridApi<
137
- TFeatures extends TableFeatures = TableFeatures,
138
- TData extends RowData = RowData,
139
- >(ctx: any) {
140
- function buildApi(): SvGridApi<TFeatures, TData> {
141
- const findColumn = (columnId: string) =>
142
- ctx.grid.getAllColumns().find((column: any) => column.id === columnId);
143
-
144
- // --- Free data export (CSV / TSV / JSON + clipboard) helpers ----------
145
- // Snapshot the VISIBLE columns (field + label + format + align) so the
146
- // exporter reproduces on-screen values. Mirrors getColumns()'s visible set.
147
- const exportColumns = (): GridExportColumn[] =>
148
- ctx.allColumns
149
- .map((c: any) => ({
150
- field: (c.columnDef as { field?: string }).field as string,
151
- header:
152
- typeof c.columnDef.header === "string" ? c.columnDef.header : c.id,
153
- format: c.columnDef.format,
154
- align: getColumnAlign(c),
155
- }))
156
- .filter((c: GridExportColumn) => !!c.field);
157
- // Rows for the requested scope: current view / checked / full dataset.
158
- const exportScopeRows = (scope: GridExportScope): TData[] => {
159
- if (scope === "all") return ctx.internalData as TData[];
160
- const out: TData[] = [];
161
- for (const row of ctx.allRows) {
162
- if (isGroupRow(row)) continue;
163
- if (scope === "selected" && !ctx.rowSelectionState[row.id]) continue;
164
- out.push(row.original as TData);
165
- }
166
- return out;
167
- };
168
- const projectForExport = (
169
- scope: GridExportScope,
170
- columns: ReadonlyArray<string> | undefined,
171
- rawValues: boolean | undefined,
172
- ) =>
173
- projectGridRows(
174
- exportScopeRows(scope) as ReadonlyArray<Record<string, unknown>>,
175
- exportColumns(),
176
- { columns, rawValues },
177
- );
178
-
179
- return {
180
- getCellValue(rowIndex, columnId) {
181
- const row = ctx.internalData[rowIndex];
182
- const column = findColumn(columnId);
183
- if (!row || !column) return undefined;
184
- if (column.columnDef.fieldFn)
185
- return column.columnDef.fieldFn(row);
186
- if (column.columnDef.field)
187
- return (row as Record<string, unknown>)[column.columnDef.field];
188
- return undefined;
189
- },
190
- setCellValue(rowIndex, columnId, value) {
191
- const row = ctx.internalData[rowIndex];
192
- const column = findColumn(columnId);
193
- if (!row || !column?.columnDef.field) return;
194
- const next = ctx.internalData.slice() as Array<TData>;
195
- next[rowIndex] = { ...row, [column.columnDef.field]: value } as TData;
196
- ctx.internalData = next;
197
- },
198
- startEditing(rowIndex, columnId) {
199
- return ctx.startEditing(rowIndex, columnId);
200
- },
201
- stopEditing(cancel) {
202
- return ctx.stopEditing(cancel ?? false);
203
- },
204
- selectCells(ranges) {
205
- // Multiple rectangles are supported. Empty array clears everything.
206
- if (!ranges || ranges.length === 0) {
207
- ctx.selectionRange = { anchor: null, focus: null };
208
- ctx.selectionRanges = [];
209
- return;
210
- }
211
- const rowCount = ctx.internalData.length;
212
- const colCount = ctx.allColumns.length;
213
- if (rowCount === 0 || colCount === 0) return;
214
- // Clamp to the visible grid bounds so callers can pass open-ended
215
- // values like `[0, 0, Infinity, Infinity]` to mean "select all".
216
- const toRange = ([r1, c1, r2, c2]: readonly [number, number, number, number]) => ({
217
- anchor: {
218
- rowIndex: Math.max(0, Math.min(rowCount - 1, Math.min(r1, r2))),
219
- colIndex: Math.max(0, Math.min(colCount - 1, Math.min(c1, c2))),
220
- },
221
- focus: {
222
- rowIndex: Math.max(0, Math.min(rowCount - 1, Math.max(r1, r2))),
223
- colIndex: Math.max(0, Math.min(colCount - 1, Math.max(c1, c2))),
224
- },
225
- });
226
- const built = ranges.map(toRange);
227
- // Last range is the active one; the rest are committed extras.
228
- ctx.selectionRanges = built.slice(0, -1);
229
- const active = built[built.length - 1]!;
230
- ctx.selectionRange = active;
231
- // Move the active cell to the last range's top-left so keyboard
232
- // navigation continues from there.
233
- ctx.setActiveCell(active.anchor.rowIndex, active.anchor.colIndex);
234
- },
235
- getSelected() {
236
- return ctx
237
- .getSelectionRects()
238
- .map((r: { minRow: number; minCol: number; maxRow: number; maxCol: number }) =>
239
- [r.minRow, r.minCol, r.maxRow, r.maxCol] as [number, number, number, number],
240
- );
241
- },
242
- openChart() {
243
- ctx.chartPanelOpen = true;
244
- },
245
- closeChart() {
246
- ctx.chartPanelOpen = false;
247
- },
248
- getChartSpec() {
249
- return ctx.chartSpec;
250
- },
251
- chartRange(ranges) {
252
- if (ranges && ranges.length) this.selectCells(ranges);
253
- ctx.chartPanelOpen = true;
254
- },
255
- configureChart(config) {
256
- if (ctx.chartingEnabled) ctx.applyChartConfig(config);
257
- },
258
- setChartAiHandler(handler) {
259
- ctx.chartAiHandler = handler ?? null;
260
- },
261
- addRow(row, position = "bottom") {
262
- this.addRows([row], position);
263
- },
264
- addRows(rows, position = "bottom") {
265
- const next = ctx.internalData.slice() as Array<TData>;
266
- if (position === "top") next.unshift(...rows);
267
- else if (position === "bottom") next.push(...rows);
268
- else if (typeof position === "number")
269
- next.splice(Math.max(0, Math.min(position, next.length)), 0, ...rows);
270
- ctx.internalData = next;
271
- },
272
- removeRow(rowIndex) {
273
- this.removeRows([rowIndex]);
274
- },
275
- removeRows(rowIndices) {
276
- const drop = new Set(rowIndices);
277
- ctx.internalData = ctx.internalData.filter((_: any, i: any) => !drop.has(i));
278
- },
279
- applyTransaction(tx) {
280
- const getId = ctx.props.getRowId;
281
- let next = ctx.internalData.slice() as Array<TData>;
282
- let added = 0;
283
- let updated = 0;
284
- let removed = 0;
285
-
286
- if (tx.remove?.length) {
287
- const removeIds = new Set<string>();
288
- const removeRefs = new Set<TData>();
289
- for (const r of tx.remove) {
290
- if (typeof r === "string") removeIds.add(r);
291
- else removeRefs.add(r as TData);
292
- }
293
- next = next.filter((row, i) => {
294
- const hit =
295
- removeRefs.has(row) ||
296
- (getId ? removeIds.has(getId(row, i)) : false);
297
- if (hit) removed += 1;
298
- return !hit;
299
- });
300
- }
301
-
302
- if (tx.update?.length && getId) {
303
- const byId = new Map<string, TData>();
304
- for (const u of tx.update) byId.set(getId(u, 0), u);
305
- next = next.map((row, i) => {
306
- const u = byId.get(getId(row, i));
307
- if (u) {
308
- updated += 1;
309
- return u;
310
- }
311
- return row;
312
- });
313
- }
314
-
315
- if (tx.add?.length) {
316
- next.push(...(tx.add as Array<TData>));
317
- added += tx.add.length;
318
- }
319
-
320
- ctx.internalData = next;
321
- return { added, updated, removed };
322
- },
323
- addColumn(column, position = "right") {
324
- this.addColumns([column], position);
325
- },
326
- addColumns(columns, position = "right") {
327
- const next = ctx.internalColumns.slice();
328
- if (position === "left") next.unshift(...columns);
329
- else if (position === "right") next.push(...columns);
330
- else if (typeof position === "number")
331
- next.splice(
332
- Math.max(0, Math.min(position, next.length)),
333
- 0,
334
- ...columns,
335
- );
336
- ctx.internalColumns = next;
337
- },
338
- removeColumn(columnId) {
339
- ctx.internalColumns = ctx.internalColumns.filter(
340
- (def: any) => !columnDefMatchesId(def, columnId),
341
- );
342
- },
343
- setColumnVisible(columnId, visible) {
344
- if (visible) {
345
- const next = { ...ctx.hiddenColumns };
346
- delete next[columnId];
347
- ctx.hiddenColumns = next;
348
- } else {
349
- ctx.hiddenColumns = { ...ctx.hiddenColumns, [columnId]: true };
350
- }
351
- },
352
- isColumnVisible(columnId) {
353
- return !ctx.hiddenColumns[columnId];
354
- },
355
- setSort(columnId, direction) {
356
- // Honour the column's `sortable` opt-out. Asking the engine
357
- // whether the column can sort folds in both the feature toggle
358
- // AND the per-column flag.
359
- const col = ctx.allColumns.find((c: any) => c.id === columnId);
360
- if (col && !col.getCanSort?.()) return;
361
- const clauses = direction
362
- ? [{ id: columnId, desc: direction === "desc" }]
363
- : (ctx.grid.getState().sorting ?? []).filter(
364
- (entry: { id: string }) => entry.id !== columnId,
365
- );
366
- ctx.grid.store.setState((prev: any) => ({ ...prev, sorting: clauses }));
367
- },
368
- clearSort() {
369
- ctx.grid.store.setState((prev: any) => ({ ...prev, sorting: [] }));
370
- },
371
- setGroupBy(columnIds) {
372
- ctx.grid.setGrouping([...columnIds]);
373
- },
374
- setFilter(columnId, filter) {
375
- // Honour the column's `filterable` opt-out. Same approach as
376
- // `setSort` - `getCanFilter()` already combines the feature flag
377
- // and the per-column field.
378
- const col = ctx.allColumns.find((c: any) => c.id === columnId);
379
- if (col && !col.getCanFilter?.()) return;
380
- if (!filter) {
381
- this.clearFilter(columnId);
382
- return;
383
- }
384
- ctx.filterMenuValues = {
385
- ...ctx.filterMenuValues,
386
- [columnId]: {
387
- operator: filter.operator,
388
- value: filter.value ?? "",
389
- ...(filter.operator === "between"
390
- ? { valueTo: filter.valueTo ?? "" }
391
- : {}),
392
- // Optional second condition + join (multi-condition filtering).
393
- ...(filter.operator2
394
- ? {
395
- operator2: filter.operator2,
396
- value2: filter.value2 ?? "",
397
- join: filter.join ?? "AND",
398
- ...(filter.operator2 === "between"
399
- ? { valueTo2: filter.valueTo2 ?? "" }
400
- : {}),
401
- }
402
- : {}),
403
- },
404
- };
405
- },
406
- setFacetFilter(columnId, values) {
407
- // Empty array OR null both mean "clear this column's facet" -
408
- // mirrors how the column menu treats unchecking the last value.
409
- if (!values || values.length === 0) {
410
- if (ctx.valueFilters[columnId]) {
411
- const next = { ...ctx.valueFilters };
412
- delete next[columnId];
413
- ctx.valueFilters = next;
414
- }
415
- return;
416
- }
417
- ctx.valueFilters = {
418
- ...ctx.valueFilters,
419
- [columnId]: new Set(values),
420
- };
421
- },
422
- clearFilter(columnId) {
423
- ctx.clearColumnFilter(columnId);
424
- },
425
- clearAllFilters() {
426
- // Wipe every filter surface in one go: column-menu, filter-row, set
427
- // filters, and the global search box.
428
- ctx.filterMenuValues = {};
429
- ctx.filterRowValues = {};
430
- ctx.valueFilters = {};
431
- if (ctx.globalFilter !== "") ctx.globalFilter = "";
432
- },
433
- getFilters() {
434
- // Return a defensive copy so callers can't mutate internal state.
435
- const out: Record<
436
- string,
437
- { operator: FilterOperator; value: string; valueTo?: string }
438
- > = {};
439
- for (const [columnId, filter] of Object.entries(ctx.filterMenuValues as Record<string, { operator: FilterOperator; value: string; valueTo?: string }>)) {
440
- out[columnId] = {
441
- operator: filter.operator,
442
- value: filter.value,
443
- ...(filter.operator === "between" && filter.valueTo
444
- ? { valueTo: filter.valueTo }
445
- : {}),
446
- };
447
- }
448
- return out;
449
- },
450
- getDisplayedRows() {
451
- // `allRows` is the final, post-filter, post-sort, post-group,
452
- // post-pagination list - exactly what the body renders. Skip group
453
- // header rows (they wrap an aggregate, not a TData row).
454
- const out: TData[] = [];
455
- for (const row of ctx.allRows) {
456
- if (row.subRows && row.subRows.length > 0) continue;
457
- out.push(row.original as TData);
458
- }
459
- return out;
460
- },
461
- getData() {
462
- return ctx.internalData;
463
- },
464
- getColumns() {
465
- // Snapshot every column with its human-readable label and
466
- // visibility flag. Used by external code (exporters, column
467
- // pickers) so they don't have to re-walk the columnDef tree.
468
- // Hidden columns ARE included (with `visible: false`) so a column
469
- // picker can re-enable them - callers that only want what's
470
- // rendered filter by `.visible` (e.g. the exporter does).
471
- const describe = (c: any, visible: boolean) => ({
472
- id: c.id,
473
- field: (c.columnDef as { field?: string }).field,
474
- header:
475
- typeof c.columnDef.header === "string"
476
- ? c.columnDef.header
477
- : c.id,
478
- visible,
479
- // Surface the format + alignment so exporters can reproduce the
480
- // on-screen display value (currency symbol, date pattern, etc.)
481
- // instead of dumping raw values. `getColumnAlign` applies the same
482
- // editorType-based default the body uses.
483
- format: c.columnDef.format,
484
- align: getColumnAlign(c),
485
- });
486
- // Visible columns first, in their current visual order...
487
- const out = ctx.allColumns.map((c: any) => describe(c, true));
488
- // ...then any column hidden via `visible: false` / setColumnVisible.
489
- // Those aren't in `allColumns` (that list is visible-only), so pull
490
- // them straight from the engine's leaf set.
491
- const visibleIds = new Set(out.map((c: { id: string }) => c.id));
492
- for (const c of ctx.grid.getAllColumns() as any[]) {
493
- if (!visibleIds.has(c.id)) out.push(describe(c, false));
494
- }
495
- return out;
496
- },
497
- // ---- Free data export (CSV / TSV / JSON + clipboard) -----------------
498
- async exportCsv(options: GridExportOptions = {}) {
499
- const { records, fields } = projectForExport(
500
- options.rows ?? "displayed",
501
- options.columns,
502
- options.rawValues,
503
- );
504
- const text = await serializeDelimited(records, fields, {
505
- csv: { delimiter: ",", bom: options.bom ?? true },
506
- signal: options.signal,
507
- onProgress: options.onProgress,
508
- });
509
- if (options.download !== false)
510
- downloadTextFile(text, `${options.filename ?? "grid"}.csv`, "text/csv;charset=utf-8");
511
- return text;
512
- },
513
- async exportTsv(options: GridExportOptions = {}) {
514
- const { records, fields } = projectForExport(
515
- options.rows ?? "displayed",
516
- options.columns,
517
- options.rawValues,
518
- );
519
- const text = await serializeDelimited(records, fields, {
520
- csv: { delimiter: "\t", bom: options.bom ?? true },
521
- signal: options.signal,
522
- onProgress: options.onProgress,
523
- });
524
- if (options.download !== false)
525
- downloadTextFile(text, `${options.filename ?? "grid"}.tsv`, "text/tab-separated-values;charset=utf-8");
526
- return text;
527
- },
528
- async exportJson(options: GridExportOptions = {}) {
529
- const { records, fields } = projectForExport(
530
- options.rows ?? "displayed",
531
- options.columns,
532
- options.rawValues,
533
- );
534
- const text = await serializeJson(records, fields, {
535
- signal: options.signal,
536
- onProgress: options.onProgress,
537
- });
538
- if (options.download !== false)
539
- downloadTextFile(text, `${options.filename ?? "grid"}.json`, "application/json");
540
- return text;
541
- },
542
- async copyToClipboard(options: GridClipboardOptions = {}) {
543
- const { records, fields, align } = projectForExport(
544
- options.rows ?? "displayed",
545
- options.columns,
546
- options.rawValues,
547
- );
548
- const format = options.format ?? "tsv";
549
- const text =
550
- format === "markdown"
551
- ? await serializeMarkdown(records, fields, { align })
552
- : await serializeDelimited(records, fields, {
553
- csv: { delimiter: format === "csv" ? "," : "\t", bom: false },
554
- });
555
- await copyTextToClipboard(text);
556
- return text;
557
- },
558
- clearRowSelection() {
559
- // Wipe the internal selection map AND emit the change so any
560
- // consumer holding a derived `selectedRows` resets too.
561
- ctx.grid.setRowSelection(() => ({}));
562
- ctx.props.onRowSelectionChange?.({}, []);
563
- },
564
- setColumnWidth(columnId: string, width: number) {
565
- const clamped = Math.max(ctx.MIN_COLUMN_WIDTH, Math.floor(width));
566
- ctx.columnWidths = { ...ctx.columnWidths, [columnId]: clamped };
567
- },
568
- getColumnWidths() {
569
- // Resolve every column's effective width by consulting both the
570
- // resize overrides (`columnWidths`) AND the columnDef defaults,
571
- // so the snapshot is round-trippable through setColumnWidth.
572
- const out: Record<string, number> = {};
573
- for (const c of ctx.allColumns) out[c.id] = ctx.getColumnWidth(c.id);
574
- return out;
575
- },
576
- autosizeColumn(columnId: string) {
577
- ctx.autosizeColumn(columnId);
578
- },
579
- autosizeAllColumns() {
580
- ctx.autosizeAllColumns();
581
- },
582
- setColumnPinning(pinning) {
583
- // Defensive copy + dedupe so callers can't mutate our state.
584
- const left = Array.from(new Set(pinning.left ?? []));
585
- const right = Array.from(new Set(pinning.right ?? []));
586
- ctx.columnPinning = { left, right };
587
- },
588
- getColumnPinning() {
589
- return {
590
- left: ctx.columnPinning.left.slice(),
591
- right: ctx.columnPinning.right.slice(),
592
- };
593
- },
594
- // ---- Column reorder
595
- setColumnOrder(order) {
596
- ctx.setColumnOrderInternal(order);
597
- },
598
- getColumnOrder() {
599
- return ctx.getCurrentColumnOrder();
600
- },
601
- setRowExpanded(id, expanded) {
602
- ctx.grid.setExpanded((prev: any) => ({ ...prev, [id]: !!expanded }));
603
- },
604
- expandAllGroups() {
605
- // Walk the current grouped row model and flip every group node on.
606
- const next: Record<string, boolean> = {};
607
- const walk = (rows: ReadonlyArray<{ id: string; getCanExpand?: () => boolean; subRows?: ReadonlyArray<unknown> }>) => {
608
- for (const row of rows) {
609
- if (row.getCanExpand?.()) next[row.id] = true;
610
- const sub = row.subRows as ReadonlyArray<{ id: string; getCanExpand?: () => boolean; subRows?: ReadonlyArray<unknown> }> | undefined;
611
- if (sub && sub.length > 0) walk(sub);
612
- }
613
- };
614
- walk(ctx.grid.getRowModel().rows as unknown as ReadonlyArray<{ id: string; getCanExpand?: () => boolean; subRows?: ReadonlyArray<unknown> }>);
615
- ctx.grid.setExpanded(() => next);
616
- },
617
- collapseAllGroups() {
618
- ctx.grid.setExpanded(() => ({}));
619
- },
620
- // ---- Undo / redo (history + pointer)
621
- undo() {
622
- if (ctx.historyPtr < 0) return false
623
- const step = ctx.history[ctx.historyPtr]
624
- if (!step) return false
625
- ctx.applyHistoryStep(step, 'undo')
626
- ctx.historyPtr -= 1
627
- ctx.historyVersion += 1
628
- return true
629
- },
630
- redo() {
631
- if (ctx.historyPtr >= ctx.history.length - 1) return false
632
- const step = ctx.history[ctx.historyPtr + 1]
633
- if (!step) return false
634
- ctx.applyHistoryStep(step, 'redo')
635
- ctx.historyPtr += 1
636
- ctx.historyVersion += 1
637
- return true
638
- },
639
- canUndo() { void ctx.historyVersion; return ctx.historyPtr >= 0 },
640
- canRedo() { void ctx.historyVersion; return ctx.historyPtr < ctx.history.length - 1 },
641
- clearHistory() { ctx.history = []; ctx.historyPtr = -1; ctx.historyVersion += 1 },
642
- // ---- Find
643
- openFind() { ctx.findOpen = true },
644
- closeFind() { ctx.findOpen = false; ctx.findQuery = '' },
645
- setFindQuery(q: string) { ctx.findQuery = q; ctx.findHitIndex = 0 },
646
- getFindHits() { return ctx.findHits.slice() },
647
- // ---- Row selection (read + write)
648
- getSelectedRows() {
649
- const out: TData[] = [];
650
- for (const row of ctx.allRows) {
651
- if (isGroupRow(row)) continue;
652
- if (ctx.rowSelectionState[row.id]) out.push(row.original as TData);
653
- }
654
- return out;
655
- },
656
- getSelectedRowIds() {
657
- const out: string[] = [];
658
- for (const row of ctx.allRows) {
659
- if (!isGroupRow(row) && ctx.rowSelectionState[row.id]) out.push(row.id);
660
- }
661
- return out;
662
- },
663
- selectRows(ids, additive = false) {
664
- const set = new Set(ids);
665
- ctx.grid.setRowSelection((prev: any) => {
666
- const next: Record<string, boolean> = additive ? { ...prev } : {};
667
- for (const id of set) next[id] = true;
668
- return next;
669
- });
670
- },
671
- selectAllRows() {
672
- const next: Record<string, boolean> = {};
673
- for (const row of ctx.allRows) if (!isGroupRow(row)) next[row.id] = true;
674
- ctx.grid.setRowSelection(() => next);
675
- },
676
- toggleRowSelected(id) {
677
- ctx.toggleRowSelectionById(id);
678
- },
679
- // ---- Pagination
680
- getPageInfo() {
681
- const { pageIndex, pageSize } = ctx.paginationState;
682
- const total = ctx.allRowsBeforePagination.length;
683
- const pageCount = Math.max(1, Math.ceil(total / Math.max(1, pageSize)));
684
- return { pageIndex, pageSize, pageCount, total };
685
- },
686
- setPage(pageIndex) {
687
- const total = ctx.allRowsBeforePagination.length;
688
- const pageCount = Math.max(
689
- 1,
690
- Math.ceil(total / Math.max(1, ctx.paginationState.pageSize)),
691
- );
692
- ctx.goToPage(Math.max(0, Math.min(pageIndex, pageCount - 1)));
693
- },
694
- nextPage() {
695
- this.setPage(ctx.paginationState.pageIndex + 1);
696
- },
697
- prevPage() {
698
- this.setPage(ctx.paginationState.pageIndex - 1);
699
- },
700
- firstPage() {
701
- this.setPage(0);
702
- },
703
- lastPage() {
704
- const total = ctx.allRowsBeforePagination.length;
705
- const pageCount = Math.max(
706
- 1,
707
- Math.ceil(total / Math.max(1, ctx.paginationState.pageSize)),
708
- );
709
- this.setPage(pageCount - 1);
710
- },
711
- setPageSize(size) {
712
- // Calls the component-level helper (lexical scope), not this method.
713
- ctx.setPageSize(Math.max(1, Math.floor(size)));
714
- },
715
- // ---- Navigation / scrolling
716
- scrollToRow(rowIndex) {
717
- if (!ctx.scrollContainer) return;
718
- const maxIndex = Math.max(0, ctx.allRows.length - 1);
719
- const clamped = Math.max(0, Math.min(rowIndex, maxIndex));
720
- // Logical offset from the virtualizer (honors uniform OR per-row
721
- // sizing), then mapped into the capped DOM scroll space so it lands
722
- // correctly on huge grids where the DOM height is scaled down.
723
- const logical = ctx.virtualizer.getOffsetForIndex(clamped);
724
- ctx.scrollContainer.scrollTop = ctx.logicalToDomRowOffset(logical);
725
- ctx.scheduleScrollSync(
726
- ctx.scrollContainer.scrollTop,
727
- ctx.scrollContainer.scrollLeft,
728
- );
729
- },
730
- getActiveCell() {
731
- const a = ctx.activeCell;
732
- if (!a || a.rowIndex < 0 || a.colIndex < 0) return null;
733
- const col = ctx.allColumns[a.colIndex];
734
- return {
735
- rowIndex: a.rowIndex,
736
- colIndex: a.colIndex,
737
- columnId: col?.id ?? "",
738
- };
739
- },
740
- setActiveCell(rowIndex, colIndex) {
741
- const r = Math.max(0, Math.min(rowIndex, Math.max(0, ctx.allRows.length - 1)));
742
- const c = Math.max(
743
- 0,
744
- Math.min(colIndex, Math.max(0, ctx.allColumns.length - 1)),
745
- );
746
- // Calls the component-level helper (lexical scope), not this method.
747
- ctx.setActiveCell(r, c);
748
- },
749
- // ---- View state (save / restore)
750
- getState() {
751
- return {
752
- sorting: (ctx.grid.getState().sorting ?? []).map(
753
- (s: { id: string; desc: boolean }) => ({ id: s.id, desc: s.desc }),
754
- ),
755
- grouping: [...(ctx.grid.getState().grouping ?? [])],
756
- pagination: {
757
- pageIndex: ctx.paginationState.pageIndex,
758
- pageSize: ctx.paginationState.pageSize,
759
- },
760
- columnWidths: this.getColumnWidths(),
761
- columnPinning: this.getColumnPinning(),
762
- columnOrder: ctx.getCurrentColumnOrder(),
763
- hiddenColumns: Object.keys(ctx.hiddenColumns).filter(
764
- (k) => ctx.hiddenColumns[k],
765
- ),
766
- globalFilter: ctx.globalFilter,
767
- columnFilters: this.getFilters(),
768
- facetFilters: Object.fromEntries(
769
- Object.entries(ctx.valueFilters).map(([k, set]: [string, any]) => [
770
- k,
771
- Array.from(set),
772
- ]),
773
- ),
774
- ...(ctx.chartingEnabled
775
- ? {
776
- chart: {
777
- open: ctx.chartPanelOpen,
778
- type: ctx.chartType,
779
- dimension: ctx.chartDimensionId,
780
- series: ctx.chartSeriesId,
781
- measure: ctx.chartMeasureId,
782
- reduce: ctx.chartReduce,
783
- stacked: ctx.chartStacked,
784
- },
785
- charts: ctx.getChartsState(),
786
- chartActive: ctx.activeChartIndex,
787
- }
788
- : {}),
789
- };
790
- },
791
- setState(state) {
792
- if (state.sorting) {
793
- const sorting = state.sorting.map((s) => ({ id: s.id, desc: s.desc }));
794
- ctx.grid.store.setState((prev: any) => ({ ...prev, sorting }));
795
- }
796
- if (state.grouping) ctx.grid.setGrouping([...state.grouping]);
797
- if (state.pagination) {
798
- const p = state.pagination;
799
- ctx.grid.setPagination((prev: any) => ({ ...prev, ...p }));
800
- }
801
- if (state.columnWidths) ctx.columnWidths = { ...state.columnWidths };
802
- if (state.columnPinning)
803
- ctx.columnPinning = {
804
- left: [...(state.columnPinning.left ?? [])],
805
- right: [...(state.columnPinning.right ?? [])],
806
- };
807
- if (state.columnOrder) ctx.setColumnOrderInternal(state.columnOrder);
808
- if (state.hiddenColumns) {
809
- const next: Record<string, boolean> = {};
810
- for (const id of state.hiddenColumns) next[id] = true;
811
- ctx.hiddenColumns = next;
812
- }
813
- if (state.globalFilter !== undefined) ctx.globalFilter = state.globalFilter;
814
- if (state.columnFilters)
815
- ctx.filterMenuValues = { ...state.columnFilters };
816
- if (state.facetFilters) {
817
- const next: Record<string, Set<string>> = {};
818
- for (const [k, arr] of Object.entries(state.facetFilters))
819
- next[k] = new Set(arr);
820
- ctx.valueFilters = next;
821
- }
822
- if ((state.chart || state.charts) && ctx.chartingEnabled) {
823
- if (Array.isArray(state.charts) && state.charts.length) {
824
- ctx.applyChartsState(state.charts, state.chartActive);
825
- }
826
- if (state.chart) {
827
- const c = state.chart;
828
- ctx.chartPanelOpen = c.open;
829
- ctx.chartType = c.type;
830
- ctx.chartDimensionId = c.dimension;
831
- ctx.chartSeriesId = c.series;
832
- ctx.chartMeasureId = c.measure;
833
- ctx.chartReduce = c.reduce;
834
- ctx.chartStacked = c.stacked;
835
- }
836
- }
837
- },
838
- refresh() {
839
- ctx.grid.store.setState((prev: any) => ({ ...prev }));
840
- },
841
- };
842
- }
843
-
844
- return {
845
- buildApi,
846
- };
847
- }
1
+ // build-api handlers extracted from the controller. Imperative event handlers
2
+ // reading/writing controller state via the `ctx` handle; the reactive core
3
+ // ($state/$derived/$effect) stays in the controller.
4
+ import {
5
+ applyExcelFilter,
6
+ normalizeForFilter,
7
+ createColumnVirtualizer,
8
+ createCoreRowModel,
9
+ createExpandedRowModel,
10
+ createFilteredRowModel,
11
+ createGroupedRowModel,
12
+ createPaginatedRowModel,
13
+ createSvelteVirtualizer,
14
+ createSortedRowModel,
15
+ createSvGrid,
16
+ filterFns,
17
+ getGridCellA11yProps,
18
+ getGridCellDomId,
19
+ getGridHeaderA11yProps,
20
+ getGridRootA11yProps,
21
+ getGridRowA11yProps,
22
+ parseEditorValue,
23
+ normalizeEditorOptions,
24
+ sortFns,
25
+ tableFeatures,
26
+ rowSortingFeature,
27
+ columnFilteringFeature,
28
+ columnGroupingFeature,
29
+ type CellContext,
30
+ type EditorContext,
31
+ type CellEditorOption,
32
+ type CellEditorType,
33
+ type CellFormatter,
34
+ type CellFormatConfig,
35
+ type Column,
36
+ type ColumnDef,
37
+ type Row,
38
+ type RowData,
39
+ type SvGridApi,
40
+ type TableFeatures,
41
+ } from "./index";
42
+ import "./sv-grid-scrollbar";
43
+ import { untrack } from "svelte";
44
+ import type { Snippet } from "svelte";
45
+ import { getKeyboardIntent, getNextActiveCell } from "./keyboard";
46
+ import {
47
+ formatNumericWithConfig,
48
+ getDateFormatter,
49
+ resolveDatePattern,
50
+ } from "./cell-formatting";
51
+ import {
52
+ projectGridRows,
53
+ serializeDelimited,
54
+ serializeJson,
55
+ serializeMarkdown,
56
+ downloadTextFile,
57
+ copyTextToClipboard,
58
+ type GridExportColumn,
59
+ type GridExportScope,
60
+ type GridExportOptions,
61
+ type GridClipboardOptions,
62
+ } from "./export-format";
63
+ import {
64
+ RenderSnippetConfig,
65
+ RenderComponentConfig,
66
+ } from "./render-component";
67
+ import { buildFillPattern } from "./fill-patterns";
68
+ import { buildSparkline, toSparklineValues } from "./sparkline";
69
+ import {
70
+ resolveCellFormat,
71
+ computeColumnStat,
72
+ formatsNeedingStats,
73
+ type ColumnStat,
74
+ type ResolvedCellFormat,
75
+ } from "./conditional-formatting";
76
+ import SvGridDropdown from "./SvGridDropdown.svelte";
77
+ import type {
78
+ Props,
79
+ SelectionPoint,
80
+ SelectionRange,
81
+ CellEditState,
82
+ FilterOperator,
83
+ FilterOption,
84
+ MenuPosition,
85
+ } from "./SvGrid.types";
86
+ import {
87
+ cfTextStyle,
88
+ fmtStat,
89
+ getCellKey,
90
+ resolveClassList,
91
+ toDateInputValue,
92
+ toDateTimeLocalInputValue,
93
+ getEditableInputValue,
94
+ getEditorInputType,
95
+ toValueArray,
96
+ getOptionLabel,
97
+ getOptionColor,
98
+ colorfulChipStyle,
99
+ getEditorClass,
100
+ asDate,
101
+ clampMenuX,
102
+ cssEscape,
103
+ rawToNumber,
104
+ formatFacetNumber,
105
+ formatFacetDate,
106
+ } from "./SvGrid.helpers";
107
+ import { createClipboard } from "./clipboard";
108
+ import {
109
+ filterOperatorOptions,
110
+ fallbackOperatorOption,
111
+ TEXT_OPERATORS,
112
+ NUMBER_OPERATORS,
113
+ DATE_OPERATORS,
114
+ CHECKBOX_OPERATORS,
115
+ operatorOption,
116
+ operatorsForColumn,
117
+ defaultOperatorFor,
118
+ operatorLabelFor,
119
+ } from "./filter-operators";
120
+ import {
121
+ type FacetBucket,
122
+ isBucketableColumn,
123
+ buildBuckets,
124
+ isInBucket,
125
+ } from "./facet-buckets";
126
+ import {
127
+ getColumnBaseValue,
128
+ isGroupRow,
129
+ toolPanelHeaderLabel,
130
+ formatSummaryNumeric,
131
+ getColumnAlign,
132
+ getPinnedCellValue,
133
+ getColumnAccessorValue,
134
+ columnDefMatchesId,
135
+ } from "./cell-values";
136
+
137
+ export function createGridApi<
138
+ TFeatures extends TableFeatures = TableFeatures,
139
+ TData extends RowData = RowData,
140
+ >(ctx: any) {
141
+ function buildApi(): SvGridApi<TFeatures, TData> {
142
+ const findColumn = (columnId: string) =>
143
+ ctx.grid.getAllColumns().find((column: any) => column.id === columnId);
144
+
145
+ // --- Free data export (CSV / TSV / JSON + clipboard) helpers ----------
146
+ // Snapshot the VISIBLE columns (field + label + format + align) so the
147
+ // exporter reproduces on-screen values. Mirrors getColumns()'s visible set.
148
+ const exportColumns = (): GridExportColumn[] =>
149
+ ctx.allColumns
150
+ .map((c: any) => ({
151
+ field: (c.columnDef as { field?: string }).field as string,
152
+ header:
153
+ typeof c.columnDef.header === "string" ? c.columnDef.header : c.id,
154
+ format: c.columnDef.format,
155
+ align: getColumnAlign(c),
156
+ }))
157
+ .filter((c: GridExportColumn) => !!c.field);
158
+ // Rows for the requested scope: current view / checked / full dataset.
159
+ const exportScopeRows = (scope: GridExportScope): TData[] => {
160
+ if (scope === "all") return ctx.internalData as TData[];
161
+ const out: TData[] = [];
162
+ for (const row of ctx.allRows) {
163
+ if (isGroupRow(row)) continue;
164
+ if (scope === "selected" && !ctx.rowSelectionState[row.id]) continue;
165
+ out.push(row.original as TData);
166
+ }
167
+ return out;
168
+ };
169
+ const projectForExport = (
170
+ scope: GridExportScope,
171
+ columns: ReadonlyArray<string> | undefined,
172
+ rawValues: boolean | undefined,
173
+ ) =>
174
+ projectGridRows(
175
+ exportScopeRows(scope) as ReadonlyArray<Record<string, unknown>>,
176
+ exportColumns(),
177
+ { columns, rawValues },
178
+ );
179
+
180
+ return {
181
+ getCellValue(rowIndex, columnId) {
182
+ const row = ctx.internalData[rowIndex];
183
+ const column = findColumn(columnId);
184
+ if (!row || !column) return undefined;
185
+ if (column.columnDef.fieldFn)
186
+ return column.columnDef.fieldFn(row);
187
+ if (column.columnDef.field)
188
+ return (row as Record<string, unknown>)[column.columnDef.field];
189
+ return undefined;
190
+ },
191
+ setCellValue(rowIndex, columnId, value) {
192
+ const row = ctx.internalData[rowIndex];
193
+ const column = findColumn(columnId);
194
+ if (!row || !column?.columnDef.field) return;
195
+ const next = ctx.internalData.slice() as Array<TData>;
196
+ next[rowIndex] = { ...row, [column.columnDef.field]: value } as TData;
197
+ ctx.internalData = next;
198
+ },
199
+ startEditing(rowIndex, columnId) {
200
+ return ctx.startEditing(rowIndex, columnId);
201
+ },
202
+ stopEditing(cancel) {
203
+ return ctx.stopEditing(cancel ?? false);
204
+ },
205
+ selectCells(ranges) {
206
+ // Multiple rectangles are supported. Empty array clears everything.
207
+ if (!ranges || ranges.length === 0) {
208
+ ctx.selectionRange = { anchor: null, focus: null };
209
+ ctx.selectionRanges = [];
210
+ return;
211
+ }
212
+ const rowCount = ctx.internalData.length;
213
+ const colCount = ctx.allColumns.length;
214
+ if (rowCount === 0 || colCount === 0) return;
215
+ // Clamp to the visible grid bounds so callers can pass open-ended
216
+ // values like `[0, 0, Infinity, Infinity]` to mean "select all".
217
+ const toRange = ([r1, c1, r2, c2]: readonly [number, number, number, number]) => ({
218
+ anchor: {
219
+ rowIndex: Math.max(0, Math.min(rowCount - 1, Math.min(r1, r2))),
220
+ colIndex: Math.max(0, Math.min(colCount - 1, Math.min(c1, c2))),
221
+ },
222
+ focus: {
223
+ rowIndex: Math.max(0, Math.min(rowCount - 1, Math.max(r1, r2))),
224
+ colIndex: Math.max(0, Math.min(colCount - 1, Math.max(c1, c2))),
225
+ },
226
+ });
227
+ const built = ranges.map(toRange);
228
+ // Last range is the active one; the rest are committed extras.
229
+ ctx.selectionRanges = built.slice(0, -1);
230
+ const active = built[built.length - 1]!;
231
+ ctx.selectionRange = active;
232
+ // Move the active cell to the last range's top-left so keyboard
233
+ // navigation continues from there.
234
+ ctx.setActiveCell(active.anchor.rowIndex, active.anchor.colIndex);
235
+ },
236
+ getSelected() {
237
+ return ctx
238
+ .getSelectionRects()
239
+ .map((r: { minRow: number; minCol: number; maxRow: number; maxCol: number }) =>
240
+ [r.minRow, r.minCol, r.maxRow, r.maxCol] as [number, number, number, number],
241
+ );
242
+ },
243
+ openChart() {
244
+ ctx.chartPanelOpen = true;
245
+ },
246
+ closeChart() {
247
+ ctx.chartPanelOpen = false;
248
+ },
249
+ getChartSpec() {
250
+ return ctx.chartSpec;
251
+ },
252
+ chartRange(ranges) {
253
+ if (ranges && ranges.length) this.selectCells(ranges);
254
+ ctx.chartPanelOpen = true;
255
+ },
256
+ configureChart(config) {
257
+ if (ctx.chartingEnabled) ctx.applyChartConfig(config);
258
+ },
259
+ setChartAiHandler(handler) {
260
+ ctx.chartAiHandler = handler ?? null;
261
+ },
262
+ addRow(row, position = "bottom") {
263
+ this.addRows([row], position);
264
+ },
265
+ addRows(rows, position = "bottom") {
266
+ const next = ctx.internalData.slice() as Array<TData>;
267
+ if (position === "top") next.unshift(...rows);
268
+ else if (position === "bottom") next.push(...rows);
269
+ else if (typeof position === "number")
270
+ next.splice(Math.max(0, Math.min(position, next.length)), 0, ...rows);
271
+ ctx.internalData = next;
272
+ },
273
+ removeRow(rowIndex) {
274
+ this.removeRows([rowIndex]);
275
+ },
276
+ removeRows(rowIndices) {
277
+ const drop = new Set(rowIndices);
278
+ ctx.internalData = ctx.internalData.filter((_: any, i: any) => !drop.has(i));
279
+ },
280
+ applyTransaction(tx) {
281
+ const getId = ctx.props.getRowId;
282
+ let next = ctx.internalData.slice() as Array<TData>;
283
+ let added = 0;
284
+ let updated = 0;
285
+ let removed = 0;
286
+
287
+ if (tx.remove?.length) {
288
+ const removeIds = new Set<string>();
289
+ const removeRefs = new Set<TData>();
290
+ for (const r of tx.remove) {
291
+ if (typeof r === "string") removeIds.add(r);
292
+ else removeRefs.add(r as TData);
293
+ }
294
+ next = next.filter((row, i) => {
295
+ const hit =
296
+ removeRefs.has(row) ||
297
+ (getId ? removeIds.has(getId(row, i)) : false);
298
+ if (hit) removed += 1;
299
+ return !hit;
300
+ });
301
+ }
302
+
303
+ if (tx.update?.length && getId) {
304
+ const byId = new Map<string, TData>();
305
+ for (const u of tx.update) byId.set(getId(u, 0), u);
306
+ next = next.map((row, i) => {
307
+ const u = byId.get(getId(row, i));
308
+ if (u) {
309
+ updated += 1;
310
+ return u;
311
+ }
312
+ return row;
313
+ });
314
+ }
315
+
316
+ if (tx.add?.length) {
317
+ next.push(...(tx.add as Array<TData>));
318
+ added += tx.add.length;
319
+ }
320
+
321
+ ctx.internalData = next;
322
+ return { added, updated, removed };
323
+ },
324
+ addColumn(column, position = "right") {
325
+ this.addColumns([column], position);
326
+ },
327
+ addColumns(columns, position = "right") {
328
+ const next = ctx.internalColumns.slice();
329
+ if (position === "left") next.unshift(...columns);
330
+ else if (position === "right") next.push(...columns);
331
+ else if (typeof position === "number")
332
+ next.splice(
333
+ Math.max(0, Math.min(position, next.length)),
334
+ 0,
335
+ ...columns,
336
+ );
337
+ ctx.internalColumns = next;
338
+ },
339
+ removeColumn(columnId) {
340
+ ctx.internalColumns = ctx.internalColumns.filter(
341
+ (def: any) => !columnDefMatchesId(def, columnId),
342
+ );
343
+ },
344
+ setColumnVisible(columnId, visible) {
345
+ if (visible) {
346
+ const next = { ...ctx.hiddenColumns };
347
+ delete next[columnId];
348
+ ctx.hiddenColumns = next;
349
+ } else {
350
+ ctx.hiddenColumns = { ...ctx.hiddenColumns, [columnId]: true };
351
+ }
352
+ },
353
+ isColumnVisible(columnId) {
354
+ return !ctx.hiddenColumns[columnId];
355
+ },
356
+ setSort(columnId, direction) {
357
+ // Honour the column's `sortable` opt-out. Asking the engine
358
+ // whether the column can sort folds in both the feature toggle
359
+ // AND the per-column flag.
360
+ const col = ctx.allColumns.find((c: any) => c.id === columnId);
361
+ if (col && !col.getCanSort?.()) return;
362
+ const clauses = direction
363
+ ? [{ id: columnId, desc: direction === "desc" }]
364
+ : (ctx.grid.getState().sorting ?? []).filter(
365
+ (entry: { id: string }) => entry.id !== columnId,
366
+ );
367
+ ctx.grid.store.setState((prev: any) => ({ ...prev, sorting: clauses }));
368
+ },
369
+ clearSort() {
370
+ ctx.grid.store.setState((prev: any) => ({ ...prev, sorting: [] }));
371
+ },
372
+ setGroupBy(columnIds) {
373
+ ctx.grid.setGrouping([...columnIds]);
374
+ },
375
+ setFilter(columnId, filter) {
376
+ // `untrack`: these imperative mutators read the same filter state they
377
+ // write (read-modify-write on `filterMenuValues`). When a consumer
378
+ // calls them from inside a reactive `$effect`, that internal read would
379
+ // otherwise be captured as a dependency of the caller's effect, and the
380
+ // write would re-dirty it - an infinite update loop. Untracking the
381
+ // body keeps the API safe to drive from an effect.
382
+ untrack(() => {
383
+ // Honour the column's `filterable` opt-out. Same approach as
384
+ // `setSort` - `getCanFilter()` already combines the feature flag
385
+ // and the per-column field.
386
+ const col = ctx.allColumns.find((c: any) => c.id === columnId);
387
+ if (col && !col.getCanFilter?.()) return;
388
+ if (!filter) {
389
+ this.clearFilter(columnId);
390
+ return;
391
+ }
392
+ ctx.filterMenuValues = {
393
+ ...ctx.filterMenuValues,
394
+ [columnId]: {
395
+ operator: filter.operator,
396
+ value: filter.value ?? "",
397
+ ...(filter.operator === "between"
398
+ ? { valueTo: filter.valueTo ?? "" }
399
+ : {}),
400
+ // Optional second condition + join (multi-condition filtering).
401
+ ...(filter.operator2
402
+ ? {
403
+ operator2: filter.operator2,
404
+ value2: filter.value2 ?? "",
405
+ join: filter.join ?? "AND",
406
+ ...(filter.operator2 === "between"
407
+ ? { valueTo2: filter.valueTo2 ?? "" }
408
+ : {}),
409
+ }
410
+ : {}),
411
+ },
412
+ };
413
+ });
414
+ },
415
+ setFacetFilter(columnId, values) {
416
+ // See `setFilter` for why the read-modify-write is untracked.
417
+ untrack(() => {
418
+ // Empty array OR null both mean "clear this column's facet" -
419
+ // mirrors how the column menu treats unchecking the last value.
420
+ if (!values || values.length === 0) {
421
+ if (ctx.valueFilters[columnId]) {
422
+ const next = { ...ctx.valueFilters };
423
+ delete next[columnId];
424
+ ctx.valueFilters = next;
425
+ }
426
+ return;
427
+ }
428
+ ctx.valueFilters = {
429
+ ...ctx.valueFilters,
430
+ [columnId]: new Set(values),
431
+ };
432
+ });
433
+ },
434
+ clearFilter(columnId) {
435
+ ctx.clearColumnFilter(columnId);
436
+ },
437
+ clearAllFilters() {
438
+ // See `setFilter` for why the read-modify-write is untracked.
439
+ untrack(() => {
440
+ // Wipe every filter surface in one go: column-menu, filter-row, set
441
+ // filters, and the global search box.
442
+ ctx.filterMenuValues = {};
443
+ ctx.filterRowValues = {};
444
+ ctx.valueFilters = {};
445
+ if (ctx.globalFilter !== "") ctx.globalFilter = "";
446
+ });
447
+ },
448
+ getFilters() {
449
+ // Return a defensive copy so callers can't mutate internal state.
450
+ const out: Record<
451
+ string,
452
+ { operator: FilterOperator; value: string; valueTo?: string }
453
+ > = {};
454
+ for (const [columnId, filter] of Object.entries(ctx.filterMenuValues as Record<string, { operator: FilterOperator; value: string; valueTo?: string }>)) {
455
+ out[columnId] = {
456
+ operator: filter.operator,
457
+ value: filter.value,
458
+ ...(filter.operator === "between" && filter.valueTo
459
+ ? { valueTo: filter.valueTo }
460
+ : {}),
461
+ };
462
+ }
463
+ return out;
464
+ },
465
+ getDisplayedRows() {
466
+ // `allRows` is the final, post-filter, post-sort, post-group,
467
+ // post-pagination list - exactly what the body renders. Skip group
468
+ // header rows (they wrap an aggregate, not a TData row).
469
+ const out: TData[] = [];
470
+ for (const row of ctx.allRows) {
471
+ if (row.subRows && row.subRows.length > 0) continue;
472
+ out.push(row.original as TData);
473
+ }
474
+ return out;
475
+ },
476
+ getData() {
477
+ return ctx.internalData;
478
+ },
479
+ getColumns() {
480
+ // Snapshot every column with its human-readable label and
481
+ // visibility flag. Used by external code (exporters, column
482
+ // pickers) so they don't have to re-walk the columnDef tree.
483
+ // Hidden columns ARE included (with `visible: false`) so a column
484
+ // picker can re-enable them - callers that only want what's
485
+ // rendered filter by `.visible` (e.g. the exporter does).
486
+ const describe = (c: any, visible: boolean) => ({
487
+ id: c.id,
488
+ field: (c.columnDef as { field?: string }).field,
489
+ header:
490
+ typeof c.columnDef.header === "string"
491
+ ? c.columnDef.header
492
+ : c.id,
493
+ visible,
494
+ // Surface the format + alignment so exporters can reproduce the
495
+ // on-screen display value (currency symbol, date pattern, etc.)
496
+ // instead of dumping raw values. `getColumnAlign` applies the same
497
+ // editorType-based default the body uses.
498
+ format: c.columnDef.format,
499
+ align: getColumnAlign(c),
500
+ });
501
+ // Visible columns first, in their current visual order...
502
+ const out = ctx.allColumns.map((c: any) => describe(c, true));
503
+ // ...then any column hidden via `visible: false` / setColumnVisible.
504
+ // Those aren't in `allColumns` (that list is visible-only), so pull
505
+ // them straight from the engine's leaf set.
506
+ const visibleIds = new Set(out.map((c: { id: string }) => c.id));
507
+ for (const c of ctx.grid.getAllColumns() as any[]) {
508
+ if (!visibleIds.has(c.id)) out.push(describe(c, false));
509
+ }
510
+ return out;
511
+ },
512
+ // ---- Free data export (CSV / TSV / JSON + clipboard) -----------------
513
+ async exportCsv(options: GridExportOptions = {}) {
514
+ const { records, fields } = projectForExport(
515
+ options.rows ?? "displayed",
516
+ options.columns,
517
+ options.rawValues,
518
+ );
519
+ const text = await serializeDelimited(records, fields, {
520
+ csv: { delimiter: ",", bom: options.bom ?? true },
521
+ signal: options.signal,
522
+ onProgress: options.onProgress,
523
+ });
524
+ if (options.download !== false)
525
+ downloadTextFile(text, `${options.filename ?? "grid"}.csv`, "text/csv;charset=utf-8");
526
+ return text;
527
+ },
528
+ async exportTsv(options: GridExportOptions = {}) {
529
+ const { records, fields } = projectForExport(
530
+ options.rows ?? "displayed",
531
+ options.columns,
532
+ options.rawValues,
533
+ );
534
+ const text = await serializeDelimited(records, fields, {
535
+ csv: { delimiter: "\t", bom: options.bom ?? true },
536
+ signal: options.signal,
537
+ onProgress: options.onProgress,
538
+ });
539
+ if (options.download !== false)
540
+ downloadTextFile(text, `${options.filename ?? "grid"}.tsv`, "text/tab-separated-values;charset=utf-8");
541
+ return text;
542
+ },
543
+ async exportJson(options: GridExportOptions = {}) {
544
+ const { records, fields } = projectForExport(
545
+ options.rows ?? "displayed",
546
+ options.columns,
547
+ options.rawValues,
548
+ );
549
+ const text = await serializeJson(records, fields, {
550
+ signal: options.signal,
551
+ onProgress: options.onProgress,
552
+ });
553
+ if (options.download !== false)
554
+ downloadTextFile(text, `${options.filename ?? "grid"}.json`, "application/json");
555
+ return text;
556
+ },
557
+ async copyToClipboard(options: GridClipboardOptions = {}) {
558
+ const { records, fields, align } = projectForExport(
559
+ options.rows ?? "displayed",
560
+ options.columns,
561
+ options.rawValues,
562
+ );
563
+ const format = options.format ?? "tsv";
564
+ const text =
565
+ format === "markdown"
566
+ ? await serializeMarkdown(records, fields, { align })
567
+ : await serializeDelimited(records, fields, {
568
+ csv: { delimiter: format === "csv" ? "," : "\t", bom: false },
569
+ });
570
+ await copyTextToClipboard(text);
571
+ return text;
572
+ },
573
+ clearRowSelection() {
574
+ // Wipe the internal selection map AND emit the change so any
575
+ // consumer holding a derived `selectedRows` resets too.
576
+ ctx.grid.setRowSelection(() => ({}));
577
+ ctx.props.onRowSelectionChange?.({}, []);
578
+ },
579
+ setColumnWidth(columnId: string, width: number) {
580
+ const clamped = Math.max(ctx.MIN_COLUMN_WIDTH, Math.floor(width));
581
+ ctx.columnWidths = { ...ctx.columnWidths, [columnId]: clamped };
582
+ },
583
+ getColumnWidths() {
584
+ // Resolve every column's effective width by consulting both the
585
+ // resize overrides (`columnWidths`) AND the columnDef defaults,
586
+ // so the snapshot is round-trippable through setColumnWidth.
587
+ const out: Record<string, number> = {};
588
+ for (const c of ctx.allColumns) out[c.id] = ctx.getColumnWidth(c.id);
589
+ return out;
590
+ },
591
+ autosizeColumn(columnId: string) {
592
+ ctx.autosizeColumn(columnId);
593
+ },
594
+ autosizeAllColumns() {
595
+ ctx.autosizeAllColumns();
596
+ },
597
+ setColumnPinning(pinning) {
598
+ // Defensive copy + dedupe so callers can't mutate our state.
599
+ const left = Array.from(new Set(pinning.left ?? []));
600
+ const right = Array.from(new Set(pinning.right ?? []));
601
+ ctx.columnPinning = { left, right };
602
+ },
603
+ getColumnPinning() {
604
+ return {
605
+ left: ctx.columnPinning.left.slice(),
606
+ right: ctx.columnPinning.right.slice(),
607
+ };
608
+ },
609
+ // ---- Column reorder
610
+ setColumnOrder(order) {
611
+ ctx.setColumnOrderInternal(order);
612
+ },
613
+ getColumnOrder() {
614
+ return ctx.getCurrentColumnOrder();
615
+ },
616
+ setRowExpanded(id, expanded) {
617
+ ctx.grid.setExpanded((prev: any) => ({ ...prev, [id]: !!expanded }));
618
+ },
619
+ expandAllGroups() {
620
+ // Walk the current grouped row model and flip every group node on.
621
+ const next: Record<string, boolean> = {};
622
+ const walk = (rows: ReadonlyArray<{ id: string; getCanExpand?: () => boolean; subRows?: ReadonlyArray<unknown> }>) => {
623
+ for (const row of rows) {
624
+ if (row.getCanExpand?.()) next[row.id] = true;
625
+ const sub = row.subRows as ReadonlyArray<{ id: string; getCanExpand?: () => boolean; subRows?: ReadonlyArray<unknown> }> | undefined;
626
+ if (sub && sub.length > 0) walk(sub);
627
+ }
628
+ };
629
+ walk(ctx.grid.getRowModel().rows as unknown as ReadonlyArray<{ id: string; getCanExpand?: () => boolean; subRows?: ReadonlyArray<unknown> }>);
630
+ ctx.grid.setExpanded(() => next);
631
+ },
632
+ collapseAllGroups() {
633
+ ctx.grid.setExpanded(() => ({}));
634
+ },
635
+ // ---- Undo / redo (history + pointer)
636
+ undo() {
637
+ if (ctx.historyPtr < 0) return false
638
+ const step = ctx.history[ctx.historyPtr]
639
+ if (!step) return false
640
+ ctx.applyHistoryStep(step, 'undo')
641
+ ctx.historyPtr -= 1
642
+ ctx.historyVersion += 1
643
+ return true
644
+ },
645
+ redo() {
646
+ if (ctx.historyPtr >= ctx.history.length - 1) return false
647
+ const step = ctx.history[ctx.historyPtr + 1]
648
+ if (!step) return false
649
+ ctx.applyHistoryStep(step, 'redo')
650
+ ctx.historyPtr += 1
651
+ ctx.historyVersion += 1
652
+ return true
653
+ },
654
+ canUndo() { void ctx.historyVersion; return ctx.historyPtr >= 0 },
655
+ canRedo() { void ctx.historyVersion; return ctx.historyPtr < ctx.history.length - 1 },
656
+ clearHistory() { ctx.history = []; ctx.historyPtr = -1; ctx.historyVersion += 1 },
657
+ // ---- Find
658
+ openFind() { ctx.findOpen = true },
659
+ closeFind() { ctx.findOpen = false; ctx.findQuery = '' },
660
+ setFindQuery(q: string) { ctx.findQuery = q; ctx.findHitIndex = 0 },
661
+ getFindHits() { return ctx.findHits.slice() },
662
+ // ---- Row selection (read + write)
663
+ getSelectedRows() {
664
+ const out: TData[] = [];
665
+ for (const row of ctx.allRows) {
666
+ if (isGroupRow(row)) continue;
667
+ if (ctx.rowSelectionState[row.id]) out.push(row.original as TData);
668
+ }
669
+ return out;
670
+ },
671
+ getSelectedRowIds() {
672
+ const out: string[] = [];
673
+ for (const row of ctx.allRows) {
674
+ if (!isGroupRow(row) && ctx.rowSelectionState[row.id]) out.push(row.id);
675
+ }
676
+ return out;
677
+ },
678
+ selectRows(ids, additive = false) {
679
+ const set = new Set(ids);
680
+ ctx.grid.setRowSelection((prev: any) => {
681
+ const next: Record<string, boolean> = additive ? { ...prev } : {};
682
+ for (const id of set) next[id] = true;
683
+ return next;
684
+ });
685
+ },
686
+ selectAllRows() {
687
+ const next: Record<string, boolean> = {};
688
+ for (const row of ctx.allRows) if (!isGroupRow(row)) next[row.id] = true;
689
+ ctx.grid.setRowSelection(() => next);
690
+ },
691
+ toggleRowSelected(id) {
692
+ ctx.toggleRowSelectionById(id);
693
+ },
694
+ // ---- Pagination
695
+ getPageInfo() {
696
+ const { pageIndex, pageSize } = ctx.paginationState;
697
+ const total = ctx.allRowsBeforePagination.length;
698
+ const pageCount = Math.max(1, Math.ceil(total / Math.max(1, pageSize)));
699
+ return { pageIndex, pageSize, pageCount, total };
700
+ },
701
+ setPage(pageIndex) {
702
+ const total = ctx.allRowsBeforePagination.length;
703
+ const pageCount = Math.max(
704
+ 1,
705
+ Math.ceil(total / Math.max(1, ctx.paginationState.pageSize)),
706
+ );
707
+ ctx.goToPage(Math.max(0, Math.min(pageIndex, pageCount - 1)));
708
+ },
709
+ nextPage() {
710
+ this.setPage(ctx.paginationState.pageIndex + 1);
711
+ },
712
+ prevPage() {
713
+ this.setPage(ctx.paginationState.pageIndex - 1);
714
+ },
715
+ firstPage() {
716
+ this.setPage(0);
717
+ },
718
+ lastPage() {
719
+ const total = ctx.allRowsBeforePagination.length;
720
+ const pageCount = Math.max(
721
+ 1,
722
+ Math.ceil(total / Math.max(1, ctx.paginationState.pageSize)),
723
+ );
724
+ this.setPage(pageCount - 1);
725
+ },
726
+ setPageSize(size) {
727
+ // Calls the component-level helper (lexical scope), not this method.
728
+ ctx.setPageSize(Math.max(1, Math.floor(size)));
729
+ },
730
+ // ---- Navigation / scrolling
731
+ scrollToRow(rowIndex) {
732
+ if (!ctx.scrollContainer) return;
733
+ const maxIndex = Math.max(0, ctx.allRows.length - 1);
734
+ const clamped = Math.max(0, Math.min(rowIndex, maxIndex));
735
+ // Logical offset from the virtualizer (honors uniform OR per-row
736
+ // sizing), then mapped into the capped DOM scroll space so it lands
737
+ // correctly on huge grids where the DOM height is scaled down.
738
+ const logical = ctx.virtualizer.getOffsetForIndex(clamped);
739
+ ctx.scrollContainer.scrollTop = ctx.logicalToDomRowOffset(logical);
740
+ ctx.scheduleScrollSync(
741
+ ctx.scrollContainer.scrollTop,
742
+ ctx.scrollContainer.scrollLeft,
743
+ );
744
+ },
745
+ getActiveCell() {
746
+ const a = ctx.activeCell;
747
+ if (!a || a.rowIndex < 0 || a.colIndex < 0) return null;
748
+ const col = ctx.allColumns[a.colIndex];
749
+ return {
750
+ rowIndex: a.rowIndex,
751
+ colIndex: a.colIndex,
752
+ columnId: col?.id ?? "",
753
+ };
754
+ },
755
+ setActiveCell(rowIndex, colIndex) {
756
+ const r = Math.max(0, Math.min(rowIndex, Math.max(0, ctx.allRows.length - 1)));
757
+ const c = Math.max(
758
+ 0,
759
+ Math.min(colIndex, Math.max(0, ctx.allColumns.length - 1)),
760
+ );
761
+ // Calls the component-level helper (lexical scope), not this method.
762
+ ctx.setActiveCell(r, c);
763
+ },
764
+ // ---- View state (save / restore)
765
+ getState() {
766
+ return {
767
+ sorting: (ctx.grid.getState().sorting ?? []).map(
768
+ (s: { id: string; desc: boolean }) => ({ id: s.id, desc: s.desc }),
769
+ ),
770
+ grouping: [...(ctx.grid.getState().grouping ?? [])],
771
+ pagination: {
772
+ pageIndex: ctx.paginationState.pageIndex,
773
+ pageSize: ctx.paginationState.pageSize,
774
+ },
775
+ columnWidths: this.getColumnWidths(),
776
+ columnPinning: this.getColumnPinning(),
777
+ columnOrder: ctx.getCurrentColumnOrder(),
778
+ hiddenColumns: Object.keys(ctx.hiddenColumns).filter(
779
+ (k) => ctx.hiddenColumns[k],
780
+ ),
781
+ globalFilter: ctx.globalFilter,
782
+ columnFilters: this.getFilters(),
783
+ facetFilters: Object.fromEntries(
784
+ Object.entries(ctx.valueFilters).map(([k, set]: [string, any]) => [
785
+ k,
786
+ Array.from(set),
787
+ ]),
788
+ ),
789
+ ...(ctx.chartingEnabled
790
+ ? {
791
+ chart: {
792
+ open: ctx.chartPanelOpen,
793
+ type: ctx.chartType,
794
+ dimension: ctx.chartDimensionId,
795
+ series: ctx.chartSeriesId,
796
+ measure: ctx.chartMeasureId,
797
+ reduce: ctx.chartReduce,
798
+ stacked: ctx.chartStacked,
799
+ },
800
+ charts: ctx.getChartsState(),
801
+ chartActive: ctx.activeChartIndex,
802
+ }
803
+ : {}),
804
+ };
805
+ },
806
+ setState(state) {
807
+ if (state.sorting) {
808
+ const sorting = state.sorting.map((s) => ({ id: s.id, desc: s.desc }));
809
+ ctx.grid.store.setState((prev: any) => ({ ...prev, sorting }));
810
+ }
811
+ if (state.grouping) {
812
+ // Drop grouping by columns that no longer exist (#53) - otherwise a
813
+ // saved view collapses every row into a group named 'undefined'.
814
+ const existing = new Set((ctx.grid.getAllColumns() as any[]).map((c) => c.id));
815
+ ctx.grid.setGrouping(state.grouping.filter((id: string) => existing.has(id)));
816
+ }
817
+ if (state.pagination) {
818
+ const p = state.pagination;
819
+ ctx.grid.setPagination((prev: any) => ({ ...prev, ...p }));
820
+ }
821
+ if (state.columnWidths) ctx.columnWidths = { ...state.columnWidths };
822
+ if (state.columnPinning)
823
+ ctx.columnPinning = {
824
+ left: [...(state.columnPinning.left ?? [])],
825
+ right: [...(state.columnPinning.right ?? [])],
826
+ };
827
+ if (state.columnOrder) ctx.setColumnOrderInternal(state.columnOrder);
828
+ if (state.hiddenColumns) {
829
+ // Seed from columns declared `visible: false` so a partial/empty saved
830
+ // state never un-hides an app-declared-hidden column (#54); then layer
831
+ // the saved user-driven hides on top.
832
+ const next: Record<string, boolean> = { ...ctx.declaredHiddenColumns };
833
+ for (const id of state.hiddenColumns) next[id] = true;
834
+ ctx.hiddenColumns = next;
835
+ }
836
+ if (state.globalFilter !== undefined) ctx.globalFilter = state.globalFilter;
837
+ if (state.columnFilters)
838
+ ctx.filterMenuValues = { ...state.columnFilters };
839
+ if (state.facetFilters) {
840
+ const next: Record<string, Set<string>> = {};
841
+ for (const [k, arr] of Object.entries(state.facetFilters))
842
+ next[k] = new Set(arr);
843
+ ctx.valueFilters = next;
844
+ }
845
+ if ((state.chart || state.charts) && ctx.chartingEnabled) {
846
+ if (Array.isArray(state.charts) && state.charts.length) {
847
+ ctx.applyChartsState(state.charts, state.chartActive);
848
+ }
849
+ if (state.chart) {
850
+ const c = state.chart;
851
+ ctx.chartPanelOpen = c.open;
852
+ ctx.chartType = c.type;
853
+ ctx.chartDimensionId = c.dimension;
854
+ ctx.chartSeriesId = c.series;
855
+ ctx.chartMeasureId = c.measure;
856
+ ctx.chartReduce = c.reduce;
857
+ ctx.chartStacked = c.stacked;
858
+ }
859
+ }
860
+ },
861
+ refresh() {
862
+ ctx.grid.store.setState((prev: any) => ({ ...prev }));
863
+ },
864
+ };
865
+ }
866
+
867
+ return {
868
+ buildApi,
869
+ };
870
+ }