@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.
- package/dist/FlexRender.svelte +96 -96
- package/dist/SvGrid.controller.svelte.js +17 -6
- package/dist/SvGrid.css +2012 -2012
- package/dist/SvGrid.svelte +2571 -2346
- package/dist/SvGridDropdown.svelte +7 -3
- package/dist/build-api.js +2 -2
- package/dist/cdn/svgrid.js +13504 -0
- package/dist/cdn/svgrid.svelte-external.js +11188 -0
- package/dist/cell-values.d.ts +1 -1
- package/dist/cell-values.js +7 -7
- package/dist/column-types.js +1 -1
- package/dist/core.d.ts +1 -1
- package/dist/core.js +2 -2
- package/dist/summaries.js +4 -4
- package/package.json +12 -2
- package/src/FlexRender.svelte +96 -96
- package/src/SvGrid.controller.svelte.ts +2363 -2352
- package/src/SvGrid.css +2012 -2012
- package/src/SvGrid.svelte +2571 -2346
- package/src/SvGrid.types.ts +537 -537
- package/src/SvGridDropdown.svelte +7 -3
- package/src/a11y.contract.test.ts +49 -49
- package/src/a11y.test.ts +59 -59
- package/src/a11y.ts +59 -59
- package/src/build-api.ts +683 -683
- package/src/cell-formatting.ts +169 -169
- package/src/cell-values.ts +4 -4
- package/src/column-types.ts +1 -1
- package/src/core.performance.test.ts +30 -30
- package/src/core.ts +1077 -1077
- package/src/createGrid.svelte.ts +42 -42
- package/src/createGrid.test.ts +10 -10
- package/src/createGridState.svelte.ts +17 -17
- package/src/editing.ts +669 -669
- package/src/flex-render.ts +3 -3
- package/src/index.ts +208 -208
- package/src/keyboard.test.ts +59 -59
- package/src/keyboard.ts +97 -97
- package/src/merge-objects.ts +48 -48
- package/src/render-component.ts +28 -28
- package/src/spreadsheet.test.ts +489 -489
- package/src/spreadsheet.ts +304 -304
- package/src/static-functions.ts +11 -11
- package/src/subscribe.ts +38 -38
- package/src/summaries.ts +4 -4
- package/src/svgrid-wrapper.types.ts +412 -412
- package/src/svgrid.features.test.ts +157 -157
- package/src/svgrid.wrapper.test.ts +40 -40
- package/src/virtualization/column-virtualizer.test.ts +27 -27
- package/src/virtualization/column-virtualizer.ts +30 -30
- package/src/virtualization/svelte-virtualizer.svelte.ts +26 -26
- package/src/virtualization/types.ts +30 -30
- package/src/virtualization/virtualizer.test.ts +47 -47
- package/src/virtualization/virtualizer.ts +296 -296
- package/dist/SvGrid.helpers.test.d.ts +0 -1
- package/dist/SvGrid.helpers.test.js +0 -298
- package/dist/SvGridChart.test.d.ts +0 -1
- package/dist/SvGridChart.test.js +0 -62
- package/dist/a11y.contract.test.d.ts +0 -1
- package/dist/a11y.contract.test.js +0 -39
- package/dist/a11y.test.d.ts +0 -1
- package/dist/a11y.test.js +0 -44
- package/dist/aligned-grids.test.d.ts +0 -1
- package/dist/aligned-grids.test.js +0 -75
- package/dist/build-api.coverage.test.d.ts +0 -20
- package/dist/build-api.coverage.test.js +0 -505
- package/dist/cell-formatting.test.d.ts +0 -1
- package/dist/cell-formatting.test.js +0 -234
- package/dist/cell-render.test.d.ts +0 -1
- package/dist/cell-render.test.js +0 -338
- package/dist/chart-export.test.d.ts +0 -1
- package/dist/chart-export.test.js +0 -302
- package/dist/chart.coverage.test.d.ts +0 -1
- package/dist/chart.coverage.test.js +0 -748
- package/dist/chart.test.d.ts +0 -1
- package/dist/chart.test.js +0 -289
- package/dist/clipboard.test.d.ts +0 -1
- package/dist/clipboard.test.js +0 -700
- package/dist/collaboration.coverage.test.d.ts +0 -1
- package/dist/collaboration.coverage.test.js +0 -200
- package/dist/collaboration.test.d.ts +0 -1
- package/dist/collaboration.test.js +0 -65
- package/dist/column-groups.test.d.ts +0 -1
- package/dist/column-groups.test.js +0 -56
- package/dist/column-types.test.d.ts +0 -1
- package/dist/column-types.test.js +0 -62
- package/dist/columns.test.d.ts +0 -1
- package/dist/columns.test.js +0 -625
- package/dist/conditional-formatting.test.d.ts +0 -1
- package/dist/conditional-formatting.test.js +0 -87
- package/dist/core.coverage.test.d.ts +0 -1
- package/dist/core.coverage.test.js +0 -186
- package/dist/core.performance.test.d.ts +0 -1
- package/dist/core.performance.test.js +0 -29
- package/dist/createGrid.test.d.ts +0 -1
- package/dist/createGrid.test.js +0 -9
- package/dist/editing.test.d.ts +0 -1
- package/dist/editing.test.js +0 -732
- package/dist/editors/cell-editors.coverage.test.d.ts +0 -1
- package/dist/editors/cell-editors.coverage.test.js +0 -139
- package/dist/editors/cell-editors.test.d.ts +0 -1
- package/dist/editors/cell-editors.test.js +0 -75
- package/dist/facet-buckets.test.d.ts +0 -1
- package/dist/facet-buckets.test.js +0 -296
- package/dist/fill-patterns.test.d.ts +0 -1
- package/dist/fill-patterns.test.js +0 -81
- package/dist/filter-operators.test.d.ts +0 -1
- package/dist/filter-operators.test.js +0 -135
- package/dist/filtering/excel-filters.test.d.ts +0 -1
- package/dist/filtering/excel-filters.test.js +0 -116
- package/dist/filtering/locale-filter.test.d.ts +0 -13
- package/dist/filtering/locale-filter.test.js +0 -189
- package/dist/flex-render.test.d.ts +0 -1
- package/dist/flex-render.test.js +0 -142
- package/dist/group-aggregate.test.d.ts +0 -1
- package/dist/group-aggregate.test.js +0 -32
- package/dist/hyperformula-adapter.test.d.ts +0 -1
- package/dist/hyperformula-adapter.test.js +0 -205
- package/dist/keyboard-handlers.coverage.test.d.ts +0 -1
- package/dist/keyboard-handlers.coverage.test.js +0 -495
- package/dist/keyboard.test.d.ts +0 -1
- package/dist/keyboard.test.js +0 -27
- package/dist/menus.test.d.ts +0 -1
- package/dist/menus.test.js +0 -560
- package/dist/named-views.coverage.test.d.ts +0 -1
- package/dist/named-views.coverage.test.js +0 -180
- package/dist/named-views.test.d.ts +0 -1
- package/dist/named-views.test.js +0 -57
- package/dist/new-features.test.d.ts +0 -1
- package/dist/new-features.test.js +0 -183
- package/dist/render-component.test.d.ts +0 -1
- package/dist/render-component.test.js +0 -41
- package/dist/row-drag.test.d.ts +0 -1
- package/dist/row-drag.test.js +0 -142
- package/dist/row-resize.test.d.ts +0 -1
- package/dist/row-resize.test.js +0 -329
- package/dist/scroll-sync.test.d.ts +0 -1
- package/dist/scroll-sync.test.js +0 -290
- package/dist/selection.multi-range.test.d.ts +0 -1
- package/dist/selection.multi-range.test.js +0 -55
- package/dist/selection.test.d.ts +0 -1
- package/dist/selection.test.js +0 -647
- package/dist/server-data-source.coverage.test.d.ts +0 -1
- package/dist/server-data-source.coverage.test.js +0 -154
- package/dist/server-data-source.test.d.ts +0 -1
- package/dist/server-data-source.test.js +0 -81
- package/dist/sparkline.test.d.ts +0 -1
- package/dist/sparkline.test.js +0 -52
- package/dist/spreadsheet.test.d.ts +0 -1
- package/dist/spreadsheet.test.js +0 -446
- package/dist/subscribe.test.d.ts +0 -1
- package/dist/subscribe.test.js +0 -93
- package/dist/svgrid.api-extensions.test.d.ts +0 -14
- package/dist/svgrid.api-extensions.test.js +0 -334
- package/dist/svgrid.api.test.d.ts +0 -15
- package/dist/svgrid.api.test.js +0 -169
- package/dist/svgrid.behavior.test.d.ts +0 -15
- package/dist/svgrid.behavior.test.js +0 -625
- package/dist/svgrid.column-reorder.test.d.ts +0 -15
- package/dist/svgrid.column-reorder.test.js +0 -224
- package/dist/svgrid.comments-autocomplete.test.d.ts +0 -1
- package/dist/svgrid.comments-autocomplete.test.js +0 -96
- package/dist/svgrid.context-menu.test.d.ts +0 -1
- package/dist/svgrid.context-menu.test.js +0 -102
- package/dist/svgrid.features.test.d.ts +0 -1
- package/dist/svgrid.features.test.js +0 -129
- package/dist/svgrid.interaction.test.d.ts +0 -9
- package/dist/svgrid.interaction.test.js +0 -349
- package/dist/svgrid.locale-filtering.test.d.ts +0 -15
- package/dist/svgrid.locale-filtering.test.js +0 -261
- package/dist/svgrid.new-features.wrapper.test.d.ts +0 -1
- package/dist/svgrid.new-features.wrapper.test.js +0 -194
- package/dist/svgrid.row-pinning.test.d.ts +0 -14
- package/dist/svgrid.row-pinning.test.js +0 -250
- package/dist/svgrid.set-filter.test.d.ts +0 -17
- package/dist/svgrid.set-filter.test.js +0 -268
- package/dist/svgrid.wrapper.test.d.ts +0 -1
- package/dist/svgrid.wrapper.test.js +0 -59
- package/dist/test-setup.d.ts +0 -1
- package/dist/test-setup.js +0 -35
- package/dist/transaction.test.d.ts +0 -1
- package/dist/transaction.test.js +0 -89
- package/dist/virtualization/column-virtualizer.test.d.ts +0 -1
- package/dist/virtualization/column-virtualizer.test.js +0 -25
- package/dist/virtualization/scroll-scaling.test.d.ts +0 -1
- package/dist/virtualization/scroll-scaling.test.js +0 -127
- package/dist/virtualization/virtualizer.test.d.ts +0 -1
- 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
|
-
|
|
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.
|
|
966
|
-
values[i] = column.columnDef.
|
|
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
|
+
}
|