@svgrid/grid 2.6.20 → 2.6.22
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/CHANGELOG.md +62 -0
- package/README.md +22 -0
- package/dist/GridMenus.svelte +17 -12
- package/dist/SvGrid.controller.svelte.d.ts +14 -8
- package/dist/SvGrid.controller.svelte.js +214 -74
- package/dist/SvGrid.css +1 -1
- package/dist/SvGrid.svelte +112 -57
- package/dist/SvGrid.types.d.ts +191 -1
- package/dist/ai.d.ts +28 -0
- package/dist/ai.js +6 -0
- package/dist/cdn/{GridMenus-B0F9iBrG.js → GridMenus-BuoBPqxx.js} +137 -132
- package/dist/cdn/GridMenus-n4llxoOI.js +494 -0
- package/dist/cdn/column-resize-DsfNXMom.js +102 -0
- package/dist/cdn/row-resize-BRcimkUT.js +95 -0
- package/dist/cdn/{src-Cd0tearp.js → src-C9Hihx1W.js} +3497 -3487
- package/dist/cdn/{src-B1TdiyS8.js → src-D1lXwq1l.js} +8258 -8248
- package/dist/cdn/svgrid.js +10 -8
- package/dist/cdn/svgrid.svelte-external.js +10 -8
- package/dist/cdn/validate-_CDJzgIo.js +75 -0
- package/dist/cell-formatting.d.ts +2 -0
- package/dist/cell-formatting.js +2 -0
- package/dist/chart-export.d.ts +1 -0
- package/dist/chart.d.ts +31 -5
- package/dist/chart.js +9 -3
- package/dist/column-groups.js +1 -1
- package/dist/column-resize.d.ts +46 -0
- package/dist/column-resize.js +205 -0
- package/dist/columns.d.ts +0 -3
- package/dist/columns.js +0 -57
- package/dist/core.d.ts +216 -4
- package/dist/core.js +532 -119
- package/dist/createTree.svelte.d.ts +3 -0
- package/dist/createTree.svelte.js +1 -0
- package/dist/datetime/date-core.d.ts +2 -0
- package/dist/datetime/date-restrict.d.ts +1 -0
- package/dist/datetime/timezone.d.ts +1 -0
- package/dist/dock-manager-model.d.ts +3 -0
- package/dist/dock-manager-model.js +1 -0
- package/dist/dock-model.d.ts +6 -0
- package/dist/dock-model.js +3 -0
- package/dist/editor-contract.d.ts +1 -0
- package/dist/filtering/excel-filters.js +28 -0
- package/dist/group-display.d.ts +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +6 -0
- package/dist/list-option.d.ts +1 -0
- package/dist/menus.js +1 -1
- package/dist/positioning.d.ts +2 -0
- package/dist/row-resize.d.ts +11 -0
- package/dist/row-resize.js +7 -1
- package/dist/scheduler-ical.d.ts +1 -0
- package/dist/scheduler-model.d.ts +1 -0
- package/dist/selection.js +9 -0
- package/dist/spreadsheet.d.ts +1 -1
- package/dist/spreadsheet.js +1 -1
- package/dist/summaries.js +22 -0
- package/dist/svgrid-wrapper.types.d.ts +5 -0
- package/dist/toast-store.svelte.d.ts +4 -0
- package/dist/validate.d.ts +50 -0
- package/dist/validate.js +187 -0
- package/package.json +4 -1
- package/src/GridMenus.svelte +17 -12
- package/src/SvGrid.controller.svelte.ts +223 -76
- package/src/SvGrid.css +1 -1
- package/src/SvGrid.svelte +112 -57
- package/src/SvGrid.types.ts +191 -1
- package/src/ai.ts +28 -0
- package/src/cell-formatting.ts +2 -0
- package/src/chart-export.ts +1 -0
- package/src/chart.ts +31 -5
- package/src/column-groups.ts +1 -1
- package/src/column-resize.test.ts +381 -0
- package/src/column-resize.ts +227 -0
- package/src/columns.test.ts +0 -103
- package/src/columns.ts +0 -58
- package/src/core.aggregate.test.ts +134 -0
- package/src/core.filter.test.ts +156 -0
- package/src/core.grouping.test.ts +146 -0
- package/src/core.row-shape.test.ts +119 -0
- package/src/core.rowmodel-cache.test.ts +121 -0
- package/src/core.sort.test.ts +293 -0
- package/src/core.ts +723 -119
- package/src/createTree.svelte.ts +3 -0
- package/src/datetime/date-core.ts +2 -0
- package/src/datetime/date-restrict.ts +1 -0
- package/src/datetime/timezone.ts +1 -0
- package/src/dock-manager-model.ts +3 -0
- package/src/dock-model.ts +6 -0
- package/src/editor-contract.ts +1 -0
- package/src/filtering/excel-filters.ts +30 -0
- package/src/filtering/normalize-fast-path.test.ts +104 -0
- package/src/group-display.ts +1 -1
- package/src/index.ts +12 -1
- package/src/list-option.ts +1 -0
- package/src/menus.ts +1 -1
- package/src/positioning.ts +2 -0
- package/src/resize-props.test.ts +361 -0
- package/src/row-resize.test.ts +31 -0
- package/src/row-resize.ts +21 -3
- package/src/scheduler-ical.ts +1 -0
- package/src/scheduler-model.ts +1 -0
- package/src/selection.ts +9 -0
- package/src/spreadsheet.ts +1 -1
- package/src/summaries.ts +21 -0
- package/src/svgrid-wrapper.types.ts +5 -0
- package/src/svgrid.summaries.test.ts +217 -0
- package/src/toast-store.svelte.ts +4 -0
- package/src/validate.test.ts +207 -0
- package/src/validate.ts +269 -0
- package/dist/cdn/GridMenus-IHK_l7m6.js +0 -489
package/src/core.ts
CHANGED
|
@@ -1,30 +1,79 @@
|
|
|
1
1
|
import type { SparklineConfig } from './sparkline'
|
|
2
2
|
import { resolveColumnId } from './column-id'
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* The constraint every row type satisfies: an object keyed by string. Your own
|
|
6
|
+
* row type (`type Person = { name: string }`) is what flows through the generics
|
|
7
|
+
* below; this is only the lower bound they are declared against.
|
|
8
|
+
*/
|
|
4
9
|
export type RowData = Record<string, unknown>
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A new value, or a function that derives it from the previous one - the shape
|
|
13
|
+
* every `set*` on the grid accepts, so callers can update state without first
|
|
14
|
+
* reading it.
|
|
15
|
+
*
|
|
16
|
+
* api.setSorting([{ id: 'name', desc: false }])
|
|
17
|
+
* api.setSorting((prev) => [...prev, { id: 'age', desc: true }])
|
|
18
|
+
*/
|
|
5
19
|
export type Updater<T> = T | ((prev: T) => T)
|
|
20
|
+
|
|
21
|
+
/** Active sort clauses, outermost first. `desc: false` is ascending. */
|
|
6
22
|
export type SortingState = Array<{ id: string; desc: boolean }>
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* One column's filter: the column `id`, the `value` being matched, and
|
|
26
|
+
* optionally which comparison to use. `fn` defaults to the column's own type -
|
|
27
|
+
* see {@link filterFns} for the available names.
|
|
28
|
+
*/
|
|
7
29
|
export type ColumnFilter = { id: string; value: unknown; fn?: keyof typeof filterFns }
|
|
30
|
+
|
|
31
|
+
/** Every active column filter. A column with no entry here is unfiltered. */
|
|
8
32
|
export type ColumnFiltersState = Array<ColumnFilter>
|
|
33
|
+
|
|
34
|
+
/** Current page position. `pageIndex` is 0-based, so page 1 is index 0. */
|
|
9
35
|
export type PaginationState = { pageIndex: number; pageSize: number }
|
|
36
|
+
|
|
37
|
+
/** Column ids the rows are grouped by, outermost first. */
|
|
10
38
|
export type GroupingState = Array<string>
|
|
39
|
+
|
|
40
|
+
/** Which rows are expanded, keyed by row id. Absent means collapsed. */
|
|
11
41
|
export type ExpandedState = Record<string, boolean>
|
|
42
|
+
|
|
43
|
+
/** Which rows are selected, keyed by row id. Absent means unselected. */
|
|
12
44
|
export type RowSelectionState = Record<string, boolean>
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Where keyboard focus sits. The indices address the *displayed* grid (after
|
|
48
|
+
* sorting, filtering and paging), not the source data.
|
|
49
|
+
*/
|
|
13
50
|
export type ActiveCellState = {
|
|
14
51
|
rowIndex: number
|
|
15
52
|
colIndex: number
|
|
16
53
|
cellId: string | null
|
|
17
54
|
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The set of features a grid has registered, as built by {@link tableFeatures}.
|
|
58
|
+
* Deliberately open: a feature is identified by its key, so the type carries
|
|
59
|
+
* which ones are on without enumerating them.
|
|
60
|
+
*/
|
|
18
61
|
export type TableFeatures = Record<string, unknown>
|
|
19
62
|
|
|
63
|
+
/** A cell's value. Unconstrained - a column can hold anything. */
|
|
20
64
|
export type CellData = unknown
|
|
21
65
|
|
|
66
|
+
/** What a column's `header` render function receives. */
|
|
22
67
|
export type HeaderContext<TData extends RowData> = {
|
|
23
68
|
header: Header<TData>
|
|
24
69
|
column: Column<TData>
|
|
25
70
|
table: SvGrid<TData>
|
|
26
71
|
}
|
|
27
72
|
|
|
73
|
+
/**
|
|
74
|
+
* What a column's `cell` render function receives. `getValue()` applies the
|
|
75
|
+
* column's accessor (`field` or `fieldFn`); `row.original` is the raw object.
|
|
76
|
+
*/
|
|
28
77
|
export type CellContext<TData extends RowData> = {
|
|
29
78
|
cell: Cell<TData>
|
|
30
79
|
row: Row<TData>
|
|
@@ -84,6 +133,11 @@ export type EditorContext<TData extends RowData> = CellContext<TData> & {
|
|
|
84
133
|
cancel: () => void
|
|
85
134
|
}
|
|
86
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Declarative cell formatting, applied through `Intl` - number, currency,
|
|
138
|
+
* percent, date and datetime. Prefer this over a `formatter` function: it is
|
|
139
|
+
* locale-aware, and export and the clipboard reuse the same configuration.
|
|
140
|
+
*/
|
|
87
141
|
export type CellFormatConfig =
|
|
88
142
|
| {
|
|
89
143
|
type: 'number'
|
|
@@ -119,6 +173,10 @@ export type CellFormatConfig =
|
|
|
119
173
|
options?: Intl.DateTimeFormatOptions
|
|
120
174
|
}
|
|
121
175
|
|
|
176
|
+
/**
|
|
177
|
+
* A column's custom display function, for anything {@link CellFormatConfig}
|
|
178
|
+
* cannot express. Returns a string - to render markup, use `cell` instead.
|
|
179
|
+
*/
|
|
122
180
|
export type CellFormatter<TData extends RowData> = (context: {
|
|
123
181
|
value: unknown
|
|
124
182
|
row: Row<TData>
|
|
@@ -126,6 +184,7 @@ export type CellFormatter<TData extends RowData> = (context: {
|
|
|
126
184
|
table: SvGrid<TData>
|
|
127
185
|
}) => string
|
|
128
186
|
|
|
187
|
+
/** A header or cell slot: a literal string, or a function returning renderable content. */
|
|
129
188
|
export type ColumnDefTemplate<TContext> = string | ((context: TContext) => unknown)
|
|
130
189
|
|
|
131
190
|
/**
|
|
@@ -151,27 +210,69 @@ export function applyGroupAggregate<TData extends RowData>(
|
|
|
151
210
|
columnId: string,
|
|
152
211
|
rows: ReadonlyArray<Row<TData>>,
|
|
153
212
|
): unknown {
|
|
154
|
-
|
|
213
|
+
// One pass, no intermediate arrays.
|
|
214
|
+
//
|
|
215
|
+
// This used to build a `raw` array, then a coerced one, then a filtered one -
|
|
216
|
+
// three allocations per aggregated column PER GROUP - before reducing. On a
|
|
217
|
+
// 100k-row grid grouped two levels deep, aggregation was about two thirds of
|
|
218
|
+
// the total grouping cost (213ms with three aggregators against 81ms with
|
|
219
|
+
// none), and each additional aggregated column added roughly 80ms.
|
|
220
|
+
//
|
|
221
|
+
// `count` first: it never needs to look at a value at all.
|
|
222
|
+
if (agg === 'count') return rows.length
|
|
223
|
+
|
|
224
|
+
if (agg === 'first') {
|
|
225
|
+
return rows.length ? rows[0]!.getCellValueByColumnId(columnId) : undefined
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (agg === 'countDistinct') {
|
|
229
|
+
const seen = new Set<string>()
|
|
230
|
+
for (const row of rows) seen.add(String(row.getCellValueByColumnId(columnId) ?? ''))
|
|
231
|
+
return seen.size
|
|
232
|
+
}
|
|
233
|
+
|
|
155
234
|
if (typeof agg === 'function') {
|
|
156
|
-
|
|
235
|
+
// Custom aggregators keep their contract: the finite numbers, then the
|
|
236
|
+
// original row objects. Note `Number(null)` is 0 and therefore finite, so
|
|
237
|
+
// nulls DO reach the callback as zeros - long-standing behaviour.
|
|
238
|
+
const nums: number[] = []
|
|
239
|
+
for (const row of rows) {
|
|
240
|
+
const n = Number(row.getCellValueByColumnId(columnId))
|
|
241
|
+
if (Number.isFinite(n)) nums.push(n)
|
|
242
|
+
}
|
|
157
243
|
return agg(nums, rows.map((r) => r.original))
|
|
158
244
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
245
|
+
|
|
246
|
+
// sum / avg / min / max / extent share one accumulation pass.
|
|
247
|
+
let count = 0
|
|
248
|
+
let sum = 0
|
|
249
|
+
let min = Infinity
|
|
250
|
+
let max = -Infinity
|
|
251
|
+
for (const row of rows) {
|
|
252
|
+
const n = Number(row.getCellValueByColumnId(columnId))
|
|
253
|
+
if (!Number.isFinite(n)) continue
|
|
254
|
+
count++
|
|
255
|
+
// Left-to-right, matching the previous `reduce`, so float rounding is
|
|
256
|
+
// bit-identical rather than merely close.
|
|
257
|
+
sum += n
|
|
258
|
+
// Math.min/max on scalars rather than `<`, which differs on -0, and rather
|
|
259
|
+
// than the old `Math.min(...nums)` - spreading a whole group throws
|
|
260
|
+
// RangeError once the bucket is big enough to exhaust the argument stack.
|
|
261
|
+
min = Math.min(min, n)
|
|
262
|
+
max = Math.max(max, n)
|
|
263
|
+
}
|
|
264
|
+
if (!count) return undefined
|
|
164
265
|
switch (agg) {
|
|
165
266
|
case 'sum':
|
|
166
|
-
return
|
|
267
|
+
return sum
|
|
167
268
|
case 'avg':
|
|
168
|
-
return
|
|
269
|
+
return sum / count
|
|
169
270
|
case 'min':
|
|
170
|
-
return
|
|
271
|
+
return min
|
|
171
272
|
case 'max':
|
|
172
|
-
return
|
|
273
|
+
return max
|
|
173
274
|
case 'extent':
|
|
174
|
-
return `${
|
|
275
|
+
return `${min} – ${max}`
|
|
175
276
|
default:
|
|
176
277
|
return undefined
|
|
177
278
|
}
|
|
@@ -197,7 +298,7 @@ export type ColumnDef<TFeatures extends TableFeatures, TData extends RowData> =
|
|
|
197
298
|
columns?: Array<ColumnDef<TFeatures, TData>>
|
|
198
299
|
/**
|
|
199
300
|
* Declarative cell spanning (merged cells). Return how many COLUMNS this
|
|
200
|
-
* cell spans to the right (1 = no span). Value-driven
|
|
301
|
+
* cell spans to the right (1 = no span). Value-driven. Feed
|
|
201
302
|
* `spansToMerges(rows, columns)` into `spreadsheetLayout` to apply - it uses
|
|
202
303
|
* the same real `colspan`/`rowspan` merge engine (no separate code path).
|
|
203
304
|
*/
|
|
@@ -236,7 +337,7 @@ export type ColumnDef<TFeatures extends TableFeatures, TData extends RowData> =
|
|
|
236
337
|
columnGroupShow?: 'open' | 'closed'
|
|
237
338
|
/**
|
|
238
339
|
* For a GROUP column (one with `columns: [...]`): start the group expanded.
|
|
239
|
-
* Defaults to `false` (collapsed),
|
|
340
|
+
* Defaults to `false` (collapsed), the conventional default - so only the always-on
|
|
240
341
|
* and `columnGroupShow: 'closed'` children show until the user expands it.
|
|
241
342
|
*/
|
|
242
343
|
openByDefault?: boolean
|
|
@@ -281,7 +382,7 @@ export type ColumnDef<TFeatures extends TableFeatures, TData extends RowData> =
|
|
|
281
382
|
*/
|
|
282
383
|
tooltip?: string | ((ctx: CellContext<TData>) => string | null | undefined)
|
|
283
384
|
/**
|
|
284
|
-
* Declarative per-cell validation
|
|
385
|
+
* Declarative per-cell validation. Runs for EVERY
|
|
285
386
|
* rendered cell - including values already present in `data` on load, not
|
|
286
387
|
* just on edit - so bad data is flagged immediately. Invalid cells get the
|
|
287
388
|
* `sv-grid-cell-invalid` class (red highlight) and the returned message as
|
|
@@ -320,7 +421,7 @@ export type ColumnDef<TFeatures extends TableFeatures, TData extends RowData> =
|
|
|
320
421
|
* Transform the committed edit value before it is written to the row.
|
|
321
422
|
* Runs after the built-in per-`editorType` coercion, so `newValue` is
|
|
322
423
|
* already type-parsed; return the final value to store (e.g. round a
|
|
323
|
-
* number, uppercase a code, look up an id).
|
|
424
|
+
* number, uppercase a code, look up an id). A `valueParser` hook.
|
|
324
425
|
*/
|
|
325
426
|
valueParser?: (params: ValueParserParams<TData>) => unknown
|
|
326
427
|
/**
|
|
@@ -378,6 +479,20 @@ export type ColumnDef<TFeatures extends TableFeatures, TData extends RowData> =
|
|
|
378
479
|
* is formatted with this column's `format` and shown in the group header.
|
|
379
480
|
*/
|
|
380
481
|
aggregate?: GroupAggregator<TData>
|
|
482
|
+
/**
|
|
483
|
+
* What this column contributes to the grid's footer summary row (the one
|
|
484
|
+
* turned on with `summary` / `enableRowSummaries`). Takes the same
|
|
485
|
+
* aggregators as {@link aggregate}, and the result is formatted with this
|
|
486
|
+
* column's `format`.
|
|
487
|
+
*
|
|
488
|
+
* Without it the footer falls back to its default: the sum of a numeric
|
|
489
|
+
* column, `Count: N` otherwise. Set `false` to leave the cell blank, which is
|
|
490
|
+
* usually what an actions or checkbox column wants.
|
|
491
|
+
*
|
|
492
|
+
* { field: 'amount', summary: 'avg' }
|
|
493
|
+
* { id: 'actions', summary: false }
|
|
494
|
+
*/
|
|
495
|
+
summary?: GroupAggregator<TData> | false
|
|
381
496
|
/**
|
|
382
497
|
* Render the cell as an in-cell sparkline chart. The cell value should be
|
|
383
498
|
* an array of numbers (or a comma/space separated string). Mutually
|
|
@@ -393,6 +508,21 @@ export type ColumnDef<TFeatures extends TableFeatures, TData extends RowData> =
|
|
|
393
508
|
sparkline?: SparklineConfig
|
|
394
509
|
/** Initial column width in pixels. Falls back to the grid's `columnWidth` prop. */
|
|
395
510
|
width?: number
|
|
511
|
+
/**
|
|
512
|
+
* Whether the user may resize this column. Only consulted when the grid has
|
|
513
|
+
* `columnResize` on - it narrows that, it does not enable anything.
|
|
514
|
+
*
|
|
515
|
+
* `false` removes the column's drag handle entirely, so pointer drag, the
|
|
516
|
+
* keyboard arrows and double-click-to-autosize are all gone with it, and the
|
|
517
|
+
* column menu drops its Autosize item. Use it for the columns whose width is
|
|
518
|
+
* part of the layout rather than a preference: a row-number gutter, a
|
|
519
|
+
* checkbox column, a fixed icon column.
|
|
520
|
+
*
|
|
521
|
+
* Programmatic sizing is unaffected - `api.autosizeColumn()`,
|
|
522
|
+
* `api.setColumnWidth()` and `fitColumns` all still apply, the same way they
|
|
523
|
+
* do when `columnResize` is off. This governs the user affordance only.
|
|
524
|
+
*/
|
|
525
|
+
resizable?: boolean
|
|
396
526
|
/**
|
|
397
527
|
* Initial visibility. Set `false` to start the column hidden while still
|
|
398
528
|
* listing it in the Choose Columns UI for the user to re-enable. Applied
|
|
@@ -445,6 +575,12 @@ export type GridColumnDef<TData extends RowData = RowData> = ColumnDef<TableFeat
|
|
|
445
575
|
/** An array of {@link GridColumnDef} - what you pass to `<SvGrid columns={...}>`. */
|
|
446
576
|
export type GridColumns<TData extends RowData = RowData> = Array<GridColumnDef<TData>>
|
|
447
577
|
|
|
578
|
+
/**
|
|
579
|
+
* A resolved column: your {@link ColumnDef} plus everything the grid computed
|
|
580
|
+
* from it - its id, its depth under any group header, and the sort handlers a
|
|
581
|
+
* header needs. This is what you receive in render contexts; the `ColumnDef`
|
|
582
|
+
* is what you wrote.
|
|
583
|
+
*/
|
|
448
584
|
export type Column<TData extends RowData> = {
|
|
449
585
|
id: string
|
|
450
586
|
columnDef: ColumnDef<any, TData>
|
|
@@ -456,6 +592,11 @@ export type Column<TData extends RowData> = {
|
|
|
456
592
|
getToggleSortingHandler: () => () => void
|
|
457
593
|
}
|
|
458
594
|
|
|
595
|
+
/**
|
|
596
|
+
* One header cell. `colSpan` is how many leaf columns it covers, and
|
|
597
|
+
* `isPlaceholder` marks the empty cells that pad a group-header row so the
|
|
598
|
+
* levels line up.
|
|
599
|
+
*/
|
|
459
600
|
export type Header<TData extends RowData> = {
|
|
460
601
|
id: string
|
|
461
602
|
isPlaceholder: boolean
|
|
@@ -464,11 +605,13 @@ export type Header<TData extends RowData> = {
|
|
|
464
605
|
getContext: () => HeaderContext<TData>
|
|
465
606
|
}
|
|
466
607
|
|
|
608
|
+
/** One row of header cells. A grid with grouped columns has several, outermost first. */
|
|
467
609
|
export type HeaderGroup<TData extends RowData> = {
|
|
468
610
|
id: string
|
|
469
611
|
headers: Array<Header<TData>>
|
|
470
612
|
}
|
|
471
613
|
|
|
614
|
+
/** One cell: the intersection of a {@link Row} and a {@link Column}. */
|
|
472
615
|
export type Cell<TData extends RowData> = {
|
|
473
616
|
id: string
|
|
474
617
|
row: Row<TData>
|
|
@@ -477,6 +620,13 @@ export type Cell<TData extends RowData> = {
|
|
|
477
620
|
getContext: () => CellContext<TData>
|
|
478
621
|
}
|
|
479
622
|
|
|
623
|
+
/**
|
|
624
|
+
* A row in the display model. `original` is your untouched data object;
|
|
625
|
+
* everything else is grid-computed. `index` is the position in the displayed
|
|
626
|
+
* set, so it shifts as sorting and filtering change - key on `id`, not index.
|
|
627
|
+
*
|
|
628
|
+
* Group rows and tree parents carry `subRows`; a plain data row does not.
|
|
629
|
+
*/
|
|
480
630
|
export type Row<TData extends RowData> = {
|
|
481
631
|
id: string
|
|
482
632
|
index: number
|
|
@@ -494,10 +644,19 @@ export type Row<TData extends RowData> = {
|
|
|
494
644
|
getCellValueByColumnId: (columnId: string) => unknown
|
|
495
645
|
}
|
|
496
646
|
|
|
647
|
+
/** The output of the row pipeline: the rows to display, in order. */
|
|
497
648
|
export type RowModel<TData extends RowData> = {
|
|
498
649
|
rows: Array<Row<TData>>
|
|
499
650
|
}
|
|
500
651
|
|
|
652
|
+
/**
|
|
653
|
+
* The minimal reactive store behind the headless core - read `state`, write
|
|
654
|
+
* through `setState`, and `subscribe` for changes. Deliberately framework
|
|
655
|
+
* free, which is what lets the core run under plain Node.
|
|
656
|
+
*
|
|
657
|
+
* In Svelte you rarely touch this: `subscribeGrid` wraps it with fine-grained
|
|
658
|
+
* selectors so a component only re-runs for the slice it read.
|
|
659
|
+
*/
|
|
501
660
|
export type Store<T> = {
|
|
502
661
|
readonly state: T
|
|
503
662
|
setState: (updater: (prev: T) => T) => void
|
|
@@ -522,17 +681,51 @@ function createStore<T>(initial: T): Store<T> {
|
|
|
522
681
|
}
|
|
523
682
|
}
|
|
524
683
|
|
|
684
|
+
/**
|
|
685
|
+
* Click-to-sort. Injected by the `sortable` shortcut.
|
|
686
|
+
*
|
|
687
|
+
* This and the five features below are opaque markers: pass the ones you want
|
|
688
|
+
* to {@link tableFeatures} and the grid wires up the matching row model. With
|
|
689
|
+
* `<SvGrid>` you rarely name them - the boolean shortcuts (`sortable`,
|
|
690
|
+
* `filterable`, `pageable`, `groupable`) inject them for you. Reach for them
|
|
691
|
+
* directly when driving the headless core, or when you want a feature on
|
|
692
|
+
* without its UI.
|
|
693
|
+
*
|
|
694
|
+
* The names match TanStack Table v9, so a features object written for it works
|
|
695
|
+
* here unchanged.
|
|
696
|
+
*/
|
|
525
697
|
export const rowSortingFeature = { key: 'rowSortingFeature' }
|
|
698
|
+
/** Per-column filtering. Injected by the `filterable` shortcut. */
|
|
526
699
|
export const columnFilteringFeature = { key: 'columnFilteringFeature' }
|
|
700
|
+
/** Paging of the row model. Injected by the `pageable` shortcut. */
|
|
527
701
|
export const rowPaginationFeature = { key: 'rowPaginationFeature' }
|
|
702
|
+
/** Row grouping with aggregation. Injected by the `groupable` shortcut. */
|
|
528
703
|
export const columnGroupingFeature = { key: 'columnGroupingFeature' }
|
|
704
|
+
/** Row selection state (the checkbox column reads it). */
|
|
529
705
|
export const rowSelectionFeature = { key: 'rowSelectionFeature' }
|
|
706
|
+
/** Expand / collapse, for tree rows and master-detail. */
|
|
530
707
|
export const rowExpandingFeature = { key: 'rowExpandingFeature' }
|
|
531
708
|
|
|
709
|
+
/**
|
|
710
|
+
* Declare which features a grid uses. Identity at runtime - its whole job is to
|
|
711
|
+
* capture the exact set in the type, so `ColumnDef<typeof features, Row>` knows
|
|
712
|
+
* what is registered and anything you did not register is tree-shaken out.
|
|
713
|
+
*
|
|
714
|
+
* ```ts
|
|
715
|
+
* const features = tableFeatures({ rowSortingFeature, columnFilteringFeature })
|
|
716
|
+
* ```
|
|
717
|
+
*
|
|
718
|
+
* Same call signature as TanStack Table v9, so a features object written for it
|
|
719
|
+
* transfers unchanged.
|
|
720
|
+
*/
|
|
532
721
|
export function tableFeatures<T extends TableFeatures>(features: T): T {
|
|
533
722
|
return features
|
|
534
723
|
}
|
|
535
724
|
|
|
725
|
+
/**
|
|
726
|
+
* Built-in comparators, chosen per column by its data type. `auto` compares as
|
|
727
|
+
* text; set a column's type or supply your own comparator to override.
|
|
728
|
+
*/
|
|
536
729
|
export const sortFns = {
|
|
537
730
|
auto: (a: unknown, b: unknown) => String(a).localeCompare(String(b)),
|
|
538
731
|
number: (a: unknown, b: unknown) => Number(a ?? 0) - Number(b ?? 0),
|
|
@@ -543,36 +736,205 @@ export const sortFns = {
|
|
|
543
736
|
},
|
|
544
737
|
}
|
|
545
738
|
|
|
739
|
+
/**
|
|
740
|
+
* Built-in match functions, named by {@link ColumnFilter}'s `fn`.
|
|
741
|
+
* `includesString` is case-insensitive substring; `equals` is strict identity.
|
|
742
|
+
*/
|
|
546
743
|
export const filterFns = {
|
|
547
744
|
includesString: (value: unknown, query: string) =>
|
|
548
745
|
String(value).toLowerCase().includes(query.toLowerCase()),
|
|
549
746
|
equals: (value: unknown, query: unknown) => value === query,
|
|
550
747
|
}
|
|
551
748
|
|
|
749
|
+
/**
|
|
750
|
+
* Everything a base row needs that is the same for every row in the table.
|
|
751
|
+
*
|
|
752
|
+
* One object per table, referenced by every row, instead of one closure scope
|
|
753
|
+
* per row. See {@link BASE_ROW_METHODS}.
|
|
754
|
+
*/
|
|
755
|
+
type BaseRowCtx<TData extends RowData> = {
|
|
756
|
+
grid: SvGrid<TData>
|
|
757
|
+
store: { state: Record<string, any> }
|
|
758
|
+
columns: Array<Column<TData>>
|
|
759
|
+
columnCount: number
|
|
760
|
+
columnIndexById: Map<string, number>
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* Keys for a base row's private fields.
|
|
765
|
+
*
|
|
766
|
+
* Symbols, not string keys, and that is load-bearing. A row's shared methods
|
|
767
|
+
* need a pointer back to the table, but `_ctx` as a normal property made every
|
|
768
|
+
* row serialise the entire grid: `JSON.stringify(oneRow)` grew with the dataset
|
|
769
|
+
* (981 chars at 3 rows, 67,719 at 3,000) because `options.data` is reachable
|
|
770
|
+
* through it, so stringifying a row model was quadratic. Rows used to serialise
|
|
771
|
+
* to a small constant and must again.
|
|
772
|
+
*
|
|
773
|
+
* A symbol key is invisible to `JSON.stringify`, `Object.keys` and `for...in`,
|
|
774
|
+
* yet IS copied by object spread - which matters because several row models
|
|
775
|
+
* legitimately do `{ ...row, depth }` and the clone needs these to work.
|
|
776
|
+
* Non-enumerable string keys would have hidden them from JSON but also from the
|
|
777
|
+
* spread, silently breaking every cloned row.
|
|
778
|
+
*/
|
|
779
|
+
const ROW_CTX = Symbol('svgrid.row.ctx')
|
|
780
|
+
const ROW_VALUES = Symbol('svgrid.row.values')
|
|
781
|
+
const ROW_CELLS = Symbol('svgrid.row.cells')
|
|
782
|
+
|
|
783
|
+
/** A base row's private fields, on top of the public {@link Row} surface. */
|
|
784
|
+
type BaseRowState<TData extends RowData> = Row<TData> & {
|
|
785
|
+
[ROW_CTX]: BaseRowCtx<TData>
|
|
786
|
+
[ROW_VALUES]: Array<unknown> | null
|
|
787
|
+
[ROW_CELLS]: Array<Cell<TData>> | null
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/**
|
|
791
|
+
* The methods every base row carries, defined ONCE and assigned by reference.
|
|
792
|
+
*
|
|
793
|
+
* Rows used to be built as object literals whose methods were closures, which
|
|
794
|
+
* meant a 100k-row grid allocated 700k closures and a closure scope per row
|
|
795
|
+
* before painting anything. Measured at 100k x 9: 13.8 ms and 56.5 MB to build,
|
|
796
|
+
* against 2.0 ms and 14.5 MB for this shape - the single largest cost in
|
|
797
|
+
* mounting a large grid.
|
|
798
|
+
*
|
|
799
|
+
* They read their row through `this` rather than a captured variable, which is
|
|
800
|
+
* why they can be shared. Note they are assigned as OWN properties rather than
|
|
801
|
+
* put on a prototype: `Row` is public, several row models legitimately do
|
|
802
|
+
* `{ ...row, depth }`, and a spread copies own properties but not a prototype.
|
|
803
|
+
* A class here would silently strip every method off a cloned row.
|
|
804
|
+
*/
|
|
805
|
+
const BASE_ROW_METHODS = {
|
|
806
|
+
getCanExpand(this: BaseRowState<RowData>) {
|
|
807
|
+
return false
|
|
808
|
+
},
|
|
809
|
+
getIsExpanded(this: BaseRowState<RowData>) {
|
|
810
|
+
return Boolean((this[ROW_CTX].store.state.expanded ?? {})[this.id])
|
|
811
|
+
},
|
|
812
|
+
toggleExpanded(this: BaseRowState<RowData>) {
|
|
813
|
+
const id = this.id
|
|
814
|
+
this[ROW_CTX].grid.setExpanded((prev) => ({ ...prev, [id]: !prev[id] }))
|
|
815
|
+
},
|
|
816
|
+
getIsSelected(this: BaseRowState<RowData>) {
|
|
817
|
+
return Boolean((this[ROW_CTX].store.state.rowSelection ?? {})[this.id])
|
|
818
|
+
},
|
|
819
|
+
toggleSelected(this: BaseRowState<RowData>) {
|
|
820
|
+
const id = this.id
|
|
821
|
+
this[ROW_CTX].grid.setRowSelection((prev) => ({ ...prev, [id]: !prev[id] }))
|
|
822
|
+
},
|
|
823
|
+
getAllCells(this: BaseRowState<RowData>) {
|
|
824
|
+
return this[ROW_CELLS] ?? buildBaseRowCells(this)
|
|
825
|
+
},
|
|
826
|
+
getCellValueByColumnId(this: BaseRowState<RowData>, columnId: string) {
|
|
827
|
+
const idx = this[ROW_CTX].columnIndexById.get(columnId)
|
|
828
|
+
if (idx === undefined) return undefined
|
|
829
|
+
if (!this[ROW_VALUES]) this[ROW_VALUES] = baseRowValues(this)
|
|
830
|
+
return this[ROW_VALUES][idx]
|
|
831
|
+
},
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/**
|
|
835
|
+
* Materialise one row's `Cell[]`, memoised on the row.
|
|
836
|
+
*
|
|
837
|
+
* A free function taking the row rather than a method using `this`, because the
|
|
838
|
+
* cell closures need a stable reference to it and aliasing `this` inside a
|
|
839
|
+
* method is exactly the pattern that produces `self`/`that` bugs.
|
|
840
|
+
*/
|
|
841
|
+
function buildBaseRowCells<TData extends RowData>(row: BaseRowState<TData>): Array<Cell<TData>> {
|
|
842
|
+
const { columns, columnCount, grid } = row[ROW_CTX]
|
|
843
|
+
const built = new Array<Cell<TData>>(columnCount)
|
|
844
|
+
for (let i = 0; i < columnCount; i++) {
|
|
845
|
+
const column = columns[i]!
|
|
846
|
+
const colIndex = i
|
|
847
|
+
const cell: Cell<TData> = {
|
|
848
|
+
id: `${row.id}_${column.id}`,
|
|
849
|
+
row,
|
|
850
|
+
column,
|
|
851
|
+
getValue: () => {
|
|
852
|
+
if (!row[ROW_VALUES]) row[ROW_VALUES] = baseRowValues(row)
|
|
853
|
+
return row[ROW_VALUES][colIndex]
|
|
854
|
+
},
|
|
855
|
+
getContext: () => ({
|
|
856
|
+
cell,
|
|
857
|
+
row,
|
|
858
|
+
column,
|
|
859
|
+
table: grid,
|
|
860
|
+
getValue: () => cell.getValue(),
|
|
861
|
+
}),
|
|
862
|
+
}
|
|
863
|
+
built[i] = cell
|
|
864
|
+
}
|
|
865
|
+
row[ROW_CELLS] = built
|
|
866
|
+
return built
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/**
|
|
870
|
+
* Resolve every column's value for one row. Kept lazy: a 100k-row grid showing
|
|
871
|
+
* twenty rows must not materialise 900k values to paint.
|
|
872
|
+
*/
|
|
873
|
+
function baseRowValues<TData extends RowData>(row: BaseRowState<TData>): Array<unknown> {
|
|
874
|
+
const { columns, columnCount } = row[ROW_CTX]
|
|
875
|
+
const original = row.original as Record<string, unknown>
|
|
876
|
+
const values = new Array<unknown>(columnCount)
|
|
877
|
+
for (let i = 0; i < columnCount; i++) {
|
|
878
|
+
const def = columns[i]!.columnDef
|
|
879
|
+
if (def.fieldFn) values[i] = def.fieldFn(original as TData)
|
|
880
|
+
else if (def.field) values[i] = original[def.field]
|
|
881
|
+
else values[i] = undefined
|
|
882
|
+
}
|
|
883
|
+
return values
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* One stage of the row pipeline: takes the rows produced so far and returns the
|
|
888
|
+
* next set. Stages compose in the order given to `_rowModels`, so filtering
|
|
889
|
+
* before sorting sorts only what survived the filter.
|
|
890
|
+
*/
|
|
552
891
|
export type RowModelFactory<TData extends RowData> = (args: {
|
|
553
892
|
table: SvGrid<TData>
|
|
554
893
|
rows: Array<Row<TData>>
|
|
555
894
|
}) => Array<Row<TData>>
|
|
556
895
|
|
|
896
|
+
/**
|
|
897
|
+
* The identity stage that starts every pipeline. Always required, even when no
|
|
898
|
+
* other stage is: it is what turns your data into rows.
|
|
899
|
+
*/
|
|
557
900
|
export function createCoreRowModel<TData extends RowData>(): RowModelFactory<TData> {
|
|
558
901
|
return ({ rows }) => rows
|
|
559
902
|
}
|
|
903
|
+
/**
|
|
904
|
+
* Drops rows that fail the active {@link ColumnFiltersState}. Pairs with
|
|
905
|
+
* `columnFilteringFeature`; without it there are no filters to apply.
|
|
906
|
+
*/
|
|
560
907
|
export function createFilteredRowModel<TData extends RowData>(): RowModelFactory<TData> {
|
|
561
908
|
return ({ table, rows }) => {
|
|
562
909
|
const filters: ColumnFiltersState = table.getState().columnFilters ?? []
|
|
563
910
|
if (!filters.length) return rows
|
|
911
|
+
|
|
912
|
+
// Resolve each filter's match function once, outside the row loop.
|
|
913
|
+
const compiled = filters.map((filter) => ({
|
|
914
|
+
id: filter.id,
|
|
915
|
+
value: filter.value,
|
|
916
|
+
fn: filter.fn ? filterFns[filter.fn] : filterFns.includesString,
|
|
917
|
+
}))
|
|
918
|
+
|
|
564
919
|
return rows.filter((row) => {
|
|
565
|
-
|
|
566
|
-
const
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
920
|
+
for (let i = 0; i < compiled.length; i++) {
|
|
921
|
+
const filter = compiled[i]!
|
|
922
|
+
// `getCellValueByColumnId` rather than `getAllCells().find(...)`.
|
|
923
|
+
// Both read the same lazily-built `cachedValues` array, but the latter
|
|
924
|
+
// also builds and caches the row's whole `Cell[]` - one object per
|
|
925
|
+
// column - purely to reach one field. On a 100k-row grid that is
|
|
926
|
+
// 100,000 cell arrays the filter never looks at again, and it defeats
|
|
927
|
+
// the laziness the row factory exists to provide.
|
|
928
|
+
if (!filter.fn(row.getCellValueByColumnId(filter.id), filter.value as any)) return false
|
|
929
|
+
}
|
|
930
|
+
return true
|
|
573
931
|
})
|
|
574
932
|
}
|
|
575
933
|
}
|
|
934
|
+
/**
|
|
935
|
+
* Narrows the rows to the current page. Put it LAST: anything after it would
|
|
936
|
+
* only ever see one page of data.
|
|
937
|
+
*/
|
|
576
938
|
export function createPaginatedRowModel<TData extends RowData>(): RowModelFactory<TData> {
|
|
577
939
|
return ({ table, rows }) => {
|
|
578
940
|
const pagination = table.getState().pagination ?? { pageIndex: 0, pageSize: rows.length || 10 }
|
|
@@ -580,6 +942,10 @@ export function createPaginatedRowModel<TData extends RowData>(): RowModelFactor
|
|
|
580
942
|
return rows.slice(start, start + pagination.pageSize)
|
|
581
943
|
}
|
|
582
944
|
}
|
|
945
|
+
/**
|
|
946
|
+
* Buckets rows by the active {@link GroupingState} and inserts a group row
|
|
947
|
+
* ahead of each bucket, carrying that bucket's aggregates.
|
|
948
|
+
*/
|
|
583
949
|
export function createGroupedRowModel<TData extends RowData>(): RowModelFactory<TData> {
|
|
584
950
|
return ({ table, rows }) => {
|
|
585
951
|
const grouping: GroupingState = table.getState().grouping ?? []
|
|
@@ -595,6 +961,9 @@ export function createGroupedRowModel<TData extends RowData>(): RowModelFactory<
|
|
|
595
961
|
levelIndex: number,
|
|
596
962
|
depth: number,
|
|
597
963
|
idPrefix: string,
|
|
964
|
+
/** Grouping columns already fixed by an ancestor bucket, and their raw
|
|
965
|
+
* values - `undefined` where that bucket mixed several. */
|
|
966
|
+
fixedValues: ReadonlyMap<string, unknown>,
|
|
598
967
|
): Array<Row<TData>> {
|
|
599
968
|
if (levelIndex >= grouping.length) {
|
|
600
969
|
// Leaves: actual data rows, with their nesting depth recorded.
|
|
@@ -603,27 +972,60 @@ export function createGroupedRowModel<TData extends RowData>(): RowModelFactory<
|
|
|
603
972
|
const groupKey = grouping[levelIndex]
|
|
604
973
|
if (!groupKey) return input
|
|
605
974
|
|
|
606
|
-
|
|
975
|
+
// Buckets carry the RAW grouping value alongside the rows, plus whether
|
|
976
|
+
// the bucket saw more than one distinct raw value. Both are needed to let
|
|
977
|
+
// deeper levels skip re-scanning this column: buckets are keyed by
|
|
978
|
+
// `String(value ?? '')`, so `null`, `undefined` and `''` collapse into one
|
|
979
|
+
// bucket, and a scan of such a bucket would report disagreement. Tracking
|
|
980
|
+
// it here costs one comparison per row and keeps the shortcut honest.
|
|
981
|
+
type Bucket = { rows: Array<Row<TData>>; raw: unknown; mixed: boolean }
|
|
982
|
+
const buckets = new Map<string, Bucket>()
|
|
607
983
|
for (const row of input) {
|
|
608
984
|
const value = row.getCellValueByColumnId(groupKey)
|
|
609
985
|
const key = String(value ?? '')
|
|
610
|
-
const
|
|
611
|
-
|
|
612
|
-
|
|
986
|
+
const bucket = buckets.get(key)
|
|
987
|
+
if (bucket) {
|
|
988
|
+
bucket.rows.push(row)
|
|
989
|
+
if (!bucket.mixed && bucket.raw !== value) bucket.mixed = true
|
|
990
|
+
} else {
|
|
991
|
+
buckets.set(key, { rows: [row], raw: value, mixed: false })
|
|
992
|
+
}
|
|
613
993
|
}
|
|
614
994
|
|
|
615
995
|
const groupRows: Array<Row<TData>> = []
|
|
616
996
|
let index = 0
|
|
617
|
-
buckets.forEach((
|
|
997
|
+
buckets.forEach((bucket, key) => {
|
|
998
|
+
const children = bucket.rows
|
|
618
999
|
const id = `${idPrefix}_${groupKey}_${key}`
|
|
619
|
-
|
|
1000
|
+
// Record this column as fixed for deeper levels ONLY when the bucket is
|
|
1001
|
+
// homogeneous. If all rows here share a raw value, so does every subset
|
|
1002
|
+
// of them, which is what makes the shortcut sound.
|
|
1003
|
+
//
|
|
1004
|
+
// A MIXED bucket must not be recorded at all - not even as "undefined".
|
|
1005
|
+
// `null`, `undefined` and `''` share a bucket key, so a mixed bucket can
|
|
1006
|
+
// still split into homogeneous children one level down, and those
|
|
1007
|
+
// children have a real shared value that a scan would find. Marking the
|
|
1008
|
+
// column resolved here would hand them the parent's disagreement.
|
|
1009
|
+
const nextFixed = bucket.mixed ? fixedValues : new Map(fixedValues).set(groupKey, bucket.raw)
|
|
1010
|
+
const subRows = buildGroups(children, levelIndex + 1, depth + 1, id, nextFixed)
|
|
620
1011
|
const isDeepest = levelIndex + 1 >= grouping.length
|
|
621
1012
|
const leafCount = isDeepest
|
|
622
1013
|
? subRows.length
|
|
623
1014
|
: subRows.reduce((sum, sub) => sum + (sub.leafCount ?? 0), 0)
|
|
624
1015
|
|
|
1016
|
+
// Every grouping column ABOVE this level is already resolved: bucketing
|
|
1017
|
+
// by it is what made it constant, so scanning the children to rediscover
|
|
1018
|
+
// it is pure waste. Only the current level's key short-circuited before,
|
|
1019
|
+
// so a second-level group walked all of its children to re-derive the
|
|
1020
|
+
// first level's value - about 100,000 reads on the 100k x 9 two-level
|
|
1021
|
+
// case, for an answer already in hand.
|
|
1022
|
+
//
|
|
1023
|
+
// The current level still returns the stringified bucket key rather than
|
|
1024
|
+
// the raw value, because that is what it has always returned and the
|
|
1025
|
+
// group row's display depends on it.
|
|
625
1026
|
const resolveColumnValue = (columnId: string): unknown => {
|
|
626
1027
|
if (columnId === groupKey) return key
|
|
1028
|
+
if (fixedValues.has(columnId)) return fixedValues.get(columnId)
|
|
627
1029
|
let resolved: unknown
|
|
628
1030
|
let hasResolved = false
|
|
629
1031
|
for (const child of children) {
|
|
@@ -680,9 +1082,17 @@ export function createGroupedRowModel<TData extends RowData>(): RowModelFactory<
|
|
|
680
1082
|
return groupRows
|
|
681
1083
|
}
|
|
682
1084
|
|
|
683
|
-
return buildGroups(rows, 0, 0, 'group')
|
|
1085
|
+
return buildGroups(rows, 0, 0, 'group', new Map())
|
|
684
1086
|
}
|
|
685
1087
|
}
|
|
1088
|
+
/**
|
|
1089
|
+
* How to read a hierarchy out of FLAT rows: each row names its parent, and the
|
|
1090
|
+
* grid reconstructs the tree. Rows whose parent id matches nothing become roots
|
|
1091
|
+
* rather than disappearing.
|
|
1092
|
+
*
|
|
1093
|
+
* For nested source data (`children: [...]`), flatten it first with
|
|
1094
|
+
* {@link flattenTreeData}.
|
|
1095
|
+
*/
|
|
686
1096
|
export type TreeRowModelOptions = {
|
|
687
1097
|
/** Field holding each row's parent id. Rows with no parent are roots. */
|
|
688
1098
|
parentField: string
|
|
@@ -761,6 +1171,11 @@ export function createTreeRowModel<TData extends RowData>(
|
|
|
761
1171
|
}
|
|
762
1172
|
}
|
|
763
1173
|
|
|
1174
|
+
/**
|
|
1175
|
+
* How to flatten NESTED source data into the parent-id shape tree rows need.
|
|
1176
|
+
* `parentField` is written onto each row, so point `treeData.parentField` at
|
|
1177
|
+
* the same name afterwards.
|
|
1178
|
+
*/
|
|
764
1179
|
export type FlattenTreeOptions = {
|
|
765
1180
|
/** Field holding an array of child objects. */
|
|
766
1181
|
childrenField: string
|
|
@@ -796,6 +1211,10 @@ export function flattenTreeData<T extends RowData>(
|
|
|
796
1211
|
return out
|
|
797
1212
|
}
|
|
798
1213
|
|
|
1214
|
+
/**
|
|
1215
|
+
* Hides the descendants of collapsed rows. Needed for grouping, tree data and
|
|
1216
|
+
* master-detail alike - all three are the same expand/collapse mechanism.
|
|
1217
|
+
*/
|
|
799
1218
|
export function createExpandedRowModel<TData extends RowData>(): RowModelFactory<TData> {
|
|
800
1219
|
return ({ table, rows }) => {
|
|
801
1220
|
const expanded: ExpandedState = table.getState().expanded ?? {}
|
|
@@ -810,36 +1229,227 @@ export function createExpandedRowModel<TData extends RowData>(): RowModelFactory
|
|
|
810
1229
|
return flattened
|
|
811
1230
|
}
|
|
812
1231
|
}
|
|
1232
|
+
/**
|
|
1233
|
+
* Orders rows by the active {@link SortingState}. Pass your own comparators to
|
|
1234
|
+
* override the built-in {@link sortFns} - useful for locale-aware or
|
|
1235
|
+
* domain-specific ordering.
|
|
1236
|
+
*/
|
|
813
1237
|
export function createSortedRowModel<TData extends RowData>(
|
|
814
1238
|
localSortFns: typeof sortFns = sortFns,
|
|
815
1239
|
): RowModelFactory<TData> {
|
|
816
|
-
return ({ table, rows })
|
|
1240
|
+
return function sortedRowModelStage({ table, rows }) {
|
|
817
1241
|
const sorting = table.getState().sorting ?? []
|
|
818
1242
|
if (!sorting.length) return rows
|
|
819
1243
|
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
1244
|
+
// Resolve every clause ONCE, before sorting.
|
|
1245
|
+
//
|
|
1246
|
+
// This used to live inside the comparator, so `getAllColumns().find(...)`
|
|
1247
|
+
// ran per comparison per clause: a single-clause sort of 100k rows made
|
|
1248
|
+
// 1,528,947 array scans, and a three-clause sort made 3,933,751 (measured;
|
|
1249
|
+
// `pnpm bench --case=sort-1col`). The comparator is called O(n log n)
|
|
1250
|
+
// times, so anything inside it that is not O(1) sets the cost of the sort.
|
|
1251
|
+
const allColumns = table.getAllColumns()
|
|
1252
|
+
const clauses: Array<{
|
|
1253
|
+
keys: Array<any>
|
|
1254
|
+
desc: boolean
|
|
1255
|
+
compare: (a: any, b: any) => number
|
|
1256
|
+
}> = []
|
|
1257
|
+
|
|
1258
|
+
for (const clause of sorting) {
|
|
1259
|
+
const column = allColumns.find((col) => col.id === clause.id)
|
|
1260
|
+
if (!column) continue
|
|
1261
|
+
const editorType = column.columnDef.editorType
|
|
1262
|
+
const comparator =
|
|
1263
|
+
editorType === 'number'
|
|
1264
|
+
? localSortFns.number
|
|
1265
|
+
: editorType === 'date' || editorType === 'datetime'
|
|
1266
|
+
? localSortFns.date
|
|
1267
|
+
: localSortFns.auto
|
|
1268
|
+
|
|
1269
|
+
// Precompute one sort key per row, so the comparator reads an array slot
|
|
1270
|
+
// instead of walking the row's column index on every comparison. For the
|
|
1271
|
+
// three built-in comparators the key is also cheaper to compare than the
|
|
1272
|
+
// raw value: a timestamp rather than two `new Date()` allocations, a
|
|
1273
|
+
// number rather than two `Number()` coercions, a collator rather than a
|
|
1274
|
+
// fresh one per `localeCompare` call.
|
|
1275
|
+
//
|
|
1276
|
+
// The identity checks against `sortFns` matter: `localSortFns` is a
|
|
1277
|
+
// public parameter, so a caller can substitute their own comparators.
|
|
1278
|
+
// When they have, we fall through to calling their function with the raw
|
|
1279
|
+
// values - still hoisted, just not specialised.
|
|
1280
|
+
const columnId = column.id
|
|
1281
|
+
const n = rows.length
|
|
1282
|
+
let keys: Array<any> = new Array(n)
|
|
1283
|
+
let compare: (a: any, b: any) => number
|
|
1284
|
+
|
|
1285
|
+
if (comparator === sortFns.number) {
|
|
1286
|
+
for (let i = 0; i < n; i++) keys[i] = Number(rows[i]!.getCellValueByColumnId(columnId) ?? 0)
|
|
1287
|
+
compare = compareNumericKeys
|
|
1288
|
+
} else if (comparator === sortFns.date) {
|
|
1289
|
+
for (let i = 0; i < n; i++) {
|
|
1290
|
+
keys[i] = new Date(rows[i]!.getCellValueByColumnId(columnId) as any).getTime()
|
|
1291
|
+
}
|
|
1292
|
+
compare = compareNumericKeys
|
|
1293
|
+
} else if (comparator === sortFns.auto) {
|
|
1294
|
+
const strings: string[] = new Array(n)
|
|
1295
|
+
// Decide whether ranking is worth attempting BEFORE paying for it.
|
|
1296
|
+
//
|
|
1297
|
+
// Building the distinct set and then discarding it costs about 9 ms on
|
|
1298
|
+
// a 100k-row column where nearly every value is unique, and ranking
|
|
1299
|
+
// saves about 19 ms where they repeat - so guessing wrong in either
|
|
1300
|
+
// direction is measurable. A small stride sample answers it for well
|
|
1301
|
+
// under a millisecond.
|
|
1302
|
+
//
|
|
1303
|
+
// Strided rather than the first N rows: data arrives sorted or
|
|
1304
|
+
// clustered often enough that a prefix is a bad estimator of the whole
|
|
1305
|
+
// column. Reading every k-th row is no more expensive and does not care
|
|
1306
|
+
// how the rows are arranged.
|
|
1307
|
+
const rankLimit = n >> 1
|
|
1308
|
+
let distinct: Set<string> | null = null
|
|
1309
|
+
if (n > 0) {
|
|
1310
|
+
const sampleTarget = Math.min(n, 256)
|
|
1311
|
+
const stride = Math.max(1, Math.floor(n / sampleTarget))
|
|
1312
|
+
const sample = new Set<string>()
|
|
1313
|
+
let sampled = 0
|
|
1314
|
+
for (let i = 0; i < n; i += stride) {
|
|
1315
|
+
sample.add(String(rows[i]!.getCellValueByColumnId(columnId)))
|
|
1316
|
+
sampled++
|
|
1317
|
+
}
|
|
1318
|
+
// Only attempt ranking when the sample suggests real repetition.
|
|
1319
|
+
if (sample.size * 2 <= sampled) distinct = new Set()
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
for (let i = 0; i < n; i++) {
|
|
1323
|
+
const s = String(rows[i]!.getCellValueByColumnId(columnId))
|
|
1324
|
+
strings[i] = s
|
|
1325
|
+
if (distinct) {
|
|
1326
|
+
distinct.add(s)
|
|
1327
|
+
if (distinct.size > rankLimit) distinct = null
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
// Collation is by far the most expensive comparison we do - a CPU
|
|
1332
|
+
// profile of a 100k text sort put 65% of the whole operation inside the
|
|
1333
|
+
// collator. But a column's DISTINCT values are usually far fewer than
|
|
1334
|
+
// its rows (statuses, regions, categories, owners), so rank the
|
|
1335
|
+
// distinct values once and sort by rank afterwards. That turns
|
|
1336
|
+
// O(n log n) collator calls into O(u log u), where u is the number of
|
|
1337
|
+
// distinct values, and the resulting order is identical because rank is
|
|
1338
|
+
// a monotone relabelling of the collated order - equal strings share a
|
|
1339
|
+
// rank, so ties still fall through to the stable sort exactly as before.
|
|
1340
|
+
//
|
|
1341
|
+
// Guarded on the uniqueness ratio: when nearly every value is distinct
|
|
1342
|
+
// the ranking pass cannot save any collator calls and would just add an
|
|
1343
|
+
// O(n) Map build, so that case keeps comparing directly.
|
|
1344
|
+
if (distinct) {
|
|
1345
|
+
const ordered = Array.from(distinct).sort(compareCollatedKeys)
|
|
1346
|
+
const rankOf = new Map<string, number>()
|
|
1347
|
+
for (let i = 0; i < ordered.length; i++) rankOf.set(ordered[i]!, i)
|
|
1348
|
+
for (let i = 0; i < n; i++) keys[i] = rankOf.get(strings[i]!)!
|
|
1349
|
+
compare = compareNumericKeys
|
|
1350
|
+
} else {
|
|
1351
|
+
keys = strings
|
|
1352
|
+
compare = compareCollatedKeys
|
|
1353
|
+
}
|
|
1354
|
+
} else {
|
|
1355
|
+
for (let i = 0; i < n; i++) keys[i] = rows[i]!.getCellValueByColumnId(columnId)
|
|
1356
|
+
compare = comparator
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
clauses.push({ keys, desc: clause.desc, compare })
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
if (!clauses.length) return rows
|
|
1363
|
+
|
|
1364
|
+
// Sort an index array, then materialise. `Array.prototype.sort` is stable,
|
|
1365
|
+
// so equal keys keep their original relative order exactly as the previous
|
|
1366
|
+
// `[...rows].sort(...)` did.
|
|
1367
|
+
const order = new Array<number>(rows.length)
|
|
1368
|
+
for (let i = 0; i < order.length; i++) order[i] = i
|
|
1369
|
+
|
|
1370
|
+
// Single-clause sorts get a specialised comparator.
|
|
1371
|
+
//
|
|
1372
|
+
// Most sorts are one column, and that path runs O(n log n) times - 1.66
|
|
1373
|
+
// million comparisons for 100k rows. The general loop pays a clause-array
|
|
1374
|
+
// index, three property loads and an indirect call on every one of them,
|
|
1375
|
+
// none of which vary once the clause list is fixed. Hoisting them into a
|
|
1376
|
+
// closure and, for the numeric comparator, inlining the subtraction removes
|
|
1377
|
+
// the call entirely.
|
|
1378
|
+
//
|
|
1379
|
+
// `keys[ib] - keys[ia]` for descending is exactly `-(keys[ia] - keys[ib])`
|
|
1380
|
+
// as far as sorting is concerned: both are NaN for unorderable values,
|
|
1381
|
+
// which the spec coerces to 0, and they differ only in producing 0 versus
|
|
1382
|
+
// -0 for equal keys, which sorts identically.
|
|
1383
|
+
if (clauses.length === 1) {
|
|
1384
|
+
const { keys, compare, desc } = clauses[0]!
|
|
1385
|
+
if (compare === compareNumericKeys) {
|
|
1386
|
+
order.sort(
|
|
1387
|
+
desc
|
|
1388
|
+
? function compareOneNumericDesc(ia, ib) { return keys[ib] - keys[ia] }
|
|
1389
|
+
: function compareOneNumericAsc(ia, ib) { return keys[ia] - keys[ib] },
|
|
1390
|
+
)
|
|
1391
|
+
} else {
|
|
1392
|
+
order.sort(
|
|
1393
|
+
desc
|
|
1394
|
+
? function compareOneDesc(ia, ib) { return -compare(keys[ia], keys[ib]) }
|
|
1395
|
+
: function compareOneAsc(ia, ib) { return compare(keys[ia], keys[ib]) },
|
|
834
1396
|
)
|
|
835
|
-
if (result !== 0) return clause.desc ? -result : result
|
|
836
1397
|
}
|
|
837
|
-
|
|
838
|
-
|
|
1398
|
+
} else {
|
|
1399
|
+
order.sort(function compareRowsByClauses(ia, ib) {
|
|
1400
|
+
for (let k = 0; k < clauses.length; k++) {
|
|
1401
|
+
const clause = clauses[k]!
|
|
1402
|
+
const result = clause.compare(clause.keys[ia], clause.keys[ib])
|
|
1403
|
+
if (result !== 0) return clause.desc ? -result : result
|
|
1404
|
+
}
|
|
1405
|
+
return 0
|
|
1406
|
+
})
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
const sorted = new Array<Row<TData>>(rows.length)
|
|
1410
|
+
for (let i = 0; i < order.length; i++) sorted[i] = rows[order[i]!]!
|
|
839
1411
|
return sorted
|
|
840
1412
|
}
|
|
841
1413
|
}
|
|
842
1414
|
|
|
1415
|
+
/**
|
|
1416
|
+
* Numeric key comparison for the built-in `number` and `date` comparators.
|
|
1417
|
+
* Subtraction rather than `<`/`>` on purpose: it reproduces the originals
|
|
1418
|
+
* exactly, NaN included. An unparseable date or a non-numeric value yields NaN,
|
|
1419
|
+
* and the sort spec turns a NaN comparison result into 0 (SortCompare coerces
|
|
1420
|
+
* it), which is the behaviour callers already depend on.
|
|
1421
|
+
*/
|
|
1422
|
+
function compareNumericKeys(a: number, b: number): number {
|
|
1423
|
+
return a - b
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
/**
|
|
1427
|
+
* Text key comparison for the built-in `auto` comparator.
|
|
1428
|
+
*
|
|
1429
|
+
* `localeCompare`, NOT a hoisted `Intl.Collator`. The specification defines
|
|
1430
|
+
* `localeCompare` with no locale or options as constructing a default collator
|
|
1431
|
+
* per call, so hoisting one looks like the obvious optimisation - and it is
|
|
1432
|
+
* measurably slower. V8 fast-paths `String.prototype.localeCompare` for the
|
|
1433
|
+
* default locale; going through a collator object misses that path. Measured
|
|
1434
|
+
* sorting 100k strings: 33 ms via `localeCompare` against 83 ms via a cached
|
|
1435
|
+
* collator on ASCII, 58 ms against 99 ms with accents mixed in, and the two
|
|
1436
|
+
* produce byte-identical orderings across all 100k positions.
|
|
1437
|
+
*
|
|
1438
|
+
* Left as its own function so the sort path has one place to change if that
|
|
1439
|
+
* ever stops being true. Re-measure before "optimising" this again.
|
|
1440
|
+
*/
|
|
1441
|
+
function compareCollatedKeys(a: string, b: string): number {
|
|
1442
|
+
return a.localeCompare(b)
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
/**
|
|
1446
|
+
* Everything {@link createSvGridCore} accepts: the data and columns, the
|
|
1447
|
+
* features and row models that make up the pipeline, and an `on*Change`
|
|
1448
|
+
* callback per piece of state for controlled use.
|
|
1449
|
+
*
|
|
1450
|
+
* `<SvGrid>` builds this for you from its props - you only construct it
|
|
1451
|
+
* directly when driving the headless core.
|
|
1452
|
+
*/
|
|
843
1453
|
export type SvGridOptions<TFeatures extends TableFeatures, TData extends RowData> = {
|
|
844
1454
|
_features: TFeatures
|
|
845
1455
|
_rowModels?: {
|
|
@@ -869,6 +1479,13 @@ export type SvGridOptions<TFeatures extends TableFeatures, TData extends RowData
|
|
|
869
1479
|
onActiveCellChange?: (updater: Updater<ActiveCellState>) => void
|
|
870
1480
|
}
|
|
871
1481
|
|
|
1482
|
+
/**
|
|
1483
|
+
* The headless grid instance: the state stores plus the read methods a renderer
|
|
1484
|
+
* needs (`getHeaderGroups()`, `getRowModel()`, the `set*` writers).
|
|
1485
|
+
*
|
|
1486
|
+
* Framework free by design - `<SvGrid>` is one renderer over this, and you can
|
|
1487
|
+
* write another. See the "Why headless?" guide.
|
|
1488
|
+
*/
|
|
872
1489
|
export type SvGrid<TData extends RowData> = {
|
|
873
1490
|
store: Store<Record<string, any>>
|
|
874
1491
|
optionsStore: Store<Record<string, any>>
|
|
@@ -892,6 +1509,14 @@ type InternalGrid<TData extends RowData> = SvGrid<TData> & {
|
|
|
892
1509
|
getAllColumns: () => Array<Column<TData>>
|
|
893
1510
|
}
|
|
894
1511
|
|
|
1512
|
+
/**
|
|
1513
|
+
* Build a headless grid: state, the row pipeline, and the read methods, with no
|
|
1514
|
+
* DOM and no Svelte. This is the engine `<SvGrid>` renders.
|
|
1515
|
+
*
|
|
1516
|
+
* Most callers want `createSvGrid` (the runes-aware wrapper) or the component
|
|
1517
|
+
* itself; reach for this when you are writing your own renderer or running the
|
|
1518
|
+
* pipeline outside a browser.
|
|
1519
|
+
*/
|
|
895
1520
|
export function createSvGridCore<TFeatures extends TableFeatures, TData extends RowData>(
|
|
896
1521
|
options: SvGridOptions<TFeatures, TData>,
|
|
897
1522
|
): SvGrid<TData> {
|
|
@@ -922,7 +1547,6 @@ export function createSvGridCore<TFeatures extends TableFeatures, TData extends
|
|
|
922
1547
|
pagination: PaginationState | undefined
|
|
923
1548
|
grouping: GroupingState | undefined
|
|
924
1549
|
expanded: ExpandedState | undefined
|
|
925
|
-
rowSelection: RowSelectionState | undefined
|
|
926
1550
|
} | null = null
|
|
927
1551
|
|
|
928
1552
|
const grid = {
|
|
@@ -1131,91 +1755,71 @@ export function createSvGridCore<TFeatures extends TableFeatures, TData extends
|
|
|
1131
1755
|
for (let i = 0; i < columns.length; i++) columnIndexById.set(columns[i]!.id, i)
|
|
1132
1756
|
const columnCount = columns.length
|
|
1133
1757
|
|
|
1758
|
+
// One shared context for every row in this table, so a row carries a
|
|
1759
|
+
// pointer rather than a closure scope. See BASE_ROW_METHODS.
|
|
1760
|
+
const rowCtx: BaseRowCtx<TData> = {
|
|
1761
|
+
grid: grid as SvGrid<TData>,
|
|
1762
|
+
store,
|
|
1763
|
+
columns,
|
|
1764
|
+
columnCount,
|
|
1765
|
+
columnIndexById,
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1134
1768
|
cachedBaseRows = new Array(options.data.length)
|
|
1135
1769
|
const getRowId = options.getRowId
|
|
1770
|
+
const m = BASE_ROW_METHODS as unknown as {
|
|
1771
|
+
getCanExpand: Row<TData>['getCanExpand']
|
|
1772
|
+
getIsExpanded: Row<TData>['getIsExpanded']
|
|
1773
|
+
toggleExpanded: Row<TData>['toggleExpanded']
|
|
1774
|
+
getIsSelected: Row<TData>['getIsSelected']
|
|
1775
|
+
toggleSelected: Row<TData>['toggleSelected']
|
|
1776
|
+
getAllCells: Row<TData>['getAllCells']
|
|
1777
|
+
getCellValueByColumnId: Row<TData>['getCellValueByColumnId']
|
|
1778
|
+
}
|
|
1136
1779
|
for (let index = 0; index < options.data.length; index++) {
|
|
1137
1780
|
const original = options.data[index]!
|
|
1138
|
-
|
|
1139
|
-
//
|
|
1140
|
-
//
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
let cachedCells: Array<Cell<TData>> | null = null
|
|
1144
|
-
|
|
1145
|
-
function computeValues(): Array<unknown> {
|
|
1146
|
-
const values = new Array<unknown>(columnCount)
|
|
1147
|
-
for (let i = 0; i < columnCount; i++) {
|
|
1148
|
-
const column = columns[i]!
|
|
1149
|
-
if (column.columnDef.fieldFn) {
|
|
1150
|
-
values[i] = column.columnDef.fieldFn(original)
|
|
1151
|
-
} else if (column.columnDef.field) {
|
|
1152
|
-
values[i] = (original as any)[column.columnDef.field]
|
|
1153
|
-
} else {
|
|
1154
|
-
values[i] = undefined
|
|
1155
|
-
}
|
|
1156
|
-
}
|
|
1157
|
-
return values
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
const row: Row<TData> = {
|
|
1161
|
-
id,
|
|
1781
|
+
// `_values` and `_cells` stay null until something reads them - a
|
|
1782
|
+
// 100k-row grid showing twenty rows must not materialise every row's
|
|
1783
|
+
// values or cell objects to paint.
|
|
1784
|
+
const row: BaseRowState<TData> = {
|
|
1785
|
+
id: getRowId ? getRowId(original, index) : String(index),
|
|
1162
1786
|
index,
|
|
1163
1787
|
original,
|
|
1164
1788
|
depth: 0,
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
if (cachedCells) return cachedCells
|
|
1176
|
-
const built = new Array<Cell<TData>>(columnCount)
|
|
1177
|
-
for (let i = 0; i < columnCount; i++) {
|
|
1178
|
-
const column = columns[i]!
|
|
1179
|
-
const colIndex = i
|
|
1180
|
-
const cell: Cell<TData> = {
|
|
1181
|
-
id: `${id}_${column.id}`,
|
|
1182
|
-
row,
|
|
1183
|
-
column,
|
|
1184
|
-
getValue: () => {
|
|
1185
|
-
if (!cachedValues) cachedValues = computeValues()
|
|
1186
|
-
return cachedValues[colIndex]
|
|
1187
|
-
},
|
|
1188
|
-
getContext: () => ({
|
|
1189
|
-
cell,
|
|
1190
|
-
row,
|
|
1191
|
-
column,
|
|
1192
|
-
table: grid,
|
|
1193
|
-
getValue: () => cell.getValue(),
|
|
1194
|
-
}),
|
|
1195
|
-
}
|
|
1196
|
-
built[i] = cell
|
|
1197
|
-
}
|
|
1198
|
-
cachedCells = built
|
|
1199
|
-
return built
|
|
1200
|
-
},
|
|
1201
|
-
getCellValueByColumnId: (columnId: string) => {
|
|
1202
|
-
const idx = columnIndexById.get(columnId)
|
|
1203
|
-
if (idx === undefined) return undefined
|
|
1204
|
-
if (!cachedValues) cachedValues = computeValues()
|
|
1205
|
-
return cachedValues[idx]
|
|
1206
|
-
},
|
|
1789
|
+
[ROW_CTX]: rowCtx,
|
|
1790
|
+
[ROW_VALUES]: null,
|
|
1791
|
+
[ROW_CELLS]: null,
|
|
1792
|
+
getCanExpand: m.getCanExpand,
|
|
1793
|
+
getIsExpanded: m.getIsExpanded,
|
|
1794
|
+
toggleExpanded: m.toggleExpanded,
|
|
1795
|
+
getIsSelected: m.getIsSelected,
|
|
1796
|
+
toggleSelected: m.toggleSelected,
|
|
1797
|
+
getAllCells: m.getAllCells,
|
|
1798
|
+
getCellValueByColumnId: m.getCellValueByColumnId,
|
|
1207
1799
|
}
|
|
1208
1800
|
cachedBaseRows[index] = row
|
|
1209
1801
|
}
|
|
1210
1802
|
}
|
|
1211
1803
|
|
|
1804
|
+
// Only the slices a pipeline stage actually READS belong in this key.
|
|
1805
|
+
//
|
|
1806
|
+
// `rowSelection` used to be here, which meant ticking one checkbox on a
|
|
1807
|
+
// 100k-row grid re-filtered and re-sorted the entire dataset to rebuild a
|
|
1808
|
+
// row array that was identical by construction. Nothing reads it: the two
|
|
1809
|
+
// consumers are `getIsSelected` closures (on data rows and on group rows)
|
|
1810
|
+
// that read `store.state` when called, so they observe a selection change
|
|
1811
|
+
// without the model being rebuilt.
|
|
1812
|
+
//
|
|
1813
|
+
// `_rowModels` is a closed set of six named slots, so no consumer stage
|
|
1814
|
+
// can be inserted that might read selection. A caller CAN supply a custom
|
|
1815
|
+
// function for one of those slots; if one ever needs a slice that is not
|
|
1816
|
+
// listed here, add it here rather than reinstating all of them.
|
|
1212
1817
|
const currentSlices = {
|
|
1213
1818
|
sorting: store.state.sorting,
|
|
1214
1819
|
columnFilters: store.state.columnFilters,
|
|
1215
1820
|
pagination: store.state.pagination,
|
|
1216
1821
|
grouping: store.state.grouping,
|
|
1217
1822
|
expanded: store.state.expanded,
|
|
1218
|
-
rowSelection: store.state.rowSelection,
|
|
1219
1823
|
}
|
|
1220
1824
|
if (
|
|
1221
1825
|
cachedRowModel &&
|
|
@@ -1225,8 +1829,7 @@ export function createSvGridCore<TFeatures extends TableFeatures, TData extends
|
|
|
1225
1829
|
cachedSlices?.columnFilters === currentSlices.columnFilters &&
|
|
1226
1830
|
cachedSlices?.pagination === currentSlices.pagination &&
|
|
1227
1831
|
cachedSlices?.grouping === currentSlices.grouping &&
|
|
1228
|
-
cachedSlices?.expanded === currentSlices.expanded
|
|
1229
|
-
cachedSlices?.rowSelection === currentSlices.rowSelection
|
|
1832
|
+
cachedSlices?.expanded === currentSlices.expanded
|
|
1230
1833
|
) {
|
|
1231
1834
|
return cachedRowModel
|
|
1232
1835
|
}
|
|
@@ -1256,6 +1859,7 @@ export function createSvGridCore<TFeatures extends TableFeatures, TData extends
|
|
|
1256
1859
|
return grid
|
|
1257
1860
|
}
|
|
1258
1861
|
|
|
1862
|
+
/** Narrowing helper for the many options that accept a value or a function. */
|
|
1259
1863
|
export function isFunction(value: unknown): value is (...args: Array<any>) => any {
|
|
1260
1864
|
return typeof value === 'function'
|
|
1261
1865
|
}
|