@svgrid/grid 3.0.5 → 3.0.7
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/README.md +28 -7
- package/dist/GridMenus.svelte +34 -4
- package/dist/SvGrid.controller.svelte.d.ts +23 -3
- package/dist/SvGrid.controller.svelte.js +235 -54
- package/dist/SvGrid.css +143 -7
- package/dist/SvGrid.helpers.js +2 -4
- package/dist/SvGrid.svelte +223 -38
- package/dist/SvGrid.types.d.ts +77 -4
- package/dist/SvGridDropdown.svelte +20 -0
- package/dist/SvGridDropdown.svelte.d.ts +12 -0
- package/dist/SvModal.svelte +22 -0
- package/dist/SvModal.svelte.d.ts +7 -0
- package/dist/a11y/dismissable.d.ts +10 -0
- package/dist/a11y/dismissable.js +24 -2
- package/dist/build-api.js +81 -31
- package/dist/cdn/{GridMenus-CID62rlI.js → GridMenus-BgUS-W3o.js} +151 -142
- package/dist/cdn/GridMenus-CcwU8I4t.js +644 -0
- package/dist/cdn/{SvChartMenu-FBSMINA6.js → SvChartMenu-BFSTpvB-.js} +1 -1
- package/dist/cdn/{SvChartMenu-Bl6PBbkT.js → SvChartMenu-BZ1Iwv0y.js} +1 -1
- package/dist/cdn/{SvDateTimePicker-DQwt4UAs.js → SvDateTimePicker-D9u_cOf8.js} +1 -1
- package/dist/cdn/{SvDateTimePicker-vNU6bZ-q.js → SvDateTimePicker-s_6IUpI7.js} +1 -1
- package/dist/cdn/{SvGridCellEditor-B1p-vCK5.js → SvGridCellEditor-9yz-SbdZ.js} +2 -2
- package/dist/cdn/{SvGridCellEditor-D_0q4xAu.js → SvGridCellEditor-D7-ql1Hd.js} +2 -2
- package/dist/cdn/{SvGridChart-C3EWAZaM.js → SvGridChart-DMUOFxOe.js} +4 -4
- package/dist/cdn/{SvGridChart-DzLkSwCH.js → SvGridChart-N7ik0oWy.js} +4 -4
- package/dist/cdn/{SvGridChartBuilder-CfII62sZ.js → SvGridChartBuilder-CB_Jnm2k.js} +4 -4
- package/dist/cdn/{SvGridChartBuilder-BE2T1ykB.js → SvGridChartBuilder-Ca7gF8ZK.js} +4 -4
- package/dist/cdn/{SvGridChartPanel-xf4sWVTo.js → SvGridChartPanel-9GOJRHP7.js} +5 -5
- package/dist/cdn/{SvGridChartPanel-CCX5_Wgd.js → SvGridChartPanel-Dl-FujN8.js} +5 -5
- package/dist/cdn/{SvGridChartView-CfuXmY5I.js → SvGridChartView-B1LFgoPP.js} +2 -2
- package/dist/cdn/{SvGridChartView-DYwabQWj.js → SvGridChartView-By3n7DGZ.js} +2 -2
- package/dist/cdn/{SvGridDropdown-D0VdjeR8.js → SvGridDropdown-BWeM3lMe.js} +46 -46
- package/dist/cdn/{SvGridDropdown-D13MtJ2j.js → SvGridDropdown-CJ6QqTxu.js} +67 -67
- package/dist/cdn/{SvModal-CIZWCcad.js → SvModal-ClxGmpAc.js} +187 -186
- package/dist/cdn/{SvModal-BAE-pjZX.js → SvModal-DhB_VMFR.js} +148 -147
- package/dist/cdn/{cell-formatting-C2Hf5gqd.js → cell-formatting-Mnv84zoE.js} +28 -6
- package/dist/cdn/{chart-Dz7SqMXH.js → chart-By7h2Guv.js} +1 -1
- package/dist/cdn/{chart-panel-messages-CUbf2R4i.js → chart-panel-messages-BC-tOgFY.js} +1 -1
- package/dist/cdn/{chart-panel-messages-CmNrMdsr.js → chart-panel-messages-DUaXr-sb.js} +1 -1
- package/dist/cdn/{chart-summary-BJW_tg_X.js → chart-summary-BDtrNEiA.js} +1 -1
- package/dist/cdn/{chart-trend-CaN9mDEV.js → chart-trend-C1aOAPBZ.js} +1 -1
- package/dist/cdn/{dismissable-DAHetSNk.js → dismissable-Brc4EZU1.js} +5 -1
- package/dist/cdn/{editor-registry-BhgE3S84.js → editor-registry-BVeHvlZR.js} +49 -50
- package/dist/cdn/{export-format-CUDLy2yn.js → export-format-y1wdyCcv.js} +93 -97
- package/dist/cdn/{row-resize-niQCp040.js → row-resize-DtOS1KG6.js} +41 -32
- package/dist/cdn/{server-block-cache-CGoWz-87.js → server-block-cache-DIqb3VZn.js} +4 -0
- package/dist/cdn/{src-jcFMkBnN.js → src-25JmJ_7g.js} +4768 -4149
- package/dist/cdn/{src-Bklofsad.js → src-kE9B98r6.js} +4796 -4177
- package/dist/cdn/svgrid.js +18 -18
- package/dist/cdn/svgrid.svelte-external.js +18 -18
- package/dist/cdn/{validate-C7Rn_Bns.js → validate-AZoD8BoR.js} +6 -2
- package/dist/cell-formatting.d.ts +9 -0
- package/dist/cell-formatting.js +52 -1
- package/dist/cell-render.js +25 -6
- package/dist/clipboard.d.ts +2 -2
- package/dist/clipboard.js +72 -23
- package/dist/columns.js +7 -2
- package/dist/conditional-formatting.d.ts +2 -0
- package/dist/conditional-formatting.js +2 -0
- package/dist/core.d.ts +22 -6
- package/dist/core.js +478 -40
- package/dist/editing.js +24 -0
- package/dist/export-format.d.ts +8 -0
- package/dist/export-format.js +23 -16
- package/dist/grid-messages.d.ts +4 -0
- package/dist/grid-messages.js +4 -0
- package/dist/headless.d.ts +5 -0
- package/dist/headless.js +12 -0
- package/dist/history.d.ts +21 -0
- package/dist/history.js +34 -4
- package/dist/index.d.ts +1 -1
- package/dist/keyboard-handlers.js +24 -1
- package/dist/keyboard.js +4 -0
- package/dist/row-drag-touch.d.ts +1 -1
- package/dist/row-drag.js +35 -1
- package/dist/row-model.d.ts +6 -0
- package/dist/row-resize.js +22 -1
- package/dist/selection.js +1 -1
- package/dist/server-block-cache.d.ts +5 -0
- package/dist/server-block-cache.js +8 -0
- package/dist/server-data-source.d.ts +35 -3
- package/dist/server-data-source.js +11 -2
- package/dist/server.d.ts +1 -1
- package/dist/sparkline.js +7 -5
- package/dist/svgrid-wrapper.types.d.ts +33 -0
- package/dist/validate.js +9 -1
- package/dist/virtualization/column-virtualizer.d.ts +1 -0
- package/dist/virtualization/svelte-virtualizer.svelte.d.ts +1 -0
- package/dist/virtualization/svelte-virtualizer.svelte.js +1 -0
- package/dist/virtualization/virtualizer.d.ts +3 -0
- package/dist/virtualization/virtualizer.js +14 -1
- package/package.json +6 -1
- package/src/GridMenus.svelte +34 -4
- package/src/SvGrid.controller.svelte.ts +236 -57
- package/src/SvGrid.css +143 -7
- package/src/SvGrid.helpers.ts +2 -4
- package/src/SvGrid.svelte +223 -38
- package/src/SvGrid.types.ts +77 -4
- package/src/SvGridDropdown.svelte +20 -0
- package/src/SvModal.svelte +22 -0
- package/src/SvModal.test.ts +30 -1
- package/src/a11y/dismissable.test.ts +10 -1
- package/src/a11y/dismissable.ts +22 -2
- package/src/build-api.ts +85 -27
- package/src/cell-formatting.test.ts +17 -0
- package/src/cell-formatting.ts +49 -1
- package/src/cell-render.test.ts +24 -2
- package/src/cell-render.ts +24 -8
- package/src/clipboard.test.ts +107 -0
- package/src/clipboard.ts +62 -23
- package/src/columns.test.ts +4 -4
- package/src/columns.ts +7 -2
- package/src/conditional-formatting.test.ts +8 -0
- package/src/conditional-formatting.ts +3 -0
- package/src/core.sort.test.ts +86 -0
- package/src/core.tick-repair.test.ts +287 -0
- package/src/core.ts +478 -40
- package/src/editing.test.ts +52 -0
- package/src/editing.ts +25 -0
- package/src/export-dates.test.ts +114 -0
- package/src/export-format.ts +26 -14
- package/src/grid-messages.test.ts +7 -0
- package/src/grid-messages.ts +10 -0
- package/src/headless.ts +24 -0
- package/src/history.test.ts +49 -1
- package/src/history.ts +48 -2
- package/src/index.ts +2 -0
- package/src/keyboard-handlers.coverage.test.ts +65 -0
- package/src/keyboard-handlers.ts +26 -1
- package/src/keyboard.ts +3 -0
- package/src/row-drag-touch.ts +2 -2
- package/src/row-drag.test.ts +51 -0
- package/src/row-drag.ts +35 -2
- package/src/row-model.ts +6 -0
- package/src/row-resize.ts +24 -1
- package/src/selection.ts +1 -1
- package/src/server-block-cache.test.ts +27 -0
- package/src/server-block-cache.ts +13 -0
- package/src/server-data-source.infinite.test.ts +62 -0
- package/src/server-data-source.test.ts +19 -0
- package/src/server-data-source.ts +48 -4
- package/src/server.ts +2 -0
- package/src/sparkline.test.ts +7 -0
- package/src/sparkline.ts +6 -4
- package/src/svgrid-wrapper.types.ts +32 -0
- package/src/svgrid.api.test.ts +21 -0
- package/src/svgrid.charting.test.ts +12 -4
- package/src/svgrid.context-menu.test.ts +12 -0
- package/src/svgrid.data-push.svelte.test.ts +94 -0
- package/src/svgrid.detail-rows.svelte.test.ts +187 -0
- package/src/svgrid.filter-menu-scroll.test.ts +9 -0
- package/src/svgrid.gantt-seam.test.ts +1 -1
- package/src/svgrid.live-update-paths.svelte.test.ts +186 -0
- package/src/svgrid.menu-scroll-close.test.ts +10 -0
- package/src/svgrid.row-model-seam.svelte.test.ts +16 -0
- package/src/svgrid.row-pinning.test.ts +15 -1
- package/src/svgrid.selection-bar-seam.test.ts +1 -1
- package/src/svgrid.sticky-groups.svelte.test.ts +145 -0
- package/src/svgrid.upsell-license.test.ts +1 -1
- package/src/test-setup.ts +12 -0
- package/src/transaction.test.ts +58 -0
- package/src/validate.ts +7 -1
- package/src/virtualization/svelte-virtualizer.svelte.ts +1 -0
- package/src/virtualization/virtualizer.test.ts +44 -0
- package/src/virtualization/virtualizer.ts +13 -1
- package/dist/cdn/GridMenus-DLL-qZeu.js +0 -635
package/src/core.ts
CHANGED
|
@@ -393,8 +393,9 @@ export type ColumnDef<TFeatures extends TableFeatures, TData extends RowData> =
|
|
|
393
393
|
* - `false` → invalid, no message
|
|
394
394
|
* - a non-empty `string` → invalid, string is the tooltip
|
|
395
395
|
*
|
|
396
|
-
*
|
|
397
|
-
*
|
|
396
|
+
* On its own `validate` flags: the value keeps rendering as-is and an
|
|
397
|
+
* edit that fails it still lands. Set `rejectInvalid` to refuse such an
|
|
398
|
+
* edit instead.
|
|
398
399
|
*/
|
|
399
400
|
validate?: (params: {
|
|
400
401
|
value: unknown
|
|
@@ -402,6 +403,16 @@ export type ColumnDef<TFeatures extends TableFeatures, TData extends RowData> =
|
|
|
402
403
|
rowIndex: number
|
|
403
404
|
column: Column<TData>
|
|
404
405
|
}) => string | boolean | null | undefined
|
|
406
|
+
/**
|
|
407
|
+
* Refuse an edit that `validate` flags. When true, a committed value that
|
|
408
|
+
* makes `validate` return `false` or a message is not written: the cell
|
|
409
|
+
* keeps its old value, no history step is recorded and
|
|
410
|
+
* `onCellValueChange` does not fire. The value is checked after
|
|
411
|
+
* `valueParser` has run, so the rule sees what would be stored. Applies to
|
|
412
|
+
* the inline editor, a commit to a selection and a full-row edit; paste
|
|
413
|
+
* does not go through the editor and is not checked.
|
|
414
|
+
*/
|
|
415
|
+
rejectInvalid?: boolean
|
|
405
416
|
/**
|
|
406
417
|
* Gate editing per column or per cell.
|
|
407
418
|
*
|
|
@@ -734,13 +745,52 @@ export function tableFeatures<T extends TableFeatures>(features: T): T {
|
|
|
734
745
|
* text; set a column's type or supply your own comparator to override.
|
|
735
746
|
*/
|
|
736
747
|
export const sortFns = {
|
|
737
|
-
auto: (a: unknown, b: unknown) =>
|
|
738
|
-
number: (a: unknown, b: unknown) =>
|
|
739
|
-
date: (a: unknown, b: unknown) =>
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
748
|
+
auto: (a: unknown, b: unknown) => compareAutoValues(a, b),
|
|
749
|
+
number: (a: unknown, b: unknown) => compareNumericKeys(numberSortKey(a), numberSortKey(b)),
|
|
750
|
+
date: (a: unknown, b: unknown) => compareNumericKeys(dateSortKey(a), dateSortKey(b)),
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/** Null, undefined and the empty string: the values every built-in sort puts first. */
|
|
754
|
+
function isBlankSortValue(value: unknown): boolean {
|
|
755
|
+
return value == null || value === ''
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Sort key for the `number` comparator. Blanks and anything that does not
|
|
760
|
+
* coerce to a number ('n/a', a placeholder) become -Infinity, so they sort
|
|
761
|
+
* together ahead of every number instead of producing NaN. A NaN key made the
|
|
762
|
+
* comparator inconsistent - NaN compared equal to everything while the numbers
|
|
763
|
+
* around it did not - and `Array.prototype.sort` then returned a visibly
|
|
764
|
+
* unsorted column (#104).
|
|
765
|
+
*/
|
|
766
|
+
function numberSortKey(value: unknown): number {
|
|
767
|
+
if (isBlankSortValue(value)) return -Infinity
|
|
768
|
+
const n = Number(value)
|
|
769
|
+
return Number.isNaN(n) ? -Infinity : n
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/** Sort key for the `date` comparator; blanks and invalid dates sort first. */
|
|
773
|
+
function dateSortKey(value: unknown): number {
|
|
774
|
+
if (isBlankSortValue(value)) return -Infinity
|
|
775
|
+
const t = new Date(value as any).getTime()
|
|
776
|
+
return Number.isNaN(t) ? -Infinity : t
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* The `auto` comparator: blanks first, then numbers in numeric order, then
|
|
781
|
+
* everything else as collated text. Ordering by kind before value keeps it
|
|
782
|
+
* transitive on mixed columns - comparing a number to a string as text
|
|
783
|
+
* instead would give 2 < 10, 10 < '1a' and '1a' < 2, a cycle.
|
|
784
|
+
*/
|
|
785
|
+
function compareAutoValues(a: unknown, b: unknown): number {
|
|
786
|
+
const aBlank = isBlankSortValue(a)
|
|
787
|
+
const bBlank = isBlankSortValue(b)
|
|
788
|
+
if (aBlank || bBlank) return aBlank === bBlank ? 0 : aBlank ? -1 : 1
|
|
789
|
+
const aNum = typeof a === 'number'
|
|
790
|
+
const bNum = typeof b === 'number'
|
|
791
|
+
if (aNum && bNum) return compareNumericKeys(numberSortKey(a), numberSortKey(b))
|
|
792
|
+
if (aNum !== bNum) return aNum ? -1 : 1
|
|
793
|
+
return compareCollatedKeys(String(a), String(b))
|
|
744
794
|
}
|
|
745
795
|
|
|
746
796
|
/**
|
|
@@ -786,12 +836,29 @@ type BaseRowCtx<TData extends RowData> = {
|
|
|
786
836
|
const ROW_CTX = Symbol('svgrid.row.ctx')
|
|
787
837
|
const ROW_VALUES = Symbol('svgrid.row.values')
|
|
788
838
|
const ROW_CELLS = Symbol('svgrid.row.cells')
|
|
839
|
+
// Where the filtered stage last put this row: the generation of that full
|
|
840
|
+
// filter and the row's index in its output. Lets a tick swap a replaced row
|
|
841
|
+
// into place without re-filtering everything (see createFilteredRowModel).
|
|
842
|
+
const ROW_FILTER_GEN = Symbol('svgrid.row.filterGen')
|
|
843
|
+
const ROW_FILTER_POS = Symbol('svgrid.row.filterPos')
|
|
844
|
+
// Stamped on the rows a sort repair drops, so the merge skips them with one
|
|
845
|
+
// property read per row instead of a Set lookup (3 ms per 100k rows).
|
|
846
|
+
const ROW_SORT_DROPPED = Symbol('svgrid.row.sortDropped')
|
|
847
|
+
// The key(s) the last full sort computed for a row: the key itself for a
|
|
848
|
+
// one-clause sort, an array for several. Read by the repair path so a tick
|
|
849
|
+
// never recomputes a kept row's key.
|
|
850
|
+
const ROW_SORT_KEY = Symbol('svgrid.row.sortKey')
|
|
851
|
+
let sortDropGeneration = 0
|
|
789
852
|
|
|
790
853
|
/** A base row's private fields, on top of the public {@link Row} surface. */
|
|
791
854
|
type BaseRowState<TData extends RowData> = Row<TData> & {
|
|
792
855
|
[ROW_CTX]: BaseRowCtx<TData>
|
|
793
856
|
[ROW_VALUES]: Array<unknown> | null
|
|
794
857
|
[ROW_CELLS]: Array<Cell<TData>> | null
|
|
858
|
+
[ROW_FILTER_GEN]: number
|
|
859
|
+
[ROW_FILTER_POS]: number
|
|
860
|
+
[ROW_SORT_DROPPED]: number
|
|
861
|
+
[ROW_SORT_KEY]: unknown
|
|
795
862
|
}
|
|
796
863
|
|
|
797
864
|
/**
|
|
@@ -912,9 +979,26 @@ export function createCoreRowModel<TData extends RowData>(): RowModelFactory<TDa
|
|
|
912
979
|
* `columnFilteringFeature`; without it there are no filters to apply.
|
|
913
980
|
*/
|
|
914
981
|
export function createFilteredRowModel<TData extends RowData>(): RowModelFactory<TData> {
|
|
982
|
+
/**
|
|
983
|
+
* The last full filter, so a tick can repair it. When `getRowModel`
|
|
984
|
+
* reports which rows were replaced (`replacedRowsOf`) and the filter state
|
|
985
|
+
* and columns are the same objects as last time, each replacement is tested
|
|
986
|
+
* on its own: a row that stays in keeps its slot, a row that stays out
|
|
987
|
+
* stays out, and a row whose membership flips sends the whole thing back
|
|
988
|
+
* through the full pass, because an insertion has to land in data order.
|
|
989
|
+
* Positions come from stamps on the rows (ROW_FILTER_GEN / ROW_FILTER_POS)
|
|
990
|
+
* written by the full pass; no map is built.
|
|
991
|
+
*/
|
|
992
|
+
let cache: { input: Array<Row<TData>>; output: Array<Row<TData>>; filters: ColumnFiltersState; columns: Array<Column<TData>>; gen: number } | null = null
|
|
993
|
+
let generation = 0
|
|
994
|
+
|
|
915
995
|
return ({ table, rows }) => {
|
|
916
996
|
const filters: ColumnFiltersState = table.getState().columnFilters ?? []
|
|
917
|
-
if (!filters.length)
|
|
997
|
+
if (!filters.length) {
|
|
998
|
+
cache = null
|
|
999
|
+
return rows
|
|
1000
|
+
}
|
|
1001
|
+
const columns = table.getAllColumns()
|
|
918
1002
|
|
|
919
1003
|
// Resolve each filter's match function once, outside the row loop.
|
|
920
1004
|
const compiled = filters.map((filter) => ({
|
|
@@ -922,8 +1006,7 @@ export function createFilteredRowModel<TData extends RowData>(): RowModelFactory
|
|
|
922
1006
|
value: filter.value,
|
|
923
1007
|
fn: filter.fn ? filterFns[filter.fn] : filterFns.includesString,
|
|
924
1008
|
}))
|
|
925
|
-
|
|
926
|
-
return rows.filter((row) => {
|
|
1009
|
+
const passes = (row: Row<TData>) => {
|
|
927
1010
|
for (let i = 0; i < compiled.length; i++) {
|
|
928
1011
|
const filter = compiled[i]!
|
|
929
1012
|
// `getCellValueByColumnId` rather than `getAllCells().find(...)`.
|
|
@@ -935,7 +1018,48 @@ export function createFilteredRowModel<TData extends RowData>(): RowModelFactory
|
|
|
935
1018
|
if (!filter.fn(row.getCellValueByColumnId(filter.id), filter.value as any)) return false
|
|
936
1019
|
}
|
|
937
1020
|
return true
|
|
938
|
-
}
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
if (cache && cache.filters === filters && cache.columns === columns) {
|
|
1024
|
+
if (cache.input === rows) return cache.output
|
|
1025
|
+
const replaced = replacedRowsOf(table)
|
|
1026
|
+
if (replaced && rows.length === cache.input.length) {
|
|
1027
|
+
let out: Array<Row<TData>> | null = null
|
|
1028
|
+
let ok = true
|
|
1029
|
+
for (let i = 0; i < rows.length && ok; i++) {
|
|
1030
|
+
const prev = cache.input[i]! as BaseRowState<TData>
|
|
1031
|
+
const next = rows[i]! as BaseRowState<TData>
|
|
1032
|
+
if (next === prev) continue
|
|
1033
|
+
if (replaced.get(prev) !== next) { ok = false; break }
|
|
1034
|
+
const wasIn = prev[ROW_FILTER_GEN] === cache.gen
|
|
1035
|
+
const isIn = passes(next)
|
|
1036
|
+
if (wasIn !== isIn) { ok = false; break }
|
|
1037
|
+
if (!isIn) continue
|
|
1038
|
+
out ??= cache.output.slice()
|
|
1039
|
+
const pos = prev[ROW_FILTER_POS]
|
|
1040
|
+
out[pos] = next
|
|
1041
|
+
next[ROW_FILTER_GEN] = cache.gen
|
|
1042
|
+
next[ROW_FILTER_POS] = pos
|
|
1043
|
+
}
|
|
1044
|
+
if (ok) {
|
|
1045
|
+
const output = out ?? cache.output
|
|
1046
|
+
cache = { ...cache, input: rows, output }
|
|
1047
|
+
return output
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
const gen = ++generation
|
|
1053
|
+
const output: Array<Row<TData>> = []
|
|
1054
|
+
for (let i = 0; i < rows.length; i++) {
|
|
1055
|
+
const row = rows[i]! as BaseRowState<TData>
|
|
1056
|
+
if (!passes(row)) continue
|
|
1057
|
+
row[ROW_FILTER_GEN] = gen
|
|
1058
|
+
row[ROW_FILTER_POS] = output.length
|
|
1059
|
+
output.push(row)
|
|
1060
|
+
}
|
|
1061
|
+
cache = { input: rows, output, filters, columns, gen }
|
|
1062
|
+
return output
|
|
939
1063
|
}
|
|
940
1064
|
}
|
|
941
1065
|
/**
|
|
@@ -1225,6 +1349,15 @@ export function flattenTreeData<T extends RowData>(
|
|
|
1225
1349
|
export function createExpandedRowModel<TData extends RowData>(): RowModelFactory<TData> {
|
|
1226
1350
|
return ({ table, rows }) => {
|
|
1227
1351
|
const expanded: ExpandedState = table.getState().expanded ?? {}
|
|
1352
|
+
// Nothing expanded means every collapsed row hides its children and the
|
|
1353
|
+
// output is the input, element for element. Returning it as is keeps the
|
|
1354
|
+
// array identity the stages downstream and the render window key on, and
|
|
1355
|
+
// skips a 100k-element copy per data change on a flat grid.
|
|
1356
|
+
let anyExpanded = false
|
|
1357
|
+
for (const id in expanded) {
|
|
1358
|
+
if (expanded[id]) { anyExpanded = true; break }
|
|
1359
|
+
}
|
|
1360
|
+
if (!anyExpanded) return rows
|
|
1228
1361
|
const flattened: Array<Row<TData>> = []
|
|
1229
1362
|
const visit = (row: Row<TData>) => {
|
|
1230
1363
|
flattened.push(row)
|
|
@@ -1241,13 +1374,76 @@ export function createExpandedRowModel<TData extends RowData>(): RowModelFactory
|
|
|
1241
1374
|
* override the built-in {@link sortFns} - useful for locale-aware or
|
|
1242
1375
|
* domain-specific ordering.
|
|
1243
1376
|
*/
|
|
1377
|
+
/**
|
|
1378
|
+
* Which rows the last `getRowModel` rebuild replaced, old row -> new row, for
|
|
1379
|
+
* the duration of the pipeline run that follows it.
|
|
1380
|
+
*
|
|
1381
|
+
* A live feed replaces a few hundred row objects in a hundred-thousand-row
|
|
1382
|
+
* array and hands the grid the new array. The base rows are rebuilt reusing
|
|
1383
|
+
* every row whose data object is the same (see `getRowModel`), so the rebuild
|
|
1384
|
+
* knows exactly which rows are new. The filtered and sorted stages read that
|
|
1385
|
+
* here and repair their previous output instead of recomputing it, when the
|
|
1386
|
+
* replacements are few and nothing else changed. Cleared after the run; a
|
|
1387
|
+
* stage that reads it outside a run sees null and does the full work.
|
|
1388
|
+
*
|
|
1389
|
+
* Keyed on the grid object in a WeakMap rather than carried on the public
|
|
1390
|
+
* `SvGrid` type, so the stage signature stays `({ table, rows }) => rows`.
|
|
1391
|
+
*/
|
|
1392
|
+
const replacedRowsByGrid = new WeakMap<object, ReadonlyMap<Row<any>, Row<any>> | null>()
|
|
1393
|
+
|
|
1394
|
+
/** The rows `getRowModel` replaced in the rebuild this pipeline run follows, or null. */
|
|
1395
|
+
export function replacedRowsOf(table: SvGrid<any>): ReadonlyMap<Row<any>, Row<any>> | null {
|
|
1396
|
+
return replacedRowsByGrid.get(table) ?? null
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
/**
|
|
1400
|
+
* The sorting stage of the row pipeline: orders the rows by the grid's
|
|
1401
|
+
* `sorting` state with the built-in comparators (or the `localSortFns` map
|
|
1402
|
+
* handed in), stable, equal keys keeping their data order. A tick that
|
|
1403
|
+
* replaces a few rows out of many is repaired incrementally rather than
|
|
1404
|
+
* re-sorted (see the note inside). Registered by `rowSortingFeature`; an app
|
|
1405
|
+
* only calls this to install its own comparator set.
|
|
1406
|
+
*/
|
|
1244
1407
|
export function createSortedRowModel<TData extends RowData>(
|
|
1245
1408
|
localSortFns: typeof sortFns = sortFns,
|
|
1246
1409
|
): RowModelFactory<TData> {
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1410
|
+
/**
|
|
1411
|
+
* What the last full sort produced, kept so a tick can repair it.
|
|
1412
|
+
*
|
|
1413
|
+
* A live feed replaces a few hundred row objects out of a hundred thousand
|
|
1414
|
+
* and hands the grid a new array. Re-sorting all of it costs the same as
|
|
1415
|
+
* the first sort did (115-149 ms in the browser for 100k rows, see
|
|
1416
|
+
* docs/help/benchmarks.md); the order it would produce differs from the
|
|
1417
|
+
* previous one only where the replaced rows land. So the previous output
|
|
1418
|
+
* is kept, with its sort keys aligned to it, and when `getRowModel` says
|
|
1419
|
+
* which rows were replaced (see `replacedRowsOf`) the stage drops those,
|
|
1420
|
+
* sorts the replacements among themselves and merges them back in one
|
|
1421
|
+
* linear pass: O(n + k log k) instead of O(n log n) key builds and
|
|
1422
|
+
* comparisons.
|
|
1423
|
+
*
|
|
1424
|
+
* The repair is only attempted when everything else is the same object as
|
|
1425
|
+
* last time - the sorting state, the columns, and every input row except
|
|
1426
|
+
* the replaced ones - and it produces exactly what the full sort would:
|
|
1427
|
+
* equal keys fall back to the data index, which is the order a stable sort
|
|
1428
|
+
* over data-ordered input keeps.
|
|
1429
|
+
*/
|
|
1430
|
+
let cache: {
|
|
1431
|
+
input: Array<Row<TData>>
|
|
1432
|
+
output: Array<Row<TData>>
|
|
1433
|
+
sorting: SortingState
|
|
1434
|
+
columns: Array<Column<TData>>
|
|
1435
|
+
clauses: Array<SortClause>
|
|
1436
|
+
} | null = null
|
|
1437
|
+
|
|
1438
|
+
type SortClause = {
|
|
1439
|
+
desc: boolean
|
|
1440
|
+
compare: (a: any, b: any) => number
|
|
1441
|
+
/** The key for one row, or `MISSING_KEY` when it cannot be produced incrementally. */
|
|
1442
|
+
keyOf: (row: Row<TData>) => any
|
|
1443
|
+
}
|
|
1444
|
+
const MISSING_KEY = Symbol('svgrid.sort.missing-key')
|
|
1250
1445
|
|
|
1446
|
+
function fullSort(rows: Array<Row<TData>>, sorting: SortingState, allColumns: Array<Column<TData>>) {
|
|
1251
1447
|
// Resolve every clause ONCE, before sorting.
|
|
1252
1448
|
//
|
|
1253
1449
|
// This used to live inside the comparator, so `getAllColumns().find(...)`
|
|
@@ -1255,11 +1451,11 @@ export function createSortedRowModel<TData extends RowData>(
|
|
|
1255
1451
|
// 1,528,947 array scans, and a three-clause sort made 3,933,751 (measured;
|
|
1256
1452
|
// `pnpm bench --case=sort-1col`). The comparator is called O(n log n)
|
|
1257
1453
|
// times, so anything inside it that is not O(1) sets the cost of the sort.
|
|
1258
|
-
const allColumns = table.getAllColumns()
|
|
1259
1454
|
const clauses: Array<{
|
|
1260
1455
|
keys: Array<any>
|
|
1261
1456
|
desc: boolean
|
|
1262
1457
|
compare: (a: any, b: any) => number
|
|
1458
|
+
keyOf: (row: Row<TData>) => any
|
|
1263
1459
|
}> = []
|
|
1264
1460
|
|
|
1265
1461
|
for (const clause of sorting) {
|
|
@@ -1288,15 +1484,36 @@ export function createSortedRowModel<TData extends RowData>(
|
|
|
1288
1484
|
const n = rows.length
|
|
1289
1485
|
let keys: Array<any> = new Array(n)
|
|
1290
1486
|
let compare: (a: any, b: any) => number
|
|
1487
|
+
let keyOf: (row: Row<TData>) => any
|
|
1488
|
+
let autoMode: 'number' | 'mixed' | 'text' | undefined
|
|
1291
1489
|
|
|
1292
1490
|
if (comparator === sortFns.number) {
|
|
1293
|
-
|
|
1491
|
+
keyOf = (row) => numberSortKey(row.getCellValueByColumnId(columnId))
|
|
1492
|
+
for (let i = 0; i < n; i++) keys[i] = keyOf(rows[i]!)
|
|
1294
1493
|
compare = compareNumericKeys
|
|
1295
1494
|
} else if (comparator === sortFns.date) {
|
|
1296
|
-
|
|
1297
|
-
|
|
1495
|
+
keyOf = (row) => dateSortKey(row.getCellValueByColumnId(columnId))
|
|
1496
|
+
for (let i = 0; i < n; i++) keys[i] = keyOf(rows[i]!)
|
|
1497
|
+
compare = compareNumericKeys
|
|
1498
|
+
} else if (comparator === sortFns.auto && (autoMode = autoSortMode(rows, columnId)) === 'number') {
|
|
1499
|
+
// A column with no declared type whose values are all numbers. Sorting
|
|
1500
|
+
// it as text put 10 before 2 (#104).
|
|
1501
|
+
keyOf = (row) => {
|
|
1502
|
+
const value = row.getCellValueByColumnId(columnId)
|
|
1503
|
+
// A text value arriving in a tick changes the mode; only a full sort
|
|
1504
|
+
// can place it.
|
|
1505
|
+
return typeof value === 'number' || isBlankSortValue(value)
|
|
1506
|
+
? numberSortKey(value)
|
|
1507
|
+
: MISSING_KEY
|
|
1298
1508
|
}
|
|
1509
|
+
for (let i = 0; i < n; i++) keys[i] = keyOf(rows[i]!)
|
|
1299
1510
|
compare = compareNumericKeys
|
|
1511
|
+
} else if (autoMode === 'mixed') {
|
|
1512
|
+
// Numbers and text in one column. Rare, so no key tricks: the raw
|
|
1513
|
+
// values through the pairwise comparator, which orders by kind first.
|
|
1514
|
+
keyOf = (row) => row.getCellValueByColumnId(columnId)
|
|
1515
|
+
for (let i = 0; i < n; i++) keys[i] = keyOf(rows[i]!)
|
|
1516
|
+
compare = compareAutoValues
|
|
1300
1517
|
} else if (comparator === sortFns.auto) {
|
|
1301
1518
|
const strings: string[] = new Array(n)
|
|
1302
1519
|
// Decide whether ranking is worth attempting BEFORE paying for it.
|
|
@@ -1319,7 +1536,7 @@ export function createSortedRowModel<TData extends RowData>(
|
|
|
1319
1536
|
const sample = new Set<string>()
|
|
1320
1537
|
let sampled = 0
|
|
1321
1538
|
for (let i = 0; i < n; i += stride) {
|
|
1322
|
-
sample.add(
|
|
1539
|
+
sample.add(autoTextKey(rows[i]!.getCellValueByColumnId(columnId)))
|
|
1323
1540
|
sampled++
|
|
1324
1541
|
}
|
|
1325
1542
|
// Only attempt ranking when the sample suggests real repetition.
|
|
@@ -1327,7 +1544,7 @@ export function createSortedRowModel<TData extends RowData>(
|
|
|
1327
1544
|
}
|
|
1328
1545
|
|
|
1329
1546
|
for (let i = 0; i < n; i++) {
|
|
1330
|
-
const s =
|
|
1547
|
+
const s = autoTextKey(rows[i]!.getCellValueByColumnId(columnId))
|
|
1331
1548
|
strings[i] = s
|
|
1332
1549
|
if (distinct) {
|
|
1333
1550
|
distinct.add(s)
|
|
@@ -1354,19 +1571,32 @@ export function createSortedRowModel<TData extends RowData>(
|
|
|
1354
1571
|
for (let i = 0; i < ordered.length; i++) rankOf.set(ordered[i]!, i)
|
|
1355
1572
|
for (let i = 0; i < n; i++) keys[i] = rankOf.get(strings[i]!)!
|
|
1356
1573
|
compare = compareNumericKeys
|
|
1574
|
+
// A replacement whose text is not among the ranked values has no
|
|
1575
|
+
// rank; the repair path treats that as "sort everything again".
|
|
1576
|
+
keyOf = (row) => {
|
|
1577
|
+
const value = row.getCellValueByColumnId(columnId)
|
|
1578
|
+
// A number arriving in a tick makes the column mixed; resort fully.
|
|
1579
|
+
if (typeof value === 'number') return MISSING_KEY
|
|
1580
|
+
return rankOf.get(autoTextKey(value)) ?? MISSING_KEY
|
|
1581
|
+
}
|
|
1357
1582
|
} else {
|
|
1358
1583
|
keys = strings
|
|
1359
1584
|
compare = compareCollatedKeys
|
|
1585
|
+
keyOf = (row) => {
|
|
1586
|
+
const value = row.getCellValueByColumnId(columnId)
|
|
1587
|
+
return typeof value === 'number' ? MISSING_KEY : autoTextKey(value)
|
|
1588
|
+
}
|
|
1360
1589
|
}
|
|
1361
1590
|
} else {
|
|
1362
|
-
|
|
1591
|
+
keyOf = (row) => row.getCellValueByColumnId(columnId)
|
|
1592
|
+
for (let i = 0; i < n; i++) keys[i] = keyOf(rows[i]!)
|
|
1363
1593
|
compare = comparator
|
|
1364
1594
|
}
|
|
1365
1595
|
|
|
1366
|
-
clauses.push({ keys, desc: clause.desc, compare })
|
|
1596
|
+
clauses.push({ keys, desc: clause.desc, compare, keyOf })
|
|
1367
1597
|
}
|
|
1368
1598
|
|
|
1369
|
-
if (!clauses.length) return
|
|
1599
|
+
if (!clauses.length) return null
|
|
1370
1600
|
|
|
1371
1601
|
// Sort an index array, then materialise. `Array.prototype.sort` is stable,
|
|
1372
1602
|
// so equal keys keep their original relative order exactly as the previous
|
|
@@ -1415,19 +1645,194 @@ export function createSortedRowModel<TData extends RowData>(
|
|
|
1415
1645
|
|
|
1416
1646
|
const sorted = new Array<Row<TData>>(rows.length)
|
|
1417
1647
|
for (let i = 0; i < order.length; i++) sorted[i] = rows[order[i]!]!
|
|
1418
|
-
|
|
1648
|
+
// Each row keeps its key(s), so a later tick can place a replacement
|
|
1649
|
+
// among the kept rows without reading any of them again. One property
|
|
1650
|
+
// write per row; an array per row only when there are several clauses.
|
|
1651
|
+
if (clauses.length === 1) {
|
|
1652
|
+
const keys = clauses[0]!.keys
|
|
1653
|
+
for (let i = 0; i < rows.length; i++) (rows[i] as any)[ROW_SORT_KEY] = keys[i]
|
|
1654
|
+
} else {
|
|
1655
|
+
for (let i = 0; i < rows.length; i++) {
|
|
1656
|
+
const per = new Array(clauses.length)
|
|
1657
|
+
for (let k = 0; k < clauses.length; k++) per[k] = clauses[k]!.keys[i]
|
|
1658
|
+
;(rows[i] as any)[ROW_SORT_KEY] = per
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
return { sorted, clauses: clauses.map(({ desc, compare, keyOf }) => ({ desc, compare, keyOf })) }
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
/**
|
|
1665
|
+
* The previous output with the replaced rows swapped for their
|
|
1666
|
+
* replacements, in sorted position. Null when the input is not the cached
|
|
1667
|
+
* input plus replacements, or a replacement has no incremental key.
|
|
1668
|
+
*
|
|
1669
|
+
* Every row carries the key(s) the last full sort computed for it
|
|
1670
|
+
* (ROW_SORT_KEY: the key itself for one clause, an array for several), so
|
|
1671
|
+
* the merge moves row references and nothing else. At 100k rows the loop
|
|
1672
|
+
* that also copied an aligned key array cost 3.7 ms; this one is the
|
|
1673
|
+
* reference copy alone.
|
|
1674
|
+
*/
|
|
1675
|
+
function repair(rows: Array<Row<TData>>, replaced: ReadonlyMap<Row<any>, Row<any>>) {
|
|
1676
|
+
const c = cache!
|
|
1677
|
+
if (rows.length !== c.input.length) return null
|
|
1678
|
+
// Which rows of THIS stage's input changed. The map may name rows a
|
|
1679
|
+
// stage upstream filtered out; only the ones that reach here matter.
|
|
1680
|
+
const dropGen = ++sortDropGeneration
|
|
1681
|
+
const added: Array<Row<TData>> = []
|
|
1682
|
+
for (let i = 0; i < rows.length; i++) {
|
|
1683
|
+
const prev = c.input[i]!
|
|
1684
|
+
const next = rows[i]!
|
|
1685
|
+
if (next === prev) continue
|
|
1686
|
+
if (replaced.get(prev) !== next) return null
|
|
1687
|
+
;(prev as any)[ROW_SORT_DROPPED] = dropGen
|
|
1688
|
+
added.push(next)
|
|
1689
|
+
}
|
|
1690
|
+
if (!added.length) return c.output
|
|
1691
|
+
|
|
1692
|
+
const clauses = c.clauses
|
|
1693
|
+
const single = clauses.length === 1
|
|
1694
|
+
for (let j = 0; j < added.length; j++) {
|
|
1695
|
+
const row = added[j]! as any
|
|
1696
|
+
if (single) {
|
|
1697
|
+
const key = clauses[0]!.keyOf(row)
|
|
1698
|
+
if (key === MISSING_KEY) return null
|
|
1699
|
+
row[ROW_SORT_KEY] = key
|
|
1700
|
+
} else {
|
|
1701
|
+
const keys = new Array(clauses.length)
|
|
1702
|
+
for (let k = 0; k < clauses.length; k++) {
|
|
1703
|
+
const key = clauses[k]!.keyOf(row)
|
|
1704
|
+
if (key === MISSING_KEY) return null
|
|
1705
|
+
keys[k] = key
|
|
1706
|
+
}
|
|
1707
|
+
row[ROW_SORT_KEY] = keys
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
// Keys first, then the data index: the tie-break a stable sort over
|
|
1712
|
+
// data-ordered input applies, so the merge lands where a full sort would.
|
|
1713
|
+
const prevOut = c.output
|
|
1714
|
+
const compareRows = single
|
|
1715
|
+
? (() => {
|
|
1716
|
+
const { compare, desc } = clauses[0]!
|
|
1717
|
+
return (a: Row<TData>, b: Row<TData>) => {
|
|
1718
|
+
const r = compare((a as any)[ROW_SORT_KEY], (b as any)[ROW_SORT_KEY])
|
|
1719
|
+
if (r !== 0) return desc ? -r : r
|
|
1720
|
+
return a.index - b.index
|
|
1721
|
+
}
|
|
1722
|
+
})()
|
|
1723
|
+
: (a: Row<TData>, b: Row<TData>) => {
|
|
1724
|
+
const ka = (a as any)[ROW_SORT_KEY]
|
|
1725
|
+
const kb = (b as any)[ROW_SORT_KEY]
|
|
1726
|
+
for (let k = 0; k < clauses.length; k++) {
|
|
1727
|
+
const clause = clauses[k]!
|
|
1728
|
+
const r = clause.compare(ka[k], kb[k])
|
|
1729
|
+
if (r !== 0) return clause.desc ? -r : r
|
|
1730
|
+
}
|
|
1731
|
+
return a.index - b.index
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
// Sort the replacements among themselves (k log k)...
|
|
1735
|
+
added.sort(compareRows)
|
|
1736
|
+
|
|
1737
|
+
// ...find where each one goes in the previous output by binary search
|
|
1738
|
+
// (k log n comparisons, against the dropped rows too - they are skipped
|
|
1739
|
+
// below, which does not move an insertion point)...
|
|
1740
|
+
const n = prevOut.length
|
|
1741
|
+
const insertAt = new Array<number>(added.length)
|
|
1742
|
+
let lo = 0
|
|
1743
|
+
for (let j = 0; j < added.length; j++) {
|
|
1744
|
+
// Monotone: the replacements are sorted, so each search starts where
|
|
1745
|
+
// the previous one landed.
|
|
1746
|
+
const row = added[j]!
|
|
1747
|
+
let hi = n
|
|
1748
|
+
while (lo < hi) {
|
|
1749
|
+
const mid = (lo + hi) >>> 1
|
|
1750
|
+
if (compareRows(row, prevOut[mid]!) < 0) hi = mid
|
|
1751
|
+
else lo = mid + 1
|
|
1752
|
+
}
|
|
1753
|
+
insertAt[j] = lo
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
// ...then one linear pass that only moves references.
|
|
1757
|
+
const out = new Array<Row<TData>>(rows.length)
|
|
1758
|
+
let w = 0
|
|
1759
|
+
let j = 0
|
|
1760
|
+
let nextInsert = insertAt[0] ?? -1
|
|
1761
|
+
for (let i = 0; i < n; i++) {
|
|
1762
|
+
while (nextInsert === i) {
|
|
1763
|
+
out[w++] = added[j++]!
|
|
1764
|
+
nextInsert = j < added.length ? insertAt[j]! : -1
|
|
1765
|
+
}
|
|
1766
|
+
const prev = prevOut[i]!
|
|
1767
|
+
if ((prev as any)[ROW_SORT_DROPPED] === dropGen) continue
|
|
1768
|
+
out[w++] = prev
|
|
1769
|
+
}
|
|
1770
|
+
while (j < added.length) out[w++] = added[j++]!
|
|
1771
|
+
return out
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
return function sortedRowModelStage({ table, rows }) {
|
|
1775
|
+
const sorting = table.getState().sorting ?? []
|
|
1776
|
+
if (!sorting.length) {
|
|
1777
|
+
cache = null
|
|
1778
|
+
return rows
|
|
1779
|
+
}
|
|
1780
|
+
const columns = table.getAllColumns()
|
|
1781
|
+
|
|
1782
|
+
if (cache && cache.sorting === sorting && cache.columns === columns) {
|
|
1783
|
+
if (cache.input === rows) return cache.output
|
|
1784
|
+
const replaced = replacedRowsOf(table)
|
|
1785
|
+
if (replaced) {
|
|
1786
|
+
const repaired = repair(rows, replaced)
|
|
1787
|
+
if (repaired) {
|
|
1788
|
+
cache = { ...cache, input: rows, output: repaired }
|
|
1789
|
+
return repaired
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
const full = fullSort(rows, sorting, columns)
|
|
1795
|
+
if (!full) {
|
|
1796
|
+
cache = null
|
|
1797
|
+
return rows
|
|
1798
|
+
}
|
|
1799
|
+
cache = { input: rows, output: full.sorted, sorting, columns, clauses: full.clauses }
|
|
1800
|
+
return full.sorted
|
|
1419
1801
|
}
|
|
1420
1802
|
}
|
|
1421
1803
|
|
|
1422
1804
|
/**
|
|
1423
|
-
* Numeric key comparison for the built-in `number
|
|
1424
|
-
*
|
|
1425
|
-
*
|
|
1426
|
-
*
|
|
1427
|
-
*
|
|
1805
|
+
* Numeric key comparison for the built-in `number`, `date` and numeric
|
|
1806
|
+
* `auto` comparators. Keys are never NaN (blanks and unparseable values map to
|
|
1807
|
+
* -Infinity), but two -Infinity keys subtract to NaN, so equal keys return 0
|
|
1808
|
+
* explicitly: the multi-clause sort reads any non-zero result, NaN included,
|
|
1809
|
+
* as decided and would skip the next clause.
|
|
1428
1810
|
*/
|
|
1429
1811
|
function compareNumericKeys(a: number, b: number): number {
|
|
1430
|
-
return a - b
|
|
1812
|
+
return a === b ? 0 : a - b
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
/** Text key for the `auto` comparator: blanks become '' so they collate first. */
|
|
1816
|
+
function autoTextKey(value: unknown): string {
|
|
1817
|
+
return isBlankSortValue(value) ? '' : String(value)
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
/**
|
|
1821
|
+
* How the `auto` comparator should treat a column: 'number' when every
|
|
1822
|
+
* non-blank value is a number, 'mixed' when numbers share it with anything
|
|
1823
|
+
* else, 'text' otherwise. One pass, stopping as soon as the answer is known.
|
|
1824
|
+
*/
|
|
1825
|
+
function autoSortMode(rows: ReadonlyArray<Row<any>>, columnId: string): 'number' | 'mixed' | 'text' {
|
|
1826
|
+
let numbers = false
|
|
1827
|
+
let other = false
|
|
1828
|
+
for (let i = 0; i < rows.length; i++) {
|
|
1829
|
+
const value = rows[i]!.getCellValueByColumnId(columnId)
|
|
1830
|
+
if (isBlankSortValue(value)) continue
|
|
1831
|
+
if (typeof value === 'number') numbers = true
|
|
1832
|
+
else other = true
|
|
1833
|
+
if (numbers && other) return 'mixed'
|
|
1834
|
+
}
|
|
1835
|
+
return numbers ? 'number' : 'text'
|
|
1431
1836
|
}
|
|
1432
1837
|
|
|
1433
1838
|
/**
|
|
@@ -1545,6 +1950,8 @@ export function createSvGridCore<TFeatures extends TableFeatures, TData extends
|
|
|
1545
1950
|
let cachedBaseRowsInput: ReadonlyArray<TData> | null = null
|
|
1546
1951
|
let cachedBaseRowsColumns: Array<Column<TData>> | null = null
|
|
1547
1952
|
let cachedBaseRows: Array<Row<TData>> = []
|
|
1953
|
+
/** Old row -> new row for the rebuild the next pipeline run follows; see `replacedRowsOf`. */
|
|
1954
|
+
let replacedRows: Map<Row<TData>, Row<TData>> | null = null
|
|
1548
1955
|
let cachedRowCtx: BaseRowCtx<TData> | null = null
|
|
1549
1956
|
let cachedRowModel: RowModel<TData> | null = null
|
|
1550
1957
|
let cachedRowModelBaseRows: Array<Row<TData>> | null = null
|
|
@@ -1754,7 +2161,11 @@ export function createSvGridCore<TFeatures extends TableFeatures, TData extends
|
|
|
1754
2161
|
},
|
|
1755
2162
|
getRowModel() {
|
|
1756
2163
|
const columns = grid.getAllColumns()
|
|
1757
|
-
|
|
2164
|
+
// Read once. `options.data` is a getter when <SvGrid> drives the engine
|
|
2165
|
+
// (a $state.raw signal), and the rebuild below touched it twice per row:
|
|
2166
|
+
// 200k tracked reads per 100k-row tick, 11 ms of a 65 ms tick.
|
|
2167
|
+
const data = options.data
|
|
2168
|
+
if (cachedBaseRowsInput !== data || cachedBaseRowsColumns !== columns) {
|
|
1758
2169
|
// Same columns as last time: the shared context still describes them,
|
|
1759
2170
|
// and a row whose data object sits at the same index can keep its
|
|
1760
2171
|
// row object. A row model that streams blocks hands the grid a new
|
|
@@ -1764,7 +2175,11 @@ export function createSvGridCore<TFeatures extends TableFeatures, TData extends
|
|
|
1764
2175
|
// an app may have changed the object in place before passing a new
|
|
1765
2176
|
// array - that is the case the old rebuild covered by accident.
|
|
1766
2177
|
const previous = cachedBaseRowsColumns === columns && cachedRowCtx ? cachedBaseRows : null
|
|
1767
|
-
|
|
2178
|
+
// Same length as last time: a replacement, not an add or remove, so
|
|
2179
|
+
// each new row stands in for the old row at its index and the stages
|
|
2180
|
+
// can repair rather than recompute. Anything else is structural.
|
|
2181
|
+
replacedRows = previous && previous.length === data.length ? new Map() : null
|
|
2182
|
+
cachedBaseRowsInput = data
|
|
1768
2183
|
cachedBaseRowsColumns = columns
|
|
1769
2184
|
// O(1) column-id → index lookup so getCellValueByColumnId doesn't do
|
|
1770
2185
|
// a linear `findIndex` on every cell read (was O(rows × cells × cols)).
|
|
@@ -1786,7 +2201,7 @@ export function createSvGridCore<TFeatures extends TableFeatures, TData extends
|
|
|
1786
2201
|
cachedRowCtx = rowCtx
|
|
1787
2202
|
}
|
|
1788
2203
|
|
|
1789
|
-
cachedBaseRows = new Array(
|
|
2204
|
+
cachedBaseRows = new Array(data.length)
|
|
1790
2205
|
const getRowId = options.getRowId
|
|
1791
2206
|
const m = BASE_ROW_METHODS as unknown as {
|
|
1792
2207
|
getCanExpand: Row<TData>['getCanExpand']
|
|
@@ -1797,8 +2212,8 @@ export function createSvGridCore<TFeatures extends TableFeatures, TData extends
|
|
|
1797
2212
|
getAllCells: Row<TData>['getAllCells']
|
|
1798
2213
|
getCellValueByColumnId: Row<TData>['getCellValueByColumnId']
|
|
1799
2214
|
}
|
|
1800
|
-
for (let index = 0; index <
|
|
1801
|
-
const original =
|
|
2215
|
+
for (let index = 0; index < data.length; index++) {
|
|
2216
|
+
const original = data[index]!
|
|
1802
2217
|
const kept = previous?.[index] as BaseRowState<TData> | undefined
|
|
1803
2218
|
if (kept && kept.original === original) {
|
|
1804
2219
|
kept[ROW_VALUES] = null
|
|
@@ -1806,6 +2221,9 @@ export function createSvGridCore<TFeatures extends TableFeatures, TData extends
|
|
|
1806
2221
|
cachedBaseRows[index] = kept
|
|
1807
2222
|
continue
|
|
1808
2223
|
}
|
|
2224
|
+
// Past a quarter of the rows the merge stops being cheaper than a
|
|
2225
|
+
// sort with the ranking pass; let the stages do the full work.
|
|
2226
|
+
if (replacedRows && replacedRows.size * 4 >= data.length) replacedRows = null
|
|
1809
2227
|
// `_values` and `_cells` stay null until something reads them - a
|
|
1810
2228
|
// 100k-row grid showing twenty rows must not materialise every row's
|
|
1811
2229
|
// values or cell objects to paint.
|
|
@@ -1817,6 +2235,10 @@ export function createSvGridCore<TFeatures extends TableFeatures, TData extends
|
|
|
1817
2235
|
[ROW_CTX]: rowCtx,
|
|
1818
2236
|
[ROW_VALUES]: null,
|
|
1819
2237
|
[ROW_CELLS]: null,
|
|
2238
|
+
[ROW_FILTER_GEN]: 0,
|
|
2239
|
+
[ROW_FILTER_POS]: -1,
|
|
2240
|
+
[ROW_SORT_DROPPED]: 0,
|
|
2241
|
+
[ROW_SORT_KEY]: undefined,
|
|
1820
2242
|
getCanExpand: m.getCanExpand,
|
|
1821
2243
|
getIsExpanded: m.getIsExpanded,
|
|
1822
2244
|
toggleExpanded: m.toggleExpanded,
|
|
@@ -1826,7 +2248,15 @@ export function createSvGridCore<TFeatures extends TableFeatures, TData extends
|
|
|
1826
2248
|
getCellValueByColumnId: m.getCellValueByColumnId,
|
|
1827
2249
|
}
|
|
1828
2250
|
cachedBaseRows[index] = row
|
|
2251
|
+
if (replacedRows && kept) replacedRows.set(kept, row)
|
|
1829
2252
|
}
|
|
2253
|
+
// A new array with every object the same is the documented way to
|
|
2254
|
+
// say "I changed rows in place, re-read them" (docs/help/cells/
|
|
2255
|
+
// view-refresh.md). Nothing was replaced, so there is nothing to
|
|
2256
|
+
// repair around: the stages recompute from the current values. A
|
|
2257
|
+
// repair assumes the rows it keeps are unchanged in value as well as
|
|
2258
|
+
// identity, which is the immutable-update contract a tick follows.
|
|
2259
|
+
if (replacedRows && replacedRows.size === 0) replacedRows = null
|
|
1830
2260
|
}
|
|
1831
2261
|
|
|
1832
2262
|
// Only the slices a pipeline stage actually READS belong in this key.
|
|
@@ -1873,9 +2303,17 @@ export function createSvGridCore<TFeatures extends TableFeatures, TData extends
|
|
|
1873
2303
|
pipeline.expandedRowModel,
|
|
1874
2304
|
pipeline.paginatedRowModel,
|
|
1875
2305
|
]
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
2306
|
+
// The stages read the replacements through `replacedRowsOf` while the
|
|
2307
|
+
// run lasts; a stage that runs outside one sees null.
|
|
2308
|
+
replacedRowsByGrid.set(grid, replacedRows)
|
|
2309
|
+
try {
|
|
2310
|
+
ordered.forEach((fn) => {
|
|
2311
|
+
if (fn) rows = fn({ table: grid, rows })
|
|
2312
|
+
})
|
|
2313
|
+
} finally {
|
|
2314
|
+
replacedRowsByGrid.set(grid, null)
|
|
2315
|
+
replacedRows = null
|
|
2316
|
+
}
|
|
1879
2317
|
cachedPipeline = options._rowModels
|
|
1880
2318
|
cachedSlices = currentSlices
|
|
1881
2319
|
cachedRowModelBaseRows = cachedBaseRows
|