@svgrid/grid 1.1.1 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. package/dist/FlexRender.svelte +96 -96
  2. package/dist/SvGrid.controller.svelte.js +17 -6
  3. package/dist/SvGrid.css +2012 -2012
  4. package/dist/SvGrid.svelte +2571 -2346
  5. package/dist/SvGridDropdown.svelte +7 -3
  6. package/dist/build-api.js +2 -2
  7. package/dist/cdn/svgrid.js +13504 -0
  8. package/dist/cdn/svgrid.svelte-external.js +11188 -0
  9. package/dist/cell-values.d.ts +1 -1
  10. package/dist/cell-values.js +7 -7
  11. package/dist/column-types.js +1 -1
  12. package/dist/core.d.ts +1 -1
  13. package/dist/core.js +2 -2
  14. package/dist/summaries.js +4 -4
  15. package/package.json +12 -2
  16. package/src/FlexRender.svelte +96 -96
  17. package/src/SvGrid.controller.svelte.ts +2363 -2352
  18. package/src/SvGrid.css +2012 -2012
  19. package/src/SvGrid.svelte +2571 -2346
  20. package/src/SvGrid.types.ts +537 -537
  21. package/src/SvGridDropdown.svelte +7 -3
  22. package/src/a11y.contract.test.ts +49 -49
  23. package/src/a11y.test.ts +59 -59
  24. package/src/a11y.ts +59 -59
  25. package/src/build-api.ts +683 -683
  26. package/src/cell-formatting.ts +169 -169
  27. package/src/cell-values.ts +4 -4
  28. package/src/column-types.ts +1 -1
  29. package/src/core.performance.test.ts +30 -30
  30. package/src/core.ts +1077 -1077
  31. package/src/createGrid.svelte.ts +42 -42
  32. package/src/createGrid.test.ts +10 -10
  33. package/src/createGridState.svelte.ts +17 -17
  34. package/src/editing.ts +669 -669
  35. package/src/flex-render.ts +3 -3
  36. package/src/index.ts +208 -208
  37. package/src/keyboard.test.ts +59 -59
  38. package/src/keyboard.ts +97 -97
  39. package/src/merge-objects.ts +48 -48
  40. package/src/render-component.ts +28 -28
  41. package/src/spreadsheet.test.ts +489 -489
  42. package/src/spreadsheet.ts +304 -304
  43. package/src/static-functions.ts +11 -11
  44. package/src/subscribe.ts +38 -38
  45. package/src/summaries.ts +4 -4
  46. package/src/svgrid-wrapper.types.ts +412 -412
  47. package/src/svgrid.features.test.ts +157 -157
  48. package/src/svgrid.wrapper.test.ts +40 -40
  49. package/src/virtualization/column-virtualizer.test.ts +27 -27
  50. package/src/virtualization/column-virtualizer.ts +30 -30
  51. package/src/virtualization/svelte-virtualizer.svelte.ts +26 -26
  52. package/src/virtualization/types.ts +30 -30
  53. package/src/virtualization/virtualizer.test.ts +47 -47
  54. package/src/virtualization/virtualizer.ts +296 -296
  55. package/dist/SvGrid.helpers.test.d.ts +0 -1
  56. package/dist/SvGrid.helpers.test.js +0 -298
  57. package/dist/SvGridChart.test.d.ts +0 -1
  58. package/dist/SvGridChart.test.js +0 -62
  59. package/dist/a11y.contract.test.d.ts +0 -1
  60. package/dist/a11y.contract.test.js +0 -39
  61. package/dist/a11y.test.d.ts +0 -1
  62. package/dist/a11y.test.js +0 -44
  63. package/dist/aligned-grids.test.d.ts +0 -1
  64. package/dist/aligned-grids.test.js +0 -75
  65. package/dist/build-api.coverage.test.d.ts +0 -20
  66. package/dist/build-api.coverage.test.js +0 -505
  67. package/dist/cell-formatting.test.d.ts +0 -1
  68. package/dist/cell-formatting.test.js +0 -234
  69. package/dist/cell-render.test.d.ts +0 -1
  70. package/dist/cell-render.test.js +0 -338
  71. package/dist/chart-export.test.d.ts +0 -1
  72. package/dist/chart-export.test.js +0 -302
  73. package/dist/chart.coverage.test.d.ts +0 -1
  74. package/dist/chart.coverage.test.js +0 -748
  75. package/dist/chart.test.d.ts +0 -1
  76. package/dist/chart.test.js +0 -289
  77. package/dist/clipboard.test.d.ts +0 -1
  78. package/dist/clipboard.test.js +0 -700
  79. package/dist/collaboration.coverage.test.d.ts +0 -1
  80. package/dist/collaboration.coverage.test.js +0 -200
  81. package/dist/collaboration.test.d.ts +0 -1
  82. package/dist/collaboration.test.js +0 -65
  83. package/dist/column-groups.test.d.ts +0 -1
  84. package/dist/column-groups.test.js +0 -56
  85. package/dist/column-types.test.d.ts +0 -1
  86. package/dist/column-types.test.js +0 -62
  87. package/dist/columns.test.d.ts +0 -1
  88. package/dist/columns.test.js +0 -625
  89. package/dist/conditional-formatting.test.d.ts +0 -1
  90. package/dist/conditional-formatting.test.js +0 -87
  91. package/dist/core.coverage.test.d.ts +0 -1
  92. package/dist/core.coverage.test.js +0 -186
  93. package/dist/core.performance.test.d.ts +0 -1
  94. package/dist/core.performance.test.js +0 -29
  95. package/dist/createGrid.test.d.ts +0 -1
  96. package/dist/createGrid.test.js +0 -9
  97. package/dist/editing.test.d.ts +0 -1
  98. package/dist/editing.test.js +0 -732
  99. package/dist/editors/cell-editors.coverage.test.d.ts +0 -1
  100. package/dist/editors/cell-editors.coverage.test.js +0 -139
  101. package/dist/editors/cell-editors.test.d.ts +0 -1
  102. package/dist/editors/cell-editors.test.js +0 -75
  103. package/dist/facet-buckets.test.d.ts +0 -1
  104. package/dist/facet-buckets.test.js +0 -296
  105. package/dist/fill-patterns.test.d.ts +0 -1
  106. package/dist/fill-patterns.test.js +0 -81
  107. package/dist/filter-operators.test.d.ts +0 -1
  108. package/dist/filter-operators.test.js +0 -135
  109. package/dist/filtering/excel-filters.test.d.ts +0 -1
  110. package/dist/filtering/excel-filters.test.js +0 -116
  111. package/dist/filtering/locale-filter.test.d.ts +0 -13
  112. package/dist/filtering/locale-filter.test.js +0 -189
  113. package/dist/flex-render.test.d.ts +0 -1
  114. package/dist/flex-render.test.js +0 -142
  115. package/dist/group-aggregate.test.d.ts +0 -1
  116. package/dist/group-aggregate.test.js +0 -32
  117. package/dist/hyperformula-adapter.test.d.ts +0 -1
  118. package/dist/hyperformula-adapter.test.js +0 -205
  119. package/dist/keyboard-handlers.coverage.test.d.ts +0 -1
  120. package/dist/keyboard-handlers.coverage.test.js +0 -495
  121. package/dist/keyboard.test.d.ts +0 -1
  122. package/dist/keyboard.test.js +0 -27
  123. package/dist/menus.test.d.ts +0 -1
  124. package/dist/menus.test.js +0 -560
  125. package/dist/named-views.coverage.test.d.ts +0 -1
  126. package/dist/named-views.coverage.test.js +0 -180
  127. package/dist/named-views.test.d.ts +0 -1
  128. package/dist/named-views.test.js +0 -57
  129. package/dist/new-features.test.d.ts +0 -1
  130. package/dist/new-features.test.js +0 -183
  131. package/dist/render-component.test.d.ts +0 -1
  132. package/dist/render-component.test.js +0 -41
  133. package/dist/row-drag.test.d.ts +0 -1
  134. package/dist/row-drag.test.js +0 -142
  135. package/dist/row-resize.test.d.ts +0 -1
  136. package/dist/row-resize.test.js +0 -329
  137. package/dist/scroll-sync.test.d.ts +0 -1
  138. package/dist/scroll-sync.test.js +0 -290
  139. package/dist/selection.multi-range.test.d.ts +0 -1
  140. package/dist/selection.multi-range.test.js +0 -55
  141. package/dist/selection.test.d.ts +0 -1
  142. package/dist/selection.test.js +0 -647
  143. package/dist/server-data-source.coverage.test.d.ts +0 -1
  144. package/dist/server-data-source.coverage.test.js +0 -154
  145. package/dist/server-data-source.test.d.ts +0 -1
  146. package/dist/server-data-source.test.js +0 -81
  147. package/dist/sparkline.test.d.ts +0 -1
  148. package/dist/sparkline.test.js +0 -52
  149. package/dist/spreadsheet.test.d.ts +0 -1
  150. package/dist/spreadsheet.test.js +0 -446
  151. package/dist/subscribe.test.d.ts +0 -1
  152. package/dist/subscribe.test.js +0 -93
  153. package/dist/svgrid.api-extensions.test.d.ts +0 -14
  154. package/dist/svgrid.api-extensions.test.js +0 -334
  155. package/dist/svgrid.api.test.d.ts +0 -15
  156. package/dist/svgrid.api.test.js +0 -169
  157. package/dist/svgrid.behavior.test.d.ts +0 -15
  158. package/dist/svgrid.behavior.test.js +0 -625
  159. package/dist/svgrid.column-reorder.test.d.ts +0 -15
  160. package/dist/svgrid.column-reorder.test.js +0 -224
  161. package/dist/svgrid.comments-autocomplete.test.d.ts +0 -1
  162. package/dist/svgrid.comments-autocomplete.test.js +0 -96
  163. package/dist/svgrid.context-menu.test.d.ts +0 -1
  164. package/dist/svgrid.context-menu.test.js +0 -102
  165. package/dist/svgrid.features.test.d.ts +0 -1
  166. package/dist/svgrid.features.test.js +0 -129
  167. package/dist/svgrid.interaction.test.d.ts +0 -9
  168. package/dist/svgrid.interaction.test.js +0 -349
  169. package/dist/svgrid.locale-filtering.test.d.ts +0 -15
  170. package/dist/svgrid.locale-filtering.test.js +0 -261
  171. package/dist/svgrid.new-features.wrapper.test.d.ts +0 -1
  172. package/dist/svgrid.new-features.wrapper.test.js +0 -194
  173. package/dist/svgrid.row-pinning.test.d.ts +0 -14
  174. package/dist/svgrid.row-pinning.test.js +0 -250
  175. package/dist/svgrid.set-filter.test.d.ts +0 -17
  176. package/dist/svgrid.set-filter.test.js +0 -268
  177. package/dist/svgrid.wrapper.test.d.ts +0 -1
  178. package/dist/svgrid.wrapper.test.js +0 -59
  179. package/dist/test-setup.d.ts +0 -1
  180. package/dist/test-setup.js +0 -35
  181. package/dist/transaction.test.d.ts +0 -1
  182. package/dist/transaction.test.js +0 -89
  183. package/dist/virtualization/column-virtualizer.test.d.ts +0 -1
  184. package/dist/virtualization/column-virtualizer.test.js +0 -25
  185. package/dist/virtualization/scroll-scaling.test.d.ts +0 -1
  186. package/dist/virtualization/scroll-scaling.test.js +0 -127
  187. package/dist/virtualization/virtualizer.test.d.ts +0 -1
  188. package/dist/virtualization/virtualizer.test.js +0 -41
