@svgrid/grid 2.2.0 → 2.2.2

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 (114) hide show
  1. package/dist/FlexRender.svelte +96 -96
  2. package/dist/GridFooter.svelte +178 -178
  3. package/dist/SvCarousel.svelte +141 -141
  4. package/dist/SvColorInput.svelte +187 -187
  5. package/dist/SvContextMenu.svelte +116 -116
  6. package/dist/SvDrawer.svelte +250 -250
  7. package/dist/SvGrid.svelte +3131 -3131
  8. package/dist/SvGridBoard.svelte +2339 -2339
  9. package/dist/SvGridChart.svelte +1724 -1724
  10. package/dist/SvGridChartPanel.svelte +485 -485
  11. package/dist/SvGridDropdown.svelte +689 -689
  12. package/dist/SvGroupCell.svelte +109 -109
  13. package/dist/SvMenu.svelte +124 -124
  14. package/dist/SvMenuList.svelte +117 -117
  15. package/dist/SvNumberInput.svelte +131 -131
  16. package/dist/SvOtpInput.svelte +158 -158
  17. package/dist/SvPopover.svelte +139 -139
  18. package/dist/SvRowGroupPanel.svelte +170 -170
  19. package/dist/SvScrollArea.svelte +61 -61
  20. package/dist/SvTooltip.svelte +127 -127
  21. package/dist/SvTour.svelte +204 -204
  22. package/dist/SvTree.svelte +377 -377
  23. package/dist/SvTreeSelect.svelte +230 -230
  24. package/dist/cdn/svgrid.js +8 -8
  25. package/dist/cdn/svgrid.svelte-external.js +8 -8
  26. package/dist/chart-export.js +8 -8
  27. package/package.json +10 -10
  28. package/src/FlexRender.svelte +96 -96
  29. package/src/GridFooter.svelte +178 -178
  30. package/src/SvCarousel.svelte +141 -141
  31. package/src/SvColorInput.svelte +187 -187
  32. package/src/SvContextMenu.svelte +116 -116
  33. package/src/SvDrawer.svelte +250 -250
  34. package/src/SvGrid.controller.svelte.ts +3158 -3158
  35. package/src/SvGrid.svelte +3131 -3131
  36. package/src/SvGrid.types.ts +1489 -1489
  37. package/src/SvGridBoard.svelte +2339 -2339
  38. package/src/SvGridChart.svelte +1724 -1724
  39. package/src/SvGridChartPanel.svelte +485 -485
  40. package/src/SvGridDropdown.svelte +689 -689
  41. package/src/SvGroupCell.svelte +109 -109
  42. package/src/SvMenu.svelte +124 -124
  43. package/src/SvMenu.test.ts +97 -97
  44. package/src/SvMenuList.svelte +117 -117
  45. package/src/SvNumberInput.svelte +131 -131
  46. package/src/SvOtpInput.svelte +158 -158
  47. package/src/SvPopover.svelte +139 -139
  48. package/src/SvRowGroupPanel.svelte +170 -170
  49. package/src/SvScrollArea.svelte +61 -61
  50. package/src/SvTooltip.svelte +127 -127
  51. package/src/SvTour.svelte +204 -204
  52. package/src/SvTree.svelte +377 -377
  53. package/src/SvTreeSelect.svelte +230 -230
  54. package/src/a11y.contract.test.ts +49 -49
  55. package/src/a11y.test.ts +59 -59
  56. package/src/a11y.ts +61 -61
  57. package/src/build-api.ts +870 -870
  58. package/src/cell-formatting.ts +169 -169
  59. package/src/cell-render.ts +469 -469
  60. package/src/chart-export.ts +201 -201
  61. package/src/chart.ts +2296 -2296
  62. package/src/collaboration.test.ts +104 -104
  63. package/src/collaboration.ts +167 -167
  64. package/src/core.performance.test.ts +30 -30
  65. package/src/core.ts +1111 -1111
  66. package/src/createAutocomplete.svelte.ts +132 -132
  67. package/src/createCombobox.svelte.ts +179 -179
  68. package/src/createCountryInput.svelte.ts +157 -157
  69. package/src/createDropdownList.svelte.ts +162 -162
  70. package/src/createGrid.svelte.ts +42 -42
  71. package/src/createGrid.test.ts +10 -10
  72. package/src/createGridState.svelte.ts +17 -17
  73. package/src/createMenu.svelte.ts +202 -202
  74. package/src/createPopoverSelect.svelte.ts +206 -206
  75. package/src/createTree.svelte.ts +319 -319
  76. package/src/editing.test.ts +859 -859
  77. package/src/editing.ts +675 -675
  78. package/src/export-data-api.test.ts +126 -126
  79. package/src/export-format.test.ts +107 -107
  80. package/src/export-format.ts +598 -598
  81. package/src/flex-render.ts +3 -3
  82. package/src/index.ts +623 -623
  83. package/src/keyboard.test.ts +59 -59
  84. package/src/keyboard.ts +97 -97
  85. package/src/list-nav.test.ts +49 -49
  86. package/src/list-nav.ts +29 -29
  87. package/src/menus.ts +639 -639
  88. package/src/merge-objects.ts +48 -48
  89. package/src/overlays.test.ts +90 -90
  90. package/src/render-component.ts +28 -28
  91. package/src/selection.test.ts +754 -754
  92. package/src/selection.ts +600 -600
  93. package/src/server-data-source.test.ts +289 -289
  94. package/src/server-data-source.ts +413 -413
  95. package/src/sparkline.test.ts +68 -68
  96. package/src/sparkline.ts +169 -169
  97. package/src/spreadsheet.test.ts +489 -489
  98. package/src/spreadsheet.ts +312 -312
  99. package/src/static-functions.ts +11 -11
  100. package/src/subscribe.ts +38 -38
  101. package/src/svgrid-wrapper.types.ts +501 -501
  102. package/src/svgrid.behavior.test.ts +706 -706
  103. package/src/svgrid.charting.test.ts +527 -527
  104. package/src/svgrid.features.test.ts +157 -157
  105. package/src/svgrid.new-features.wrapper.test.ts +251 -251
  106. package/src/svgrid.wrapper.test.ts +40 -40
  107. package/src/test-setup.ts +62 -62
  108. package/src/themes/index.ts +159 -159
  109. package/src/virtualization/column-virtualizer.test.ts +27 -27
  110. package/src/virtualization/column-virtualizer.ts +30 -30
  111. package/src/virtualization/svelte-virtualizer.svelte.ts +26 -26
  112. package/src/virtualization/types.ts +30 -30
  113. package/src/virtualization/virtualizer.test.ts +47 -47
  114. package/src/virtualization/virtualizer.ts +296 -296
package/src/build-api.ts CHANGED
@@ -1,870 +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 { 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
- }
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
+ }