package/src/core.ts CHANGED
@@ -1,1077 +1,1077 @@
1
- import type { SparklineConfig } from './sparkline'
2
-
3
- export type RowData = Record<string, unknown>
4
- export type Updater<T> = T | ((prev: T) => T)
5
- export type SortingState = Array<{ id: string; desc: boolean }>
6
- export type ColumnFilter = { id: string; value: unknown; fn?: keyof typeof filterFns }
7
- export type ColumnFiltersState = Array<ColumnFilter>
8
- export type PaginationState = { pageIndex: number; pageSize: number }
9
- export type GroupingState = Array<string>
10
- export type ExpandedState = Record<string, boolean>
11
- export type RowSelectionState = Record<string, boolean>
12
- export type ActiveCellState = {
13
- rowIndex: number
14
- colIndex: number
15
- cellId: string | null
16
- }
17
- export type TableFeatures = Record<string, unknown>
18
-
19
- export type CellData = unknown
20
-
21
- export type HeaderContext<TData extends RowData> = {
22
- header: Header<TData>
23
- column: Column<TData>
24
- table: SvGrid<TData>
25
- }
26
-
27
- export type CellContext<TData extends RowData> = {
28
- cell: Cell<TData>
29
- row: Row<TData>
30
- column: Column<TData>
31
- table: SvGrid<TData>
32
- getValue: () => unknown
33
- }
34
-
35
- /** Params passed to a column's `colSpan(...)` / `rowSpan(...)` callbacks. */
36
- export type CellSpanParams<TData extends RowData = RowData> = {
37
- /** The row's underlying data object. */
38
- data: TData
39
- /** Display-row index in the current (filtered/sorted) row set. */
40
- rowIndex: number
41
- /** The column's id. */
42
- columnId: string
43
- /** The cell's base value for this column. */
44
- value: unknown
45
- }
46
-
47
- /** Params passed to a column's `valueParser(...)` on edit commit. */
48
- export type ValueParserParams<TData extends RowData = RowData> = {
49
- /** The value after built-in per-`editorType` coercion. */
50
- newValue: unknown
51
- /** The cell's previous value. */
52
- oldValue: unknown
53
- /** The raw string the editor produced (pre-coercion). */
54
- rawInput: string
55
- /** The row's underlying data object. */
56
- data: TData
57
- /** The column's id. */
58
- columnId: string
59
- }
60
-
61
- /**
62
- * Context passed to a custom `cellEditor` snippet/component. Three write
63
- * helpers cover the lifecycle:
64
- *
65
- * - `update(next)` - stage `next` as the draft, keep the editor open.
66
- * Use this for live-preview controls (sliders,
67
- * color pickers) so the user can keep adjusting.
68
- * - `commit(next?)` - write the value AND close the editor. The
69
- * argument is optional; when omitted, the most
70
- * recently `update()`d value is saved. Use this
71
- * for "done" gestures (Enter, picking an option).
72
- * - `cancel()` - discard the draft and close the editor.
73
- */
74
- export type EditorContext<TData extends RowData> = CellContext<TData> & {
75
- value: unknown
76
- update: (next: unknown) => void
77
- commit: (next?: unknown) => void
78
- cancel: () => void
79
- }
80
-
81
- export type CellFormatConfig =
82
- | {
83
- type: 'number'
84
- locales?: string | Array<string>
85
- options?: Intl.NumberFormatOptions
86
- }
87
- | {
88
- type: 'currency'
89
- /** ISO 4217 (default USD) */
90
- currency?: string
91
- locales?: string | Array<string>
92
- options?: Omit<Intl.NumberFormatOptions, 'style' | 'currency'>
93
- }
94
- | {
95
- type: 'percent'
96
- locales?: string | Array<string>
97
- options?: Omit<Intl.NumberFormatOptions, 'style'>
98
- /**
99
- * If true, numeric cell values are 0–100 (e.g. 42 → 42%) instead of Intl’s 0–1 fraction (0.42 → 42%).
100
- * Default false.
101
- */
102
- valueIsPercentPoints?: boolean
103
- }
104
- | {
105
- type: 'date' | 'datetime'
106
- locales?: string | Array<string>
107
- /**
108
- * Shortcut patterns merged with `options`:
109
- * `'d'` short numeric date, `'D'` long date, `'y-m-d'` yyyy/mm/dd-style,
110
- * `'short'`|`'medium'`|`'long'` use dateStyle/timeStyle presets.
111
- */
112
- pattern?: string
113
- options?: Intl.DateTimeFormatOptions
114
- }
115
-
116
- export type CellFormatter<TData extends RowData> = (context: {
117
- value: unknown
118
- row: Row<TData>
119
- column: Column<TData>
120
- table: SvGrid<TData>
121
- }) => string
122
-
123
- export type ColumnDefTemplate<TContext> = string | ((context: TContext) => unknown)
124
-
125
- /**
126
- * How a column's value is aggregated for a group row when `columnGrouping`
127
- * is active. Built-in reducers cover the common cases; pass a function for
128
- * anything custom (weighted average, median, percentile, distinct count).
129
- * The function receives the finite numeric values AND the raw leaf rows.
130
- */
131
- export type GroupAggregator<TData = any> =
132
- | 'sum'
133
- | 'avg'
134
- | 'min'
135
- | 'max'
136
- | 'count'
137
- | 'countDistinct'
138
- | 'extent'
139
- | 'first'
140
- | ((values: number[], rows: Array<TData>) => unknown)
141
-
142
- /** Apply a group aggregator over a bucket's leaf rows for one column. */
143
- export function applyGroupAggregate<TData extends RowData>(
144
- agg: GroupAggregator<TData>,
145
- columnId: string,
146
- rows: ReadonlyArray<Row<TData>>,
147
- ): unknown {
148
- const raw = rows.map((r) => r.getCellValueByColumnId(columnId))
149
- if (typeof agg === 'function') {
150
- const nums = raw.map((v) => Number(v)).filter((n) => Number.isFinite(n))
151
- return agg(nums, rows.map((r) => r.original))
152
- }
153
- if (agg === 'count') return rows.length
154
- if (agg === 'countDistinct') return new Set(raw.map((v) => String(v ?? ''))).size
155
- if (agg === 'first') return raw[0]
156
- const nums = raw.map((v) => Number(v)).filter((n) => Number.isFinite(n))
157
- if (!nums.length) return undefined
158
- switch (agg) {
159
- case 'sum':
160
- return nums.reduce((a, b) => a + b, 0)
161
- case 'avg':
162
- return nums.reduce((a, b) => a + b, 0) / nums.length
163
- case 'min':
164
- return Math.min(...nums)
165
- case 'max':
166
- return Math.max(...nums)
167
- case 'extent':
168
- return `${Math.min(...nums)} – ${Math.max(...nums)}`
169
- default:
170
- return undefined
171
- }
172
- }
173
-
174
- export type ColumnDef<TFeatures extends TableFeatures, TData extends RowData> = {
175
- id?: string
176
- field?: keyof TData & string
177
- accessorFn?: (row: TData) => unknown
178
- header?: ColumnDefTemplate<HeaderContext<TData>>
179
- footer?: ColumnDefTemplate<HeaderContext<TData>>
180
- cell?: ColumnDefTemplate<CellContext<TData>>
181
- columns?: Array<ColumnDef<TFeatures, TData>>
182
- /**
183
- * Declarative cell spanning (merged cells). Return how many COLUMNS this
184
- * cell spans to the right (1 = no span). Value-driven, AG-Grid-style. Feed
185
- * `spansToMerges(rows, columns)` into `spreadsheetLayout` to apply - it uses
186
- * the same real `colspan`/`rowspan` merge engine (no separate code path).
187
- */
188
- colSpan?: (params: CellSpanParams<TData>) => number
189
- /**
190
- * Declarative cell spanning (merged cells). Return how many ROWS this cell
191
- * spans downward (1 = no span). See `colSpan` for how to apply.
192
- */
193
- rowSpan?: (params: CellSpanParams<TData>) => number
194
- /**
195
- * High-level data type for the column. A convenience that resolves to the
196
- * right `editorType`, alignment, date `format`, and filter operators without
197
- * setting each by hand:
198
- * 'text' → text editor, left-aligned
199
- * 'number' → number editor, right-aligned, numeric filter operators
200
- * 'boolean' → checkbox editor, centered
201
- * 'date' → date editor (Date values), right-aligned, `{ type: 'date' }` format
202
- * 'dateString' → date editor for ISO date STRINGS (e.g. '2026-06-27')
203
- * Anything you set explicitly (`editorType`, `align`, `format`) still wins -
204
- * `cellDataType` only fills the gaps. Grid-level `inferColumnTypes` infers
205
- * this from the first data row for columns that declare neither.
206
- */
207
- cellDataType?: 'text' | 'number' | 'boolean' | 'date' | 'dateString'
208
- /**
209
- * For a column INSIDE a collapsible column group: `'open'` shows this column
210
- * only while the group is expanded, `'closed'` only while collapsed. Omit to
211
- * always show it. Setting it on any direct child gives the parent group a
212
- * collapse toggle. Pair with `openByDefault` on the group.
213
- */
214
- columnGroupShow?: 'open' | 'closed'
215
- /**
216
- * For a GROUP column (one with `columns: [...]`): start the group expanded.
217
- * Defaults to `false` (collapsed), matching AG Grid - so only the always-on
218
- * and `columnGroupShow: 'closed'` children show until the user expands it.
219
- */
220
- openByDefault?: boolean
221
- editorType?:
222
- | 'text'
223
- | 'number'
224
- | 'date'
225
- | 'datetime'
226
- | 'time' // native <input type="time"> - HH:MM or HH:MM:SS
227
- | 'password' // native <input type="password"> with masked rendering
228
- | 'checkbox'
229
- | 'list'
230
- | 'chips'
231
- | 'select' // custom dropdown - single value, no typeahead
232
- | 'rich-select' // custom dropdown with a typeahead search input
233
- | 'autocomplete' // free-text input with a live-filtered suggestion list (accepts any value)
234
- | 'textarea' // multi-line editor; Tab or Ctrl+Enter commits, plain Enter inserts a newline
235
- | 'color' // native <input type="color"> swatch
236
- | 'rating' // 5-star rating control
237
- /**
238
- * Custom in-cell editor. Receives the cell context PLUS a `commit(value)`
239
- * and `cancel()` helper. Use when none of the built-in `editorType`s fit;
240
- * the snippet's outer element is mounted inside the editing cell and
241
- * inherits keyboard handling (Esc cancels, Enter commits unless your
242
- * snippet preventDefaults it).
243
- *
244
- * Coexists with `editorType`: when both are set, `cellEditor` wins and
245
- * `editorType` is treated as a hint for parsing the saved value.
246
- */
247
- cellEditor?: ColumnDefTemplate<EditorContext<TData>>
248
- /**
249
- * Per-column tooltip. String shows as a native `title=`; `(ctx) => string`
250
- * runs per cell so the tooltip can reflect the value. Returning an empty
251
- * string skips the tooltip.
252
- */
253
- tooltip?: string | ((ctx: CellContext<TData>) => string | null | undefined)
254
- /**
255
- * Gate editing per column or per cell.
256
- *
257
- * - `true` (or omitted): the column is fully editable.
258
- * - `false`: the column is read-only - double-click, type-to-edit,
259
- * fill-handle drag, Delete, and clipboard paste all skip it.
260
- * - `(ctx) => boolean`: evaluated for each cell, so you can lock
261
- * individual rows (e.g. by role, status, ownership). Returning
262
- * `false` opts the cell out of every editing path, identical to
263
- * setting `editable: false` on the whole column for that row.
264
- *
265
- * The grid-wide `enableInlineEditing` prop still wins when set to
266
- * `false`.
267
- */
268
- editable?: boolean | ((context: CellContext<TData>) => boolean)
269
- /**
270
- * Transform the committed edit value before it is written to the row.
271
- * Runs after the built-in per-`editorType` coercion, so `newValue` is
272
- * already type-parsed; return the final value to store (e.g. round a
273
- * number, uppercase a code, look up an id). AG-Grid-style `valueParser`.
274
- */
275
- valueParser?: (params: ValueParserParams<TData>) => unknown
276
- /**
277
- * Briefly flash / highlight this column's cell when its value changes
278
- * (streaming feeds, edits, server pushes). `true` uses the default flash;
279
- * pass `{ className }` to apply your own animation class instead.
280
- */
281
- cellFlash?: boolean | { className?: string }
282
- /**
283
- * When `false`, this column never shows a sort indicator and clicking
284
- * its header is a no-op - `api.setSort(thisColumn, ...)` is also
285
- * ignored. Defaults to `true` (the column participates in sorting as
286
- * long as `rowSortingFeature` is registered).
287
- */
288
- sortable?: boolean
289
- /**
290
- * When `false`, this column never shows a filter funnel / menu and
291
- * `api.setFilter(thisColumn, ...)` is ignored. Defaults to `true` (the
292
- * column is filterable as long as `columnFilteringFeature` is
293
- * registered).
294
- */
295
- filterable?: boolean
296
- /**
297
- * Options for `editorType: 'list' | 'chips'`. Either bare values (the
298
- * string is both value and label) or `{ value, label }` objects.
299
- * For `chips` this is optional - when omitted, the chips editor becomes
300
- * free-form (user types and presses Enter to commit a chip).
301
- *
302
- * Pass a function `(row) => options` for row-dependent (cascading)
303
- * options - e.g. City options that depend on Country in the same row.
304
- */
305
- editorOptions?:
306
- | ReadonlyArray<
307
- string | number | { value: string | number; label?: string; color?: string }
308
- >
309
- | ((
310
- row: TData,
311
- ) => ReadonlyArray<
312
- string | number | { value: string | number; label?: string; color?: string }
313
- >)
314
- /** When true, list/chips allow multiple selections. Cell value becomes an array. */
315
- editorMultiple?: boolean
316
- /** Separator used when joining array values for the readonly cell display. Defaults to ', '. */
317
- editorSeparator?: string
318
- format?: CellFormatConfig
319
- formatter?: CellFormatter<TData>
320
- /**
321
- * Aggregate this column's values into the group row when grouping is
322
- * active. `'sum' | 'avg' | 'min' | 'max' | 'count' | 'countDistinct' |
323
- * 'extent' | 'first'`, or a custom `(values, rows) => unknown`. The result
324
- * is formatted with this column's `format` and shown in the group header.
325
- */
326
- aggregate?: GroupAggregator<TData>
327
- /**
328
- * Render the cell as an in-cell sparkline chart. The cell value should be
329
- * an array of numbers (or a comma/space separated string). Mutually
330
- * exclusive with a custom `cell` renderer (a `cell` wins if both are set).
331
- *
332
- * { sparkline: { type: 'line' } } // default line
333
- * { sparkline: { type: 'bar', color: '#16a34a' } }
334
- * { sparkline: { type: 'winloss' } } // sign-only up/down
335
- *
336
- * See `SparklineConfig` for the full option set (type, color,
337
- * negativeColor, width, height, fixed min/max).
338
- */
339
- sparkline?: SparklineConfig
340
- /** Initial column width in pixels. Falls back to the grid's `columnWidth` prop. */
341
- width?: number
342
- /**
343
- * Initial visibility. Set `false` to start the column hidden while still
344
- * listing it in the Choose Columns UI for the user to re-enable. Applied
345
- * once at mount; after that `api.setColumnVisible` / user toggles win.
346
- * On a group column, `false` hides the whole group's leaf columns.
347
- */
348
- visible?: boolean
349
- /**
350
- * Horizontal alignment for header and body cells. When omitted, the
351
- * default is inferred from `editorType`:
352
- * - `'number' | 'date' | 'datetime'` → `'right'`
353
- * - `'checkbox'` → `'center'`
354
- * - everything else → `'left'`
355
- */
356
- align?: 'left' | 'center' | 'right'
357
- /**
358
- * Per-cell conditional CSS. Two shapes:
359
- *
360
- * - **String** (or array of strings): class name(s) added to the
361
- * cell's `<td>` for every row in this column.
362
- * - **Function**: invoked per cell with the same `CellContext` shape
363
- * the `cell` renderer receives. Return a string, an array of
364
- * strings, or an object mapping class names to booleans.
365
- *
366
- * Use it for status tinting, conditional bold, "negative number"
367
- * coloring - anything that's a function of the row's value. Cells
368
- * still receive their format / cell renderer; the class just
369
- * augments the rendered `<td>`.
370
- */
371
- cellClass?:
372
- | string
373
- | ReadonlyArray<string>
374
- | ((ctx: CellContext<TData>) => string | ReadonlyArray<string> | Record<string, boolean> | undefined | null)
375
- }
376
-
377
- export type Column<TData extends RowData> = {
378
- id: string
379
- columnDef: ColumnDef<any, TData>
380
- depth: number
381
- parentId?: string
382
- getCanSort: () => boolean
383
- getCanFilter: () => boolean
384
- getIsSorted: () => false | 'asc' | 'desc'
385
- getToggleSortingHandler: () => () => void
386
- }
387
-
388
- export type Header<TData extends RowData> = {
389
- id: string
390
- isPlaceholder: boolean
391
- colSpan: number
392
- column: Column<TData>
393
- getContext: () => HeaderContext<TData>
394
- }
395
-
396
- export type HeaderGroup<TData extends RowData> = {
397
- id: string
398
- headers: Array<Header<TData>>
399
- }
400
-
401
- export type Cell<TData extends RowData> = {
402
- id: string
403
- row: Row<TData>
404
- column: Column<TData>
405
- getValue: () => unknown
406
- getContext: () => CellContext<TData>
407
- }
408
-
409
- export type Row<TData extends RowData> = {
410
- id: string
411
- index: number
412
- original: TData
413
- depth: number
414
- subRows?: Array<Row<TData>>
415
- /** Total leaf (data) rows under this group row. Undefined for data rows. */
416
- leafCount?: number
417
- getCanExpand: () => boolean
418
- getIsExpanded: () => boolean
419
- toggleExpanded: () => void
420
- getIsSelected: () => boolean
421
- toggleSelected: () => void
422
- getAllCells: () => Array<Cell<TData>>
423
- getCellValueByColumnId: (columnId: string) => unknown
424
- }
425
-
426
- export type RowModel<TData extends RowData> = {
427
- rows: Array<Row<TData>>
428
- }
429
-
430
- export type Store<T> = {
431
- readonly state: T
432
- setState: (updater: (prev: T) => T) => void
433
- subscribe: (listener: () => void) => () => void
434
- }
435
-
436
- function createStore<T>(initial: T): Store<T> {
437
- let value = initial
438
- const listeners = new Set<() => void>()
439
- return {
440
- get state() {
441
- return value
442
- },
443
- setState(updater) {
444
- value = updater(value)
445
- listeners.forEach((listener) => listener())
446
- },
447
- subscribe(listener) {
448
- listeners.add(listener)
449
- return () => listeners.delete(listener)
450
- },
451
- }
452
- }
453
-
454
- export const rowSortingFeature = { key: 'rowSortingFeature' }
455
- export const columnFilteringFeature = { key: 'columnFilteringFeature' }
456
- export const rowPaginationFeature = { key: 'rowPaginationFeature' }
457
- export const columnGroupingFeature = { key: 'columnGroupingFeature' }
458
- export const rowSelectionFeature = { key: 'rowSelectionFeature' }
459
- export const rowExpandingFeature = { key: 'rowExpandingFeature' }
460
-
461
- export function tableFeatures<T extends TableFeatures>(features: T): T {
462
- return features
463
- }
464
-
465
- export const sortFns = {
466
- auto: (a: unknown, b: unknown) => String(a).localeCompare(String(b)),
467
- number: (a: unknown, b: unknown) => Number(a ?? 0) - Number(b ?? 0),
468
- date: (a: unknown, b: unknown) => {
469
- const aa = new Date(a as any).getTime()
470
- const bb = new Date(b as any).getTime()
471
- return aa - bb
472
- },
473
- }
474
-
475
- export const filterFns = {
476
- includesString: (value: unknown, query: string) =>
477
- String(value).toLowerCase().includes(query.toLowerCase()),
478
- equals: (value: unknown, query: unknown) => value === query,
479
- }
480
-
481
- export type RowModelFactory<TData extends RowData> = (args: {
482
- table: SvGrid<TData>
483
- rows: Array<Row<TData>>
484
- }) => Array<Row<TData>>
485
-
486
- export function createCoreRowModel<TData extends RowData>(): RowModelFactory<TData> {
487
- return ({ rows }) => rows
488
- }
489
- export function createFilteredRowModel<TData extends RowData>(): RowModelFactory<TData> {
490
- return ({ table, rows }) => {
491
- const filters: ColumnFiltersState = table.getState().columnFilters ?? []
492
- if (!filters.length) return rows
493
- return rows.filter((row) => {
494
- return filters.every((filter) => {
495
- const cellValue = row
496
- .getAllCells()
497
- .find((cell) => cell.column.id === filter.id)
498
- ?.getValue()
499
- const filterFn = filter.fn ? filterFns[filter.fn] : filterFns.includesString
500
- return filterFn(cellValue, filter.value as any)
501
- })
502
- })
503
- }
504
- }
505
- export function createPaginatedRowModel<TData extends RowData>(): RowModelFactory<TData> {
506
- return ({ table, rows }) => {
507
- const pagination = table.getState().pagination ?? { pageIndex: 0, pageSize: rows.length || 10 }
508
- const start = pagination.pageIndex * pagination.pageSize
509
- return rows.slice(start, start + pagination.pageSize)
510
- }
511
- }
512
- export function createGroupedRowModel<TData extends RowData>(): RowModelFactory<TData> {
513
- return ({ table, rows }) => {
514
- const grouping: GroupingState = table.getState().grouping ?? []
515
- if (!grouping.length) return rows
516
- const columns = table.getAllColumns()
517
-
518
- // Recursively bucket rows by each grouping column in turn. At every level a
519
- // group row is built that stands in for its children - a non-group column
520
- // resolves to the value shared by every leaf row, or to undefined when the
521
- // leaves disagree.
522
- function buildGroups(
523
- input: Array<Row<TData>>,
524
- levelIndex: number,
525
- depth: number,
526
- idPrefix: string,
527
- ): Array<Row<TData>> {
528
- if (levelIndex >= grouping.length) {
529
- // Leaves: actual data rows, with their nesting depth recorded.
530
- return input.map((row) => ({ ...row, depth }))
531
- }
532
- const groupKey = grouping[levelIndex]
533
- if (!groupKey) return input
534
-
535
- const buckets = new Map<string, Array<Row<TData>>>()
536
- for (const row of input) {
537
- const value = row.getCellValueByColumnId(groupKey)
538
- const key = String(value ?? '')
539
- const list = buckets.get(key) ?? []
540
- list.push(row)
541
- buckets.set(key, list)
542
- }
543
-
544
- const groupRows: Array<Row<TData>> = []
545
- let index = 0
546
- buckets.forEach((children, key) => {
547
- const id = `${idPrefix}_${groupKey}_${key}`
548
- const subRows = buildGroups(children, levelIndex + 1, depth + 1, id)
549
- const isDeepest = levelIndex + 1 >= grouping.length
550
- const leafCount = isDeepest
551
- ? subRows.length
552
- : subRows.reduce((sum, sub) => sum + (sub.leafCount ?? 0), 0)
553
-
554
- const resolveColumnValue = (columnId: string): unknown => {
555
- if (columnId === groupKey) return key
556
- let resolved: unknown
557
- let hasResolved = false
558
- for (const child of children) {
559
- const childValue = child.getCellValueByColumnId(columnId)
560
- if (!hasResolved) {
561
- resolved = childValue
562
- hasResolved = true
563
- } else if (childValue !== resolved) {
564
- return undefined
565
- }
566
- }
567
- return resolved
568
- }
569
-
570
- const groupOriginal: Record<string, unknown> = {}
571
- columns.forEach((column) => {
572
- const field = column.columnDef.field
573
- if (!field) return
574
- const agg = column.columnDef.aggregate
575
- groupOriginal[field] = agg
576
- ? applyGroupAggregate(agg, column.id, children)
577
- : resolveColumnValue(column.id)
578
- })
579
-
580
- const groupRow: Row<TData> = {
581
- id,
582
- index: index++,
583
- original: groupOriginal as TData,
584
- depth,
585
- subRows,
586
- leafCount,
587
- getCanExpand: () => true,
588
- getIsExpanded: () => Boolean((table.getState().expanded ?? {})[id]),
589
- toggleExpanded: () => {
590
- table.setExpanded((prev) => ({ ...prev, [id]: !prev[id] }))
591
- },
592
- getIsSelected: () => Boolean((table.getState().rowSelection ?? {})[id]),
593
- toggleSelected: () => {
594
- table.setRowSelection((prev) => ({ ...prev, [id]: !prev[id] }))
595
- },
596
- getAllCells: () => [],
597
- // Prefer the precomputed group value (which carries aggregates)
598
- // and fall back to the shared-value resolver for columns without
599
- // a field.
600
- getCellValueByColumnId: (columnId: string) => {
601
- const col = columns.find((c) => c.id === columnId)
602
- const field = col?.columnDef.field
603
- if (field && field in groupOriginal) return groupOriginal[field]
604
- return resolveColumnValue(columnId)
605
- },
606
- }
607
- groupRows.push(groupRow)
608
- })
609
- return groupRows
610
- }
611
-
612
- return buildGroups(rows, 0, 0, 'group')
613
- }
614
- }
615
- export function createExpandedRowModel<TData extends RowData>(): RowModelFactory<TData> {
616
- return ({ table, rows }) => {
617
- const expanded: ExpandedState = table.getState().expanded ?? {}
618
- const flattened: Array<Row<TData>> = []
619
- const visit = (row: Row<TData>) => {
620
- flattened.push(row)
621
- if (row.subRows?.length && expanded[row.id]) {
622
- for (const sub of row.subRows) visit(sub)
623
- }
624
- }
625
- for (const row of rows) visit(row)
626
- return flattened
627
- }
628
- }
629
- export function createSortedRowModel<TData extends RowData>(
630
- localSortFns: typeof sortFns = sortFns,
631
- ): RowModelFactory<TData> {
632
- return ({ table, rows }) => {
633
- const sorting = table.getState().sorting ?? []
634
- if (!sorting.length) return rows
635
-
636
- const sorted = [...rows].sort((a, b) => {
637
- for (const clause of sorting) {
638
- const column = table.getAllColumns().find((col) => col.id === clause.id)
639
- if (!column) continue
640
- const editorType = column.columnDef.editorType
641
- const comparator =
642
- editorType === 'number'
643
- ? localSortFns.number
644
- : editorType === 'date' || editorType === 'datetime'
645
- ? localSortFns.date
646
- : localSortFns.auto
647
- const result = comparator(
648
- a.getCellValueByColumnId(column.id),
649
- b.getCellValueByColumnId(column.id),
650
- )
651
- if (result !== 0) return clause.desc ? -result : result
652
- }
653
- return 0
654
- })
655
- return sorted
656
- }
657
- }
658
-
659
- export type SvGridOptions<TFeatures extends TableFeatures, TData extends RowData> = {
660
- _features: TFeatures
661
- _rowModels?: {
662
- coreRowModel?: RowModelFactory<TData>
663
- filteredRowModel?: RowModelFactory<TData>
664
- sortedRowModel?: RowModelFactory<TData>
665
- paginatedRowModel?: RowModelFactory<TData>
666
- groupedRowModel?: RowModelFactory<TData>
667
- expandedRowModel?: RowModelFactory<TData>
668
- }
669
- columns: Array<ColumnDef<TFeatures, TData>>
670
- data: ReadonlyArray<TData>
671
- /**
672
- * Optional row-id resolver. When set, the value it returns becomes
673
- * `row.id` (and therefore the selection / expansion / edit key). When
674
- * omitted, ids fall back to the row's array index as a string. Use a
675
- * stable id (database PK, UUID, etc.) so selection survives reorders.
676
- */
677
- getRowId?: (row: TData, index: number) => string
678
- state?: Partial<Record<string, any>>
679
- onSortingChange?: (updater: Updater<SortingState>) => void
680
- onColumnFiltersChange?: (updater: Updater<ColumnFiltersState>) => void
681
- onPaginationChange?: (updater: Updater<PaginationState>) => void
682
- onGroupingChange?: (updater: Updater<GroupingState>) => void
683
- onExpandedChange?: (updater: Updater<ExpandedState>) => void
684
- onRowSelectionChange?: (updater: Updater<RowSelectionState>) => void
685
- onActiveCellChange?: (updater: Updater<ActiveCellState>) => void
686
- }
687
-
688
- export type SvGrid<TData extends RowData> = {
689
- store: Store<Record<string, any>>
690
- optionsStore: Store<Record<string, any>>
691
- state: Record<string, any>
692
- getState: () => Record<string, any>
693
- setOptions: (updater: Updater<Record<string, any>>) => void
694
- setColumnFilters: (updater: Updater<ColumnFiltersState>) => void
695
- setPagination: (updater: Updater<PaginationState>) => void
696
- setGrouping: (updater: Updater<GroupingState>) => void
697
- setExpanded: (updater: Updater<ExpandedState>) => void
698
- setRowSelection: (updater: Updater<RowSelectionState>) => void
699
- setActiveCell: (updater: Updater<ActiveCellState>) => void
700
- moveActiveCell: (next: { rowDelta?: number; colDelta?: number }) => void
701
- getAllColumns: () => Array<Column<TData>>
702
- getHeaderGroups: () => Array<HeaderGroup<TData>>
703
- getFooterGroups: () => Array<HeaderGroup<TData>>
704
- getRowModel: () => RowModel<TData>
705
- }
706
-
707
- type InternalGrid<TData extends RowData> = SvGrid<TData> & {
708
- getAllColumns: () => Array<Column<TData>>
709
- }
710
-
711
- export function createSvGridCore<TFeatures extends TableFeatures, TData extends RowData>(
712
- options: SvGridOptions<TFeatures, TData>,
713
- ): SvGrid<TData> {
714
- const internalState: Record<string, any> = {
715
- sorting: [],
716
- columnFilters: [],
717
- pagination: { pageIndex: 0, pageSize: options.data.length || 10 },
718
- grouping: [],
719
- expanded: {},
720
- rowSelection: {},
721
- activeCell: { rowIndex: 0, colIndex: 0, cellId: null },
722
- ...(options.state ?? {}),
723
- }
724
- const store = createStore(internalState)
725
- const optionsStore = createStore(options as Record<string, any>)
726
- let cachedColumnsInput: Array<ColumnDef<TFeatures, TData>> | null = null
727
- let cachedColumns: Array<Column<TData>> = []
728
- let cachedHeaderGroups: Array<HeaderGroup<TData>> = []
729
- let cachedBaseRowsInput: ReadonlyArray<TData> | null = null
730
- let cachedBaseRowsColumns: Array<Column<TData>> | null = null
731
- let cachedBaseRows: Array<Row<TData>> = []
732
- let cachedRowModel: RowModel<TData> | null = null
733
- let cachedRowModelBaseRows: Array<Row<TData>> | null = null
734
- let cachedPipeline = options._rowModels
735
- let cachedSlices: {
736
- sorting: SortingState | undefined
737
- columnFilters: ColumnFiltersState | undefined
738
- pagination: PaginationState | undefined
739
- grouping: GroupingState | undefined
740
- expanded: ExpandedState | undefined
741
- rowSelection: RowSelectionState | undefined
742
- } | null = null
743
-
744
- const grid = {
745
- store,
746
- optionsStore,
747
- get state() {
748
- return store.state
749
- },
750
- getState() {
751
- return store.state
752
- },
753
- setOptions(updater: Updater<Record<string, any>>) {
754
- optionsStore.setState((prev) =>
755
- typeof updater === 'function' ? (updater as any)(prev) : updater,
756
- )
757
- },
758
- setColumnFilters(updater: Updater<ColumnFiltersState>) {
759
- store.setState((prev) => ({
760
- ...prev,
761
- columnFilters:
762
- typeof updater === 'function' ? (updater as any)(prev.columnFilters ?? []) : updater,
763
- }))
764
- options.onColumnFiltersChange?.(updater)
765
- },
766
- setPagination(updater: Updater<PaginationState>) {
767
- store.setState((prev) => ({
768
- ...prev,
769
- pagination:
770
- typeof updater === 'function'
771
- ? (updater as any)(prev.pagination ?? { pageIndex: 0, pageSize: 10 })
772
- : updater,
773
- }))
774
- options.onPaginationChange?.(updater)
775
- },
776
- setGrouping(updater: Updater<GroupingState>) {
777
- store.setState((prev) => ({
778
- ...prev,
779
- grouping: typeof updater === 'function' ? (updater as any)(prev.grouping ?? []) : updater,
780
- }))
781
- options.onGroupingChange?.(updater)
782
- },
783
- setExpanded(updater: Updater<ExpandedState>) {
784
- store.setState((prev) => ({
785
- ...prev,
786
- expanded: typeof updater === 'function' ? (updater as any)(prev.expanded ?? {}) : updater,
787
- }))
788
- options.onExpandedChange?.(updater)
789
- },
790
- setRowSelection(updater: Updater<RowSelectionState>) {
791
- store.setState((prev) => ({
792
- ...prev,
793
- rowSelection:
794
- typeof updater === 'function' ? (updater as any)(prev.rowSelection ?? {}) : updater,
795
- }))
796
- options.onRowSelectionChange?.(updater)
797
- },
798
- setActiveCell(updater: Updater<ActiveCellState>) {
799
- store.setState((prev) => {
800
- const previous: ActiveCellState = prev.activeCell ?? {
801
- rowIndex: 0,
802
- colIndex: 0,
803
- cellId: null,
804
- }
805
- const nextActive =
806
- typeof updater === 'function' ? updater(previous) : updater
807
- return {
808
- ...prev,
809
- activeCell: nextActive,
810
- }
811
- })
812
- options.onActiveCellChange?.(updater)
813
- },
814
- moveActiveCell(next: { rowDelta?: number; colDelta?: number }) {
815
- const rows = grid.getRowModel().rows
816
- const columns = grid.getAllColumns()
817
- const maxRow = Math.max(rows.length - 1, 0)
818
- const maxCol = Math.max(columns.length - 1, 0)
819
- const current: ActiveCellState = grid.getState().activeCell ?? {
820
- rowIndex: 0,
821
- colIndex: 0,
822
- cellId: null,
823
- }
824
-
825
- const rowIndex = Math.min(
826
- Math.max(current.rowIndex + (next.rowDelta ?? 0), 0),
827
- maxRow,
828
- )
829
- const colIndex = Math.min(
830
- Math.max(current.colIndex + (next.colDelta ?? 0), 0),
831
- maxCol,
832
- )
833
- const columnId = columns[colIndex]?.id ?? 'col_0'
834
- grid.setActiveCell({
835
- rowIndex,
836
- colIndex,
837
- cellId: `${rowIndex}_${columnId}`,
838
- })
839
- },
840
- getAllColumns() {
841
- // Cache hit: referentially identical columns array.
842
- if (cachedColumnsInput === options.columns && cachedColumns.length) {
843
- return cachedColumns
844
- }
845
- // Soft cache hit: consumers commonly recreate the columns array
846
- // inline on every render (e.g. `columns={[...]}`). If the new
847
- // array has the same length AND each entry has the same `field` /
848
- // `id` / `header` (the visibility-affecting structure of a
849
- // column), trust the previous build. Mutable inner fields like
850
- // `cell` and `editorOptions` are still picked up on the next real
851
- // render that bumps an actual data dep - they're read at cell-
852
- // render time, not at this top-level cache.
853
- if (
854
- cachedColumnsInput &&
855
- options.columns.length === cachedColumnsInput.length &&
856
- cachedColumns.length === options.columns.length &&
857
- options.columns.every((c, i) => {
858
- const prev = cachedColumnsInput![i]!
859
- return (
860
- c.field === prev.field &&
861
- c.id === prev.id &&
862
- c.header === prev.header &&
863
- c.editorType === prev.editorType
864
- )
865
- })
866
- ) {
867
- // Update the stored input reference so the strict check hits
868
- // next time, but reuse the built column model.
869
- cachedColumnsInput = options.columns
870
- return cachedColumns
871
- }
872
-
873
- cachedColumnsInput = options.columns
874
- cachedHeaderGroups = []
875
- const build = (
876
- defs: Array<ColumnDef<TFeatures, TData>>,
877
- depth: number,
878
- parentId?: string,
879
- ): Array<Column<TData>> => {
880
- const leaves: Array<Column<TData>> = []
881
- defs.forEach((columnDef, index) => {
882
- const id = columnDef.id ?? columnDef.field ?? `${parentId ?? 'col'}_${depth}_${index}`
883
- if (columnDef.columns?.length) {
884
- leaves.push(...build(columnDef.columns, depth + 1, id))
885
- return
886
- }
887
- leaves.push({
888
- id,
889
- depth,
890
- parentId,
891
- columnDef,
892
- getCanSort: () =>
893
- Boolean((options._features as any).rowSortingFeature) &&
894
- columnDef.sortable !== false,
895
- getCanFilter: () =>
896
- Boolean((options._features as any).columnFilteringFeature) &&
897
- columnDef.filterable !== false,
898
- getIsSorted: () => {
899
- const entry = store.state.sorting?.find((s: any) => s.id === id)
900
- if (!entry) return false
901
- return entry.desc ? 'desc' : 'asc'
902
- },
903
- getToggleSortingHandler: () => () => {
904
- const clauses: SortingState = store.state.sorting ?? []
905
- const current = clauses.find((s: any) => s.id === id)
906
- const nextClause: SortingState = !current
907
- ? [...clauses, { id, desc: false }]
908
- : current.desc
909
- ? clauses.filter((s) => s.id !== id)
910
- : clauses.map((s) => (s.id === id ? { ...s, desc: true } : s))
911
- store.setState((prev) => ({ ...prev, sorting: nextClause }))
912
- options.onSortingChange?.(nextClause)
913
- },
914
- })
915
- })
916
- return leaves
917
- }
918
- cachedColumns = build(options.columns, 0)
919
- return cachedColumns
920
- },
921
- getHeaderGroups() {
922
- if (cachedHeaderGroups.length) return cachedHeaderGroups
923
- const headers = grid.getAllColumns().map((column) => {
924
- const header: Header<TData> = {
925
- id: column.id,
926
- isPlaceholder: false,
927
- colSpan: 1,
928
- column,
929
- getContext: () => ({ header, column, table: grid }),
930
- }
931
- return header
932
- })
933
- cachedHeaderGroups = [{ id: 'header_group_0', headers }]
934
- return cachedHeaderGroups
935
- },
936
- getFooterGroups() {
937
- return grid.getHeaderGroups()
938
- },
939
- getRowModel() {
940
- const columns = grid.getAllColumns()
941
- if (cachedBaseRowsInput !== options.data || cachedBaseRowsColumns !== columns) {
942
- cachedBaseRowsInput = options.data
943
- cachedBaseRowsColumns = columns
944
- // O(1) column-id → index lookup so getCellValueByColumnId doesn't do
945
- // a linear `findIndex` on every cell read (was O(rows × cells × cols)).
946
- const columnIndexById = new Map<string, number>()
947
- for (let i = 0; i < columns.length; i++) columnIndexById.set(columns[i]!.id, i)
948
- const columnCount = columns.length
949
-
950
- cachedBaseRows = new Array(options.data.length)
951
- const getRowId = options.getRowId
952
- for (let index = 0; index < options.data.length; index++) {
953
- const original = options.data[index]!
954
- const id = getRowId ? getRowId(original, index) : String(index)
955
- // `values` and `cells` are computed lazily - for a 100k-row grid
956
- // with only ~20 visible rows we don't want to materialise every
957
- // row's full value array or cell objects up front.
958
- let cachedValues: Array<unknown> | null = null
959
- let cachedCells: Array<Cell<TData>> | null = null
960
-
961
- function computeValues(): Array<unknown> {
962
- const values = new Array<unknown>(columnCount)
963
- for (let i = 0; i < columnCount; i++) {
964
- const column = columns[i]!
965
- if (column.columnDef.accessorFn) {
966
- values[i] = column.columnDef.accessorFn(original)
967
- } else if (column.columnDef.field) {
968
- values[i] = (original as any)[column.columnDef.field]
969
- } else {
970
- values[i] = undefined
971
- }
972
- }
973
- return values
974
- }
975
-
976
- const row: Row<TData> = {
977
- id,
978
- index,
979
- original,
980
- depth: 0,
981
- getCanExpand: () => false,
982
- getIsExpanded: () => Boolean((store.state.expanded ?? {})[id]),
983
- toggleExpanded: () => {
984
- grid.setExpanded((prev) => ({ ...prev, [id]: !prev[id] }))
985
- },
986
- getIsSelected: () => Boolean((store.state.rowSelection ?? {})[id]),
987
- toggleSelected: () => {
988
- grid.setRowSelection((prev) => ({ ...prev, [id]: !prev[id] }))
989
- },
990
- getAllCells: () => {
991
- if (cachedCells) return cachedCells
992
- const built = new Array<Cell<TData>>(columnCount)
993
- for (let i = 0; i < columnCount; i++) {
994
- const column = columns[i]!
995
- const colIndex = i
996
- const cell: Cell<TData> = {
997
- id: `${id}_${column.id}`,
998
- row,
999
- column,
1000
- getValue: () => {
1001
- if (!cachedValues) cachedValues = computeValues()
1002
- return cachedValues[colIndex]
1003
- },
1004
- getContext: () => ({
1005
- cell,
1006
- row,
1007
- column,
1008
- table: grid,
1009
- getValue: () => cell.getValue(),
1010
- }),
1011
- }
1012
- built[i] = cell
1013
- }
1014
- cachedCells = built
1015
- return built
1016
- },
1017
- getCellValueByColumnId: (columnId: string) => {
1018
- const idx = columnIndexById.get(columnId)
1019
- if (idx === undefined) return undefined
1020
- if (!cachedValues) cachedValues = computeValues()
1021
- return cachedValues[idx]
1022
- },
1023
- }
1024
- cachedBaseRows[index] = row
1025
- }
1026
- }
1027
-
1028
- const currentSlices = {
1029
- sorting: store.state.sorting,
1030
- columnFilters: store.state.columnFilters,
1031
- pagination: store.state.pagination,
1032
- grouping: store.state.grouping,
1033
- expanded: store.state.expanded,
1034
- rowSelection: store.state.rowSelection,
1035
- }
1036
- if (
1037
- cachedRowModel &&
1038
- cachedRowModelBaseRows === cachedBaseRows &&
1039
- cachedPipeline === options._rowModels &&
1040
- cachedSlices?.sorting === currentSlices.sorting &&
1041
- cachedSlices?.columnFilters === currentSlices.columnFilters &&
1042
- cachedSlices?.pagination === currentSlices.pagination &&
1043
- cachedSlices?.grouping === currentSlices.grouping &&
1044
- cachedSlices?.expanded === currentSlices.expanded &&
1045
- cachedSlices?.rowSelection === currentSlices.rowSelection
1046
- ) {
1047
- return cachedRowModel
1048
- }
1049
-
1050
- let rows: Array<Row<TData>> = cachedBaseRows
1051
-
1052
- const pipeline = options._rowModels ?? {}
1053
- const ordered: Array<RowModelFactory<TData> | undefined> = [
1054
- pipeline.coreRowModel,
1055
- pipeline.filteredRowModel,
1056
- pipeline.sortedRowModel,
1057
- pipeline.groupedRowModel,
1058
- pipeline.expandedRowModel,
1059
- pipeline.paginatedRowModel,
1060
- ]
1061
- ordered.forEach((fn) => {
1062
- if (fn) rows = fn({ table: grid, rows })
1063
- })
1064
- cachedPipeline = options._rowModels
1065
- cachedSlices = currentSlices
1066
- cachedRowModelBaseRows = cachedBaseRows
1067
- cachedRowModel = { rows }
1068
- return cachedRowModel
1069
- },
1070
- } as InternalGrid<TData>
1071
-
1072
- return grid
1073
- }
1074
-
1075
- export function isFunction(value: unknown): value is (...args: Array<any>) => any {
1076
- return typeof value === 'function'
1077
- }
1
+ import type { SparklineConfig } from './sparkline'
2
+
3
+ export type RowData = Record<string, unknown>
4
+ export type Updater<T> = T | ((prev: T) => T)
5
+ export type SortingState = Array<{ id: string; desc: boolean }>
6
+ export type ColumnFilter = { id: string; value: unknown; fn?: keyof typeof filterFns }
7
+ export type ColumnFiltersState = Array<ColumnFilter>
8
+ export type PaginationState = { pageIndex: number; pageSize: number }
9
+ export type GroupingState = Array<string>
10
+ export type ExpandedState = Record<string, boolean>
11
+ export type RowSelectionState = Record<string, boolean>
12
+ export type ActiveCellState = {
13
+ rowIndex: number
14
+ colIndex: number
15
+ cellId: string | null
16
+ }
17
+ export type TableFeatures = Record<string, unknown>
18
+
19
+ export type CellData = unknown
20
+
21
+ export type HeaderContext<TData extends RowData> = {
22
+ header: Header<TData>
23
+ column: Column<TData>
24
+ table: SvGrid<TData>
25
+ }
26
+
27
+ export type CellContext<TData extends RowData> = {
28
+ cell: Cell<TData>
29
+ row: Row<TData>
30
+ column: Column<TData>
31
+ table: SvGrid<TData>
32
+ getValue: () => unknown
33
+ }
34
+
35
+ /** Params passed to a column's `colSpan(...)` / `rowSpan(...)` callbacks. */
36
+ export type CellSpanParams<TData extends RowData = RowData> = {
37
+ /** The row's underlying data object. */
38
+ data: TData
39
+ /** Display-row index in the current (filtered/sorted) row set. */
40
+ rowIndex: number
41
+ /** The column's id. */
42
+ columnId: string
43
+ /** The cell's base value for this column. */
44
+ value: unknown
45
+ }
46
+
47
+ /** Params passed to a column's `valueParser(...)` on edit commit. */
48
+ export type ValueParserParams<TData extends RowData = RowData> = {
49
+ /** The value after built-in per-`editorType` coercion. */
50
+ newValue: unknown
51
+ /** The cell's previous value. */
52
+ oldValue: unknown
53
+ /** The raw string the editor produced (pre-coercion). */
54
+ rawInput: string
55
+ /** The row's underlying data object. */
56
+ data: TData
57
+ /** The column's id. */
58
+ columnId: string
59
+ }
60
+
61
+ /**
62
+ * Context passed to a custom `cellEditor` snippet/component. Three write
63
+ * helpers cover the lifecycle:
64
+ *
65
+ * - `update(next)` - stage `next` as the draft, keep the editor open.
66
+ * Use this for live-preview controls (sliders,
67
+ * color pickers) so the user can keep adjusting.
68
+ * - `commit(next?)` - write the value AND close the editor. The
69
+ * argument is optional; when omitted, the most
70
+ * recently `update()`d value is saved. Use this
71
+ * for "done" gestures (Enter, picking an option).
72
+ * - `cancel()` - discard the draft and close the editor.
73
+ */
74
+ export type EditorContext<TData extends RowData> = CellContext<TData> & {
75
+ value: unknown
76
+ update: (next: unknown) => void
77
+ commit: (next?: unknown) => void
78
+ cancel: () => void
79
+ }
80
+
81
+ export type CellFormatConfig =
82
+ | {
83
+ type: 'number'
84
+ locales?: string | Array<string>
85
+ options?: Intl.NumberFormatOptions
86
+ }
87
+ | {
88
+ type: 'currency'
89
+ /** ISO 4217 (default USD) */
90
+ currency?: string
91
+ locales?: string | Array<string>
92
+ options?: Omit<Intl.NumberFormatOptions, 'style' | 'currency'>
93
+ }
94
+ | {
95
+ type: 'percent'
96
+ locales?: string | Array<string>
97
+ options?: Omit<Intl.NumberFormatOptions, 'style'>
98
+ /**
99
+ * If true, numeric cell values are 0–100 (e.g. 42 → 42%) instead of Intl’s 0–1 fraction (0.42 → 42%).
100
+ * Default false.
101
+ */
102
+ valueIsPercentPoints?: boolean
103
+ }
104
+ | {
105
+ type: 'date' | 'datetime'
106
+ locales?: string | Array<string>
107
+ /**
108
+ * Shortcut patterns merged with `options`:
109
+ * `'d'` short numeric date, `'D'` long date, `'y-m-d'` yyyy/mm/dd-style,
110
+ * `'short'`|`'medium'`|`'long'` use dateStyle/timeStyle presets.
111
+ */
112
+ pattern?: string
113
+ options?: Intl.DateTimeFormatOptions
114
+ }
115
+
116
+ export type CellFormatter<TData extends RowData> = (context: {
117
+ value: unknown
118
+ row: Row<TData>
119
+ column: Column<TData>
120
+ table: SvGrid<TData>
121
+ }) => string
122
+
123
+ export type ColumnDefTemplate<TContext> = string | ((context: TContext) => unknown)
124
+
125
+ /**
126
+ * How a column's value is aggregated for a group row when `columnGrouping`
127
+ * is active. Built-in reducers cover the common cases; pass a function for
128
+ * anything custom (weighted average, median, percentile, distinct count).
129
+ * The function receives the finite numeric values AND the raw leaf rows.
130
+ */
131
+ export type GroupAggregator<TData = any> =
132
+ | 'sum'
133
+ | 'avg'
134
+ | 'min'
135
+ | 'max'
136
+ | 'count'
137
+ | 'countDistinct'
138
+ | 'extent'
139
+ | 'first'
140
+ | ((values: number[], rows: Array<TData>) => unknown)
141
+
142
+ /** Apply a group aggregator over a bucket's leaf rows for one column. */
143
+ export function applyGroupAggregate<TData extends RowData>(
144
+ agg: GroupAggregator<TData>,
145
+ columnId: string,
146
+ rows: ReadonlyArray<Row<TData>>,
147
+ ): unknown {
148
+ const raw = rows.map((r) => r.getCellValueByColumnId(columnId))
149
+ if (typeof agg === 'function') {
150
+ const nums = raw.map((v) => Number(v)).filter((n) => Number.isFinite(n))
151
+ return agg(nums, rows.map((r) => r.original))
152
+ }
153
+ if (agg === 'count') return rows.length
154
+ if (agg === 'countDistinct') return new Set(raw.map((v) => String(v ?? ''))).size
155
+ if (agg === 'first') return raw[0]
156
+ const nums = raw.map((v) => Number(v)).filter((n) => Number.isFinite(n))
157
+ if (!nums.length) return undefined
158
+ switch (agg) {
159
+ case 'sum':
160
+ return nums.reduce((a, b) => a + b, 0)
161
+ case 'avg':
162
+ return nums.reduce((a, b) => a + b, 0) / nums.length
163
+ case 'min':
164
+ return Math.min(...nums)
165
+ case 'max':
166
+ return Math.max(...nums)
167
+ case 'extent':
168
+ return `${Math.min(...nums)} – ${Math.max(...nums)}`
169
+ default:
170
+ return undefined
171
+ }
172
+ }
173
+
174
+ export type ColumnDef<TFeatures extends TableFeatures, TData extends RowData> = {
175
+ id?: string
176
+ field?: keyof TData & string
177
+ fieldFn?: (row: TData) => unknown
178
+ header?: ColumnDefTemplate<HeaderContext<TData>>
179
+ footer?: ColumnDefTemplate<HeaderContext<TData>>
180
+ cell?: ColumnDefTemplate<CellContext<TData>>
181
+ columns?: Array<ColumnDef<TFeatures, TData>>
182
+ /**
183
+ * Declarative cell spanning (merged cells). Return how many COLUMNS this
184
+ * cell spans to the right (1 = no span). Value-driven, AG-Grid-style. Feed
185
+ * `spansToMerges(rows, columns)` into `spreadsheetLayout` to apply - it uses
186
+ * the same real `colspan`/`rowspan` merge engine (no separate code path).
187
+ */
188
+ colSpan?: (params: CellSpanParams<TData>) => number
189
+ /**
190
+ * Declarative cell spanning (merged cells). Return how many ROWS this cell
191
+ * spans downward (1 = no span). See `colSpan` for how to apply.
192
+ */
193
+ rowSpan?: (params: CellSpanParams<TData>) => number
194
+ /**
195
+ * High-level data type for the column. A convenience that resolves to the
196
+ * right `editorType`, alignment, date `format`, and filter operators without
197
+ * setting each by hand:
198
+ * 'text' → text editor, left-aligned
199
+ * 'number' → number editor, right-aligned, numeric filter operators
200
+ * 'boolean' → checkbox editor, centered
201
+ * 'date' → date editor (Date values), right-aligned, `{ type: 'date' }` format
202
+ * 'dateString' → date editor for ISO date STRINGS (e.g. '2026-06-27')
203
+ * Anything you set explicitly (`editorType`, `align`, `format`) still wins -
204
+ * `cellDataType` only fills the gaps. Grid-level `inferColumnTypes` infers
205
+ * this from the first data row for columns that declare neither.
206
+ */
207
+ cellDataType?: 'text' | 'number' | 'boolean' | 'date' | 'dateString'
208
+ /**
209
+ * For a column INSIDE a collapsible column group: `'open'` shows this column
210
+ * only while the group is expanded, `'closed'` only while collapsed. Omit to
211
+ * always show it. Setting it on any direct child gives the parent group a
212
+ * collapse toggle. Pair with `openByDefault` on the group.
213
+ */
214
+ columnGroupShow?: 'open' | 'closed'
215
+ /**
216
+ * For a GROUP column (one with `columns: [...]`): start the group expanded.
217
+ * Defaults to `false` (collapsed), matching AG Grid - so only the always-on
218
+ * and `columnGroupShow: 'closed'` children show until the user expands it.
219
+ */
220
+ openByDefault?: boolean
221
+ editorType?:
222
+ | 'text'
223
+ | 'number'
224
+ | 'date'
225
+ | 'datetime'
226
+ | 'time' // native <input type="time"> - HH:MM or HH:MM:SS
227
+ | 'password' // native <input type="password"> with masked rendering
228
+ | 'checkbox'
229
+ | 'list'
230
+ | 'chips'
231
+ | 'select' // custom dropdown - single value, no typeahead
232
+ | 'rich-select' // custom dropdown with a typeahead search input
233
+ | 'autocomplete' // free-text input with a live-filtered suggestion list (accepts any value)
234
+ | 'textarea' // multi-line editor; Tab or Ctrl+Enter commits, plain Enter inserts a newline
235
+ | 'color' // native <input type="color"> swatch
236
+ | 'rating' // 5-star rating control
237
+ /**
238
+ * Custom in-cell editor. Receives the cell context PLUS a `commit(value)`
239
+ * and `cancel()` helper. Use when none of the built-in `editorType`s fit;
240
+ * the snippet's outer element is mounted inside the editing cell and
241
+ * inherits keyboard handling (Esc cancels, Enter commits unless your
242
+ * snippet preventDefaults it).
243
+ *
244
+ * Coexists with `editorType`: when both are set, `cellEditor` wins and
245
+ * `editorType` is treated as a hint for parsing the saved value.
246
+ */
247
+ cellEditor?: ColumnDefTemplate<EditorContext<TData>>
248
+ /**
249
+ * Per-column tooltip. String shows as a native `title=`; `(ctx) => string`
250
+ * runs per cell so the tooltip can reflect the value. Returning an empty
251
+ * string skips the tooltip.
252
+ */
253
+ tooltip?: string | ((ctx: CellContext<TData>) => string | null | undefined)
254
+ /**
255
+ * Gate editing per column or per cell.
256
+ *
257
+ * - `true` (or omitted): the column is fully editable.
258
+ * - `false`: the column is read-only - double-click, type-to-edit,
259
+ * fill-handle drag, Delete, and clipboard paste all skip it.
260
+ * - `(ctx) => boolean`: evaluated for each cell, so you can lock
261
+ * individual rows (e.g. by role, status, ownership). Returning
262
+ * `false` opts the cell out of every editing path, identical to
263
+ * setting `editable: false` on the whole column for that row.
264
+ *
265
+ * The grid-wide `enableInlineEditing` prop still wins when set to
266
+ * `false`.
267
+ */
268
+ editable?: boolean | ((context: CellContext<TData>) => boolean)
269
+ /**
270
+ * Transform the committed edit value before it is written to the row.
271
+ * Runs after the built-in per-`editorType` coercion, so `newValue` is
272
+ * already type-parsed; return the final value to store (e.g. round a
273
+ * number, uppercase a code, look up an id). AG-Grid-style `valueParser`.
274
+ */
275
+ valueParser?: (params: ValueParserParams<TData>) => unknown
276
+ /**
277
+ * Briefly flash / highlight this column's cell when its value changes
278
+ * (streaming feeds, edits, server pushes). `true` uses the default flash;
279
+ * pass `{ className }` to apply your own animation class instead.
280
+ */
281
+ cellFlash?: boolean | { className?: string }
282
+ /**
283
+ * When `false`, this column never shows a sort indicator and clicking
284
+ * its header is a no-op - `api.setSort(thisColumn, ...)` is also
285
+ * ignored. Defaults to `true` (the column participates in sorting as
286
+ * long as `rowSortingFeature` is registered).
287
+ */
288
+ sortable?: boolean
289
+ /**
290
+ * When `false`, this column never shows a filter funnel / menu and
291
+ * `api.setFilter(thisColumn, ...)` is ignored. Defaults to `true` (the
292
+ * column is filterable as long as `columnFilteringFeature` is
293
+ * registered).
294
+ */
295
+ filterable?: boolean
296
+ /**
297
+ * Options for `editorType: 'list' | 'chips'`. Either bare values (the
298
+ * string is both value and label) or `{ value, label }` objects.
299
+ * For `chips` this is optional - when omitted, the chips editor becomes
300
+ * free-form (user types and presses Enter to commit a chip).
301
+ *
302
+ * Pass a function `(row) => options` for row-dependent (cascading)
303
+ * options - e.g. City options that depend on Country in the same row.
304
+ */
305
+ editorOptions?:
306
+ | ReadonlyArray<
307
+ string | number | { value: string | number; label?: string; color?: string }
308
+ >
309
+ | ((
310
+ row: TData,
311
+ ) => ReadonlyArray<
312
+ string | number | { value: string | number; label?: string; color?: string }
313
+ >)
314
+ /** When true, list/chips allow multiple selections. Cell value becomes an array. */
315
+ editorMultiple?: boolean
316
+ /** Separator used when joining array values for the readonly cell display. Defaults to ', '. */
317
+ editorSeparator?: string
318
+ format?: CellFormatConfig
319
+ formatter?: CellFormatter<TData>
320
+ /**
321
+ * Aggregate this column's values into the group row when grouping is
322
+ * active. `'sum' | 'avg' | 'min' | 'max' | 'count' | 'countDistinct' |
323
+ * 'extent' | 'first'`, or a custom `(values, rows) => unknown`. The result
324
+ * is formatted with this column's `format` and shown in the group header.
325
+ */
326
+ aggregate?: GroupAggregator<TData>
327
+ /**
328
+ * Render the cell as an in-cell sparkline chart. The cell value should be
329
+ * an array of numbers (or a comma/space separated string). Mutually
330
+ * exclusive with a custom `cell` renderer (a `cell` wins if both are set).
331
+ *
332
+ * { sparkline: { type: 'line' } } // default line
333
+ * { sparkline: { type: 'bar', color: '#16a34a' } }
334
+ * { sparkline: { type: 'winloss' } } // sign-only up/down
335
+ *
336
+ * See `SparklineConfig` for the full option set (type, color,
337
+ * negativeColor, width, height, fixed min/max).
338
+ */
339
+ sparkline?: SparklineConfig
340
+ /** Initial column width in pixels. Falls back to the grid's `columnWidth` prop. */
341
+ width?: number
342
+ /**
343
+ * Initial visibility. Set `false` to start the column hidden while still
344
+ * listing it in the Choose Columns UI for the user to re-enable. Applied
345
+ * once at mount; after that `api.setColumnVisible` / user toggles win.
346
+ * On a group column, `false` hides the whole group's leaf columns.
347
+ */
348
+ visible?: boolean
349
+ /**
350
+ * Horizontal alignment for header and body cells. When omitted, the
351
+ * default is inferred from `editorType`:
352
+ * - `'number' | 'date' | 'datetime'` → `'right'`
353
+ * - `'checkbox'` → `'center'`
354
+ * - everything else → `'left'`
355
+ */
356
+ align?: 'left' | 'center' | 'right'
357
+ /**
358
+ * Per-cell conditional CSS. Two shapes:
359
+ *
360
+ * - **String** (or array of strings): class name(s) added to the
361
+ * cell's `<td>` for every row in this column.
362
+ * - **Function**: invoked per cell with the same `CellContext` shape
363
+ * the `cell` renderer receives. Return a string, an array of
364
+ * strings, or an object mapping class names to booleans.
365
+ *
366
+ * Use it for status tinting, conditional bold, "negative number"
367
+ * coloring - anything that's a function of the row's value. Cells
368
+ * still receive their format / cell renderer; the class just
369
+ * augments the rendered `<td>`.
370
+ */
371
+ cellClass?:
372
+ | string
373
+ | ReadonlyArray<string>
374
+ | ((ctx: CellContext<TData>) => string | ReadonlyArray<string> | Record<string, boolean> | undefined | null)
375
+ }
376
+
377
+ export type Column<TData extends RowData> = {
378
+ id: string
379
+ columnDef: ColumnDef<any, TData>
380
+ depth: number
381
+ parentId?: string
382
+ getCanSort: () => boolean
383
+ getCanFilter: () => boolean
384
+ getIsSorted: () => false | 'asc' | 'desc'
385
+ getToggleSortingHandler: () => () => void
386
+ }
387
+
388
+ export type Header<TData extends RowData> = {
389
+ id: string
390
+ isPlaceholder: boolean
391
+ colSpan: number
392
+ column: Column<TData>
393
+ getContext: () => HeaderContext<TData>
394
+ }
395
+
396
+ export type HeaderGroup<TData extends RowData> = {
397
+ id: string
398
+ headers: Array<Header<TData>>
399
+ }
400
+
401
+ export type Cell<TData extends RowData> = {
402
+ id: string
403
+ row: Row<TData>
404
+ column: Column<TData>
405
+ getValue: () => unknown
406
+ getContext: () => CellContext<TData>
407
+ }
408
+
409
+ export type Row<TData extends RowData> = {
410
+ id: string
411
+ index: number
412
+ original: TData
413
+ depth: number
414
+ subRows?: Array<Row<TData>>
415
+ /** Total leaf (data) rows under this group row. Undefined for data rows. */
416
+ leafCount?: number
417
+ getCanExpand: () => boolean
418
+ getIsExpanded: () => boolean
419
+ toggleExpanded: () => void
420
+ getIsSelected: () => boolean
421
+ toggleSelected: () => void
422
+ getAllCells: () => Array<Cell<TData>>
423
+ getCellValueByColumnId: (columnId: string) => unknown
424
+ }
425
+
426
+ export type RowModel<TData extends RowData> = {
427
+ rows: Array<Row<TData>>
428
+ }
429
+
430
+ export type Store<T> = {
431
+ readonly state: T
432
+ setState: (updater: (prev: T) => T) => void
433
+ subscribe: (listener: () => void) => () => void
434
+ }
435
+
436
+ function createStore<T>(initial: T): Store<T> {
437
+ let value = initial
438
+ const listeners = new Set<() => void>()
439
+ return {
440
+ get state() {
441
+ return value
442
+ },
443
+ setState(updater) {
444
+ value = updater(value)
445
+ listeners.forEach((listener) => listener())
446
+ },
447
+ subscribe(listener) {
448
+ listeners.add(listener)
449
+ return () => listeners.delete(listener)
450
+ },
451
+ }
452
+ }
453
+
454
+ export const rowSortingFeature = { key: 'rowSortingFeature' }
455
+ export const columnFilteringFeature = { key: 'columnFilteringFeature' }
456
+ export const rowPaginationFeature = { key: 'rowPaginationFeature' }
457
+ export const columnGroupingFeature = { key: 'columnGroupingFeature' }
458
+ export const rowSelectionFeature = { key: 'rowSelectionFeature' }
459
+ export const rowExpandingFeature = { key: 'rowExpandingFeature' }
460
+
461
+ export function tableFeatures<T extends TableFeatures>(features: T): T {
462
+ return features
463
+ }
464
+
465
+ export const sortFns = {
466
+ auto: (a: unknown, b: unknown) => String(a).localeCompare(String(b)),
467
+ number: (a: unknown, b: unknown) => Number(a ?? 0) - Number(b ?? 0),
468
+ date: (a: unknown, b: unknown) => {
469
+ const aa = new Date(a as any).getTime()
470
+ const bb = new Date(b as any).getTime()
471
+ return aa - bb
472
+ },
473
+ }
474
+
475
+ export const filterFns = {
476
+ includesString: (value: unknown, query: string) =>
477
+ String(value).toLowerCase().includes(query.toLowerCase()),
478
+ equals: (value: unknown, query: unknown) => value === query,
479
+ }
480
+
481
+ export type RowModelFactory<TData extends RowData> = (args: {
482
+ table: SvGrid<TData>
483
+ rows: Array<Row<TData>>
484
+ }) => Array<Row<TData>>
485
+
486
+ export function createCoreRowModel<TData extends RowData>(): RowModelFactory<TData> {
487
+ return ({ rows }) => rows
488
+ }
489
+ export function createFilteredRowModel<TData extends RowData>(): RowModelFactory<TData> {
490
+ return ({ table, rows }) => {
491
+ const filters: ColumnFiltersState = table.getState().columnFilters ?? []
492
+ if (!filters.length) return rows
493
+ return rows.filter((row) => {
494
+ return filters.every((filter) => {
495
+ const cellValue = row
496
+ .getAllCells()
497
+ .find((cell) => cell.column.id === filter.id)
498
+ ?.getValue()
499
+ const filterFn = filter.fn ? filterFns[filter.fn] : filterFns.includesString
500
+ return filterFn(cellValue, filter.value as any)
501
+ })
502
+ })
503
+ }
504
+ }
505
+ export function createPaginatedRowModel<TData extends RowData>(): RowModelFactory<TData> {
506
+ return ({ table, rows }) => {
507
+ const pagination = table.getState().pagination ?? { pageIndex: 0, pageSize: rows.length || 10 }
508
+ const start = pagination.pageIndex * pagination.pageSize
509
+ return rows.slice(start, start + pagination.pageSize)
510
+ }
511
+ }
512
+ export function createGroupedRowModel<TData extends RowData>(): RowModelFactory<TData> {
513
+ return ({ table, rows }) => {
514
+ const grouping: GroupingState = table.getState().grouping ?? []
515
+ if (!grouping.length) return rows
516
+ const columns = table.getAllColumns()
517
+
518
+ // Recursively bucket rows by each grouping column in turn. At every level a
519
+ // group row is built that stands in for its children - a non-group column
520
+ // resolves to the value shared by every leaf row, or to undefined when the
521
+ // leaves disagree.
522
+ function buildGroups(
523
+ input: Array<Row<TData>>,
524
+ levelIndex: number,
525
+ depth: number,
526
+ idPrefix: string,
527
+ ): Array<Row<TData>> {
528
+ if (levelIndex >= grouping.length) {
529
+ // Leaves: actual data rows, with their nesting depth recorded.
530
+ return input.map((row) => ({ ...row, depth }))
531
+ }
532
+ const groupKey = grouping[levelIndex]
533
+ if (!groupKey) return input
534
+
535
+ const buckets = new Map<string, Array<Row<TData>>>()
536
+ for (const row of input) {
537
+ const value = row.getCellValueByColumnId(groupKey)
538
+ const key = String(value ?? '')
539
+ const list = buckets.get(key) ?? []
540
+ list.push(row)
541
+ buckets.set(key, list)
542
+ }
543
+
544
+ const groupRows: Array<Row<TData>> = []
545
+ let index = 0
546
+ buckets.forEach((children, key) => {
547
+ const id = `${idPrefix}_${groupKey}_${key}`
548
+ const subRows = buildGroups(children, levelIndex + 1, depth + 1, id)
549
+ const isDeepest = levelIndex + 1 >= grouping.length
550
+ const leafCount = isDeepest
551
+ ? subRows.length
552
+ : subRows.reduce((sum, sub) => sum + (sub.leafCount ?? 0), 0)
553
+
554
+ const resolveColumnValue = (columnId: string): unknown => {
555
+ if (columnId === groupKey) return key
556
+ let resolved: unknown
557
+ let hasResolved = false
558
+ for (const child of children) {
559
+ const childValue = child.getCellValueByColumnId(columnId)
560
+ if (!hasResolved) {
561
+ resolved = childValue
562
+ hasResolved = true
563
+ } else if (childValue !== resolved) {
564
+ return undefined
565
+ }
566
+ }
567
+ return resolved
568
+ }
569
+
570
+ const groupOriginal: Record<string, unknown> = {}
571
+ columns.forEach((column) => {
572
+ const field = column.columnDef.field
573
+ if (!field) return
574
+ const agg = column.columnDef.aggregate
575
+ groupOriginal[field] = agg
576
+ ? applyGroupAggregate(agg, column.id, children)
577
+ : resolveColumnValue(column.id)
578
+ })
579
+
580
+ const groupRow: Row<TData> = {
581
+ id,
582
+ index: index++,
583
+ original: groupOriginal as TData,
584
+ depth,
585
+ subRows,
586
+ leafCount,
587
+ getCanExpand: () => true,
588
+ getIsExpanded: () => Boolean((table.getState().expanded ?? {})[id]),
589
+ toggleExpanded: () => {
590
+ table.setExpanded((prev) => ({ ...prev, [id]: !prev[id] }))
591
+ },
592
+ getIsSelected: () => Boolean((table.getState().rowSelection ?? {})[id]),
593
+ toggleSelected: () => {
594
+ table.setRowSelection((prev) => ({ ...prev, [id]: !prev[id] }))
595
+ },
596
+ getAllCells: () => [],
597
+ // Prefer the precomputed group value (which carries aggregates)
598
+ // and fall back to the shared-value resolver for columns without
599
+ // a field.
600
+ getCellValueByColumnId: (columnId: string) => {
601
+ const col = columns.find((c) => c.id === columnId)
602
+ const field = col?.columnDef.field
603
+ if (field && field in groupOriginal) return groupOriginal[field]
604
+ return resolveColumnValue(columnId)
605
+ },
606
+ }
607
+ groupRows.push(groupRow)
608
+ })
609
+ return groupRows
610
+ }
611
+
612
+ return buildGroups(rows, 0, 0, 'group')
613
+ }
614
+ }
615
+ export function createExpandedRowModel<TData extends RowData>(): RowModelFactory<TData> {
616
+ return ({ table, rows }) => {
617
+ const expanded: ExpandedState = table.getState().expanded ?? {}
618
+ const flattened: Array<Row<TData>> = []
619
+ const visit = (row: Row<TData>) => {
620
+ flattened.push(row)
621
+ if (row.subRows?.length && expanded[row.id]) {
622
+ for (const sub of row.subRows) visit(sub)
623
+ }
624
+ }
625
+ for (const row of rows) visit(row)
626
+ return flattened
627
+ }
628
+ }
629
+ export function createSortedRowModel<TData extends RowData>(
630
+ localSortFns: typeof sortFns = sortFns,
631
+ ): RowModelFactory<TData> {
632
+ return ({ table, rows }) => {
633
+ const sorting = table.getState().sorting ?? []
634
+ if (!sorting.length) return rows
635
+
636
+ const sorted = [...rows].sort((a, b) => {
637
+ for (const clause of sorting) {
638
+ const column = table.getAllColumns().find((col) => col.id === clause.id)
639
+ if (!column) continue
640
+ const editorType = column.columnDef.editorType
641
+ const comparator =
642
+ editorType === 'number'
643
+ ? localSortFns.number
644
+ : editorType === 'date' || editorType === 'datetime'
645
+ ? localSortFns.date
646
+ : localSortFns.auto
647
+ const result = comparator(
648
+ a.getCellValueByColumnId(column.id),
649
+ b.getCellValueByColumnId(column.id),
650
+ )
651
+ if (result !== 0) return clause.desc ? -result : result
652
+ }
653
+ return 0
654
+ })
655
+ return sorted
656
+ }
657
+ }
658
+
659
+ export type SvGridOptions<TFeatures extends TableFeatures, TData extends RowData> = {
660
+ _features: TFeatures
661
+ _rowModels?: {
662
+ coreRowModel?: RowModelFactory<TData>
663
+ filteredRowModel?: RowModelFactory<TData>
664
+ sortedRowModel?: RowModelFactory<TData>
665
+ paginatedRowModel?: RowModelFactory<TData>
666
+ groupedRowModel?: RowModelFactory<TData>
667
+ expandedRowModel?: RowModelFactory<TData>
668
+ }
669
+ columns: Array<ColumnDef<TFeatures, TData>>
670
+ data: ReadonlyArray<TData>
671
+ /**
672
+ * Optional row-id resolver. When set, the value it returns becomes
673
+ * `row.id` (and therefore the selection / expansion / edit key). When
674
+ * omitted, ids fall back to the row's array index as a string. Use a
675
+ * stable id (database PK, UUID, etc.) so selection survives reorders.
676
+ */
677
+ getRowId?: (row: TData, index: number) => string
678
+ state?: Partial<Record<string, any>>
679
+ onSortingChange?: (updater: Updater<SortingState>) => void
680
+ onColumnFiltersChange?: (updater: Updater<ColumnFiltersState>) => void
681
+ onPaginationChange?: (updater: Updater<PaginationState>) => void
682
+ onGroupingChange?: (updater: Updater<GroupingState>) => void
683
+ onExpandedChange?: (updater: Updater<ExpandedState>) => void
684
+ onRowSelectionChange?: (updater: Updater<RowSelectionState>) => void
685
+ onActiveCellChange?: (updater: Updater<ActiveCellState>) => void
686
+ }
687
+
688
+ export type SvGrid<TData extends RowData> = {
689
+ store: Store<Record<string, any>>
690
+ optionsStore: Store<Record<string, any>>
691
+ state: Record<string, any>
692
+ getState: () => Record<string, any>
693
+ setOptions: (updater: Updater<Record<string, any>>) => void
694
+ setColumnFilters: (updater: Updater<ColumnFiltersState>) => void
695
+ setPagination: (updater: Updater<PaginationState>) => void
696
+ setGrouping: (updater: Updater<GroupingState>) => void
697
+ setExpanded: (updater: Updater<ExpandedState>) => void
698
+ setRowSelection: (updater: Updater<RowSelectionState>) => void
699
+ setActiveCell: (updater: Updater<ActiveCellState>) => void
700
+ moveActiveCell: (next: { rowDelta?: number; colDelta?: number }) => void
701
+ getAllColumns: () => Array<Column<TData>>
702
+ getHeaderGroups: () => Array<HeaderGroup<TData>>
703
+ getFooterGroups: () => Array<HeaderGroup<TData>>
704
+ getRowModel: () => RowModel<TData>
705
+ }
706
+
707
+ type InternalGrid<TData extends RowData> = SvGrid<TData> & {
708
+ getAllColumns: () => Array<Column<TData>>
709
+ }
710
+
711
+ export function createSvGridCore<TFeatures extends TableFeatures, TData extends RowData>(
712
+ options: SvGridOptions<TFeatures, TData>,
713
+ ): SvGrid<TData> {
714
+ const internalState: Record<string, any> = {
715
+ sorting: [],
716
+ columnFilters: [],
717
+ pagination: { pageIndex: 0, pageSize: options.data.length || 10 },
718
+ grouping: [],
719
+ expanded: {},
720
+ rowSelection: {},
721
+ activeCell: { rowIndex: 0, colIndex: 0, cellId: null },
722
+ ...(options.state ?? {}),
723
+ }
724
+ const store = createStore(internalState)
725
+ const optionsStore = createStore(options as Record<string, any>)
726
+ let cachedColumnsInput: Array<ColumnDef<TFeatures, TData>> | null = null
727
+ let cachedColumns: Array<Column<TData>> = []
728
+ let cachedHeaderGroups: Array<HeaderGroup<TData>> = []
729
+ let cachedBaseRowsInput: ReadonlyArray<TData> | null = null
730
+ let cachedBaseRowsColumns: Array<Column<TData>> | null = null
731
+ let cachedBaseRows: Array<Row<TData>> = []
732
+ let cachedRowModel: RowModel<TData> | null = null
733
+ let cachedRowModelBaseRows: Array<Row<TData>> | null = null
734
+ let cachedPipeline = options._rowModels
735
+ let cachedSlices: {
736
+ sorting: SortingState | undefined
737
+ columnFilters: ColumnFiltersState | undefined
738
+ pagination: PaginationState | undefined
739
+ grouping: GroupingState | undefined
740
+ expanded: ExpandedState | undefined
741
+ rowSelection: RowSelectionState | undefined
742
+ } | null = null
743
+
744
+ const grid = {
745
+ store,
746
+ optionsStore,
747
+ get state() {
748
+ return store.state
749
+ },
750
+ getState() {
751
+ return store.state
752
+ },
753
+ setOptions(updater: Updater<Record<string, any>>) {
754
+ optionsStore.setState((prev) =>
755
+ typeof updater === 'function' ? (updater as any)(prev) : updater,
756
+ )
757
+ },
758
+ setColumnFilters(updater: Updater<ColumnFiltersState>) {
759
+ store.setState((prev) => ({
760
+ ...prev,
761
+ columnFilters:
762
+ typeof updater === 'function' ? (updater as any)(prev.columnFilters ?? []) : updater,
763
+ }))
764
+ options.onColumnFiltersChange?.(updater)
765
+ },
766
+ setPagination(updater: Updater<PaginationState>) {
767
+ store.setState((prev) => ({
768
+ ...prev,
769
+ pagination:
770
+ typeof updater === 'function'
771
+ ? (updater as any)(prev.pagination ?? { pageIndex: 0, pageSize: 10 })
772
+ : updater,
773
+ }))
774
+ options.onPaginationChange?.(updater)
775
+ },
776
+ setGrouping(updater: Updater<GroupingState>) {
777
+ store.setState((prev) => ({
778
+ ...prev,
779
+ grouping: typeof updater === 'function' ? (updater as any)(prev.grouping ?? []) : updater,
780
+ }))
781
+ options.onGroupingChange?.(updater)
782
+ },
783
+ setExpanded(updater: Updater<ExpandedState>) {
784
+ store.setState((prev) => ({
785
+ ...prev,
786
+ expanded: typeof updater === 'function' ? (updater as any)(prev.expanded ?? {}) : updater,
787
+ }))
788
+ options.onExpandedChange?.(updater)
789
+ },
790
+ setRowSelection(updater: Updater<RowSelectionState>) {
791
+ store.setState((prev) => ({
792
+ ...prev,
793
+ rowSelection:
794
+ typeof updater === 'function' ? (updater as any)(prev.rowSelection ?? {}) : updater,
795
+ }))
796
+ options.onRowSelectionChange?.(updater)
797
+ },
798
+ setActiveCell(updater: Updater<ActiveCellState>) {
799
+ store.setState((prev) => {
800
+ const previous: ActiveCellState = prev.activeCell ?? {
801
+ rowIndex: 0,
802
+ colIndex: 0,
803
+ cellId: null,
804
+ }
805
+ const nextActive =
806
+ typeof updater === 'function' ? updater(previous) : updater
807
+ return {
808
+ ...prev,
809
+ activeCell: nextActive,
810
+ }
811
+ })
812
+ options.onActiveCellChange?.(updater)
813
+ },
814
+ moveActiveCell(next: { rowDelta?: number; colDelta?: number }) {
815
+ const rows = grid.getRowModel().rows
816
+ const columns = grid.getAllColumns()
817
+ const maxRow = Math.max(rows.length - 1, 0)
818
+ const maxCol = Math.max(columns.length - 1, 0)
819
+ const current: ActiveCellState = grid.getState().activeCell ?? {
820
+ rowIndex: 0,
821
+ colIndex: 0,
822
+ cellId: null,
823
+ }
824
+
825
+ const rowIndex = Math.min(
826
+ Math.max(current.rowIndex + (next.rowDelta ?? 0), 0),
827
+ maxRow,
828
+ )
829
+ const colIndex = Math.min(
830
+ Math.max(current.colIndex + (next.colDelta ?? 0), 0),
831
+ maxCol,
832
+ )
833
+ const columnId = columns[colIndex]?.id ?? 'col_0'
834
+ grid.setActiveCell({
835
+ rowIndex,
836
+ colIndex,
837
+ cellId: `${rowIndex}_${columnId}`,
838
+ })
839
+ },
840
+ getAllColumns() {
841
+ // Cache hit: referentially identical columns array.
842
+ if (cachedColumnsInput === options.columns && cachedColumns.length) {
843
+ return cachedColumns
844
+ }
845
+ // Soft cache hit: consumers commonly recreate the columns array
846
+ // inline on every render (e.g. `columns={[...]}`). If the new
847
+ // array has the same length AND each entry has the same `field` /
848
+ // `id` / `header` (the visibility-affecting structure of a
849
+ // column), trust the previous build. Mutable inner fields like
850
+ // `cell` and `editorOptions` are still picked up on the next real
851
+ // render that bumps an actual data dep - they're read at cell-
852
+ // render time, not at this top-level cache.
853
+ if (
854
+ cachedColumnsInput &&
855
+ options.columns.length === cachedColumnsInput.length &&
856
+ cachedColumns.length === options.columns.length &&
857
+ options.columns.every((c, i) => {
858
+ const prev = cachedColumnsInput![i]!
859
+ return (
860
+ c.field === prev.field &&
861
+ c.id === prev.id &&
862
+ c.header === prev.header &&
863
+ c.editorType === prev.editorType
864
+ )
865
+ })
866
+ ) {
867
+ // Update the stored input reference so the strict check hits
868
+ // next time, but reuse the built column model.
869
+ cachedColumnsInput = options.columns
870
+ return cachedColumns
871
+ }
872
+
873
+ cachedColumnsInput = options.columns
874
+ cachedHeaderGroups = []
875
+ const build = (
876
+ defs: Array<ColumnDef<TFeatures, TData>>,
877
+ depth: number,
878
+ parentId?: string,
879
+ ): Array<Column<TData>> => {
880
+ const leaves: Array<Column<TData>> = []
881
+ defs.forEach((columnDef, index) => {
882
+ const id = columnDef.id ?? columnDef.field ?? `${parentId ?? 'col'}_${depth}_${index}`
883
+ if (columnDef.columns?.length) {
884
+ leaves.push(...build(columnDef.columns, depth + 1, id))
885
+ return
886
+ }
887
+ leaves.push({
888
+ id,
889
+ depth,
890
+ parentId,
891
+ columnDef,
892
+ getCanSort: () =>
893
+ Boolean((options._features as any).rowSortingFeature) &&
894
+ columnDef.sortable !== false,
895
+ getCanFilter: () =>
896
+ Boolean((options._features as any).columnFilteringFeature) &&
897
+ columnDef.filterable !== false,
898
+ getIsSorted: () => {
899
+ const entry = store.state.sorting?.find((s: any) => s.id === id)
900
+ if (!entry) return false
901
+ return entry.desc ? 'desc' : 'asc'
902
+ },
903
+ getToggleSortingHandler: () => () => {
904
+ const clauses: SortingState = store.state.sorting ?? []
905
+ const current = clauses.find((s: any) => s.id === id)
906
+ const nextClause: SortingState = !current
907
+ ? [...clauses, { id, desc: false }]
908
+ : current.desc
909
+ ? clauses.filter((s) => s.id !== id)
910
+ : clauses.map((s) => (s.id === id ? { ...s, desc: true } : s))
911
+ store.setState((prev) => ({ ...prev, sorting: nextClause }))
912
+ options.onSortingChange?.(nextClause)
913
+ },
914
+ })
915
+ })
916
+ return leaves
917
+ }
918
+ cachedColumns = build(options.columns, 0)
919
+ return cachedColumns
920
+ },
921
+ getHeaderGroups() {
922
+ if (cachedHeaderGroups.length) return cachedHeaderGroups
923
+ const headers = grid.getAllColumns().map((column) => {
924
+ const header: Header<TData> = {
925
+ id: column.id,
926
+ isPlaceholder: false,
927
+ colSpan: 1,
928
+ column,
929
+ getContext: () => ({ header, column, table: grid }),
930
+ }
931
+ return header
932
+ })
933
+ cachedHeaderGroups = [{ id: 'header_group_0', headers }]
934
+ return cachedHeaderGroups
935
+ },
936
+ getFooterGroups() {
937
+ return grid.getHeaderGroups()
938
+ },
939
+ getRowModel() {
940
+ const columns = grid.getAllColumns()
941
+ if (cachedBaseRowsInput !== options.data || cachedBaseRowsColumns !== columns) {
942
+ cachedBaseRowsInput = options.data
943
+ cachedBaseRowsColumns = columns
944
+ // O(1) column-id → index lookup so getCellValueByColumnId doesn't do
945
+ // a linear `findIndex` on every cell read (was O(rows × cells × cols)).
946
+ const columnIndexById = new Map<string, number>()
947
+ for (let i = 0; i < columns.length; i++) columnIndexById.set(columns[i]!.id, i)
948
+ const columnCount = columns.length
949
+
950
+ cachedBaseRows = new Array(options.data.length)
951
+ const getRowId = options.getRowId
952
+ for (let index = 0; index < options.data.length; index++) {
953
+ const original = options.data[index]!
954
+ const id = getRowId ? getRowId(original, index) : String(index)
955
+ // `values` and `cells` are computed lazily - for a 100k-row grid
956
+ // with only ~20 visible rows we don't want to materialise every
957
+ // row's full value array or cell objects up front.
958
+ let cachedValues: Array<unknown> | null = null
959
+ let cachedCells: Array<Cell<TData>> | null = null
960
+
961
+ function computeValues(): Array<unknown> {
962
+ const values = new Array<unknown>(columnCount)
963
+ for (let i = 0; i < columnCount; i++) {
964
+ const column = columns[i]!
965
+ if (column.columnDef.fieldFn) {
966
+ values[i] = column.columnDef.fieldFn(original)
967
+ } else if (column.columnDef.field) {
968
+ values[i] = (original as any)[column.columnDef.field]
969
+ } else {
970
+ values[i] = undefined
971
+ }
972
+ }
973
+ return values
974
+ }
975
+
976
+ const row: Row<TData> = {
977
+ id,
978
+ index,
979
+ original,
980
+ depth: 0,
981
+ getCanExpand: () => false,
982
+ getIsExpanded: () => Boolean((store.state.expanded ?? {})[id]),
983
+ toggleExpanded: () => {
984
+ grid.setExpanded((prev) => ({ ...prev, [id]: !prev[id] }))
985
+ },
986
+ getIsSelected: () => Boolean((store.state.rowSelection ?? {})[id]),
987
+ toggleSelected: () => {
988
+ grid.setRowSelection((prev) => ({ ...prev, [id]: !prev[id] }))
989
+ },
990
+ getAllCells: () => {
991
+ if (cachedCells) return cachedCells
992
+ const built = new Array<Cell<TData>>(columnCount)
993
+ for (let i = 0; i < columnCount; i++) {
994
+ const column = columns[i]!
995
+ const colIndex = i
996
+ const cell: Cell<TData> = {
997
+ id: `${id}_${column.id}`,
998
+ row,
999
+ column,
1000
+ getValue: () => {
1001
+ if (!cachedValues) cachedValues = computeValues()
1002
+ return cachedValues[colIndex]
1003
+ },
1004
+ getContext: () => ({
1005
+ cell,
1006
+ row,
1007
+ column,
1008
+ table: grid,
1009
+ getValue: () => cell.getValue(),
1010
+ }),
1011
+ }
1012
+ built[i] = cell
1013
+ }
1014
+ cachedCells = built
1015
+ return built
1016
+ },
1017
+ getCellValueByColumnId: (columnId: string) => {
1018
+ const idx = columnIndexById.get(columnId)
1019
+ if (idx === undefined) return undefined
1020
+ if (!cachedValues) cachedValues = computeValues()
1021
+ return cachedValues[idx]
1022
+ },
1023
+ }
1024
+ cachedBaseRows[index] = row
1025
+ }
1026
+ }
1027
+
1028
+ const currentSlices = {
1029
+ sorting: store.state.sorting,
1030
+ columnFilters: store.state.columnFilters,
1031
+ pagination: store.state.pagination,
1032
+ grouping: store.state.grouping,
1033
+ expanded: store.state.expanded,
1034
+ rowSelection: store.state.rowSelection,
1035
+ }
1036
+ if (
1037
+ cachedRowModel &&
1038
+ cachedRowModelBaseRows === cachedBaseRows &&
1039
+ cachedPipeline === options._rowModels &&
1040
+ cachedSlices?.sorting === currentSlices.sorting &&
1041
+ cachedSlices?.columnFilters === currentSlices.columnFilters &&
1042
+ cachedSlices?.pagination === currentSlices.pagination &&
1043
+ cachedSlices?.grouping === currentSlices.grouping &&
1044
+ cachedSlices?.expanded === currentSlices.expanded &&
1045
+ cachedSlices?.rowSelection === currentSlices.rowSelection
1046
+ ) {
1047
+ return cachedRowModel
1048
+ }
1049
+
1050
+ let rows: Array<Row<TData>> = cachedBaseRows
1051
+
1052
+ const pipeline = options._rowModels ?? {}
1053
+ const ordered: Array<RowModelFactory<TData> | undefined> = [
1054
+ pipeline.coreRowModel,
1055
+ pipeline.filteredRowModel,
1056
+ pipeline.sortedRowModel,
1057
+ pipeline.groupedRowModel,
1058
+ pipeline.expandedRowModel,
1059
+ pipeline.paginatedRowModel,
1060
+ ]
1061
+ ordered.forEach((fn) => {
1062
+ if (fn) rows = fn({ table: grid, rows })
1063
+ })
1064
+ cachedPipeline = options._rowModels
1065
+ cachedSlices = currentSlices
1066
+ cachedRowModelBaseRows = cachedBaseRows
1067
+ cachedRowModel = { rows }
1068
+ return cachedRowModel
1069
+ },
1070
+ } as InternalGrid<TData>
1071
+
1072
+ return grid
1073
+ }
1074
+
1075
+ export function isFunction(value: unknown): value is (...args: Array<any>) => any {
1076
+ return typeof value === 'function'
1077
+ }