@svgrid/grid 3.0.6 → 3.0.8
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 +1 -1
- package/dist/SvGrid.controller.svelte.d.ts +2 -0
- package/dist/SvGrid.controller.svelte.js +46 -9
- package/dist/SvGrid.css +16 -5
- package/dist/SvGrid.helpers.js +2 -4
- package/dist/SvGrid.svelte +4 -1
- package/dist/cdn/{GridMenus-C-NtjlrJ.js → GridMenus-B8-Gbfq2.js} +2 -2
- package/dist/cdn/{GridMenus-TssHej3q.js → GridMenus-D15bhpX5.js} +2 -2
- package/dist/cdn/{SvGridCellEditor-BlyCSp0q.js → SvGridCellEditor-9yz-SbdZ.js} +1 -1
- package/dist/cdn/{SvGridCellEditor-ClRRA9lW.js → SvGridCellEditor-D7-ql1Hd.js} +1 -1
- package/dist/cdn/{SvGridChart-DsmwTyZF.js → SvGridChart-DMUOFxOe.js} +3 -3
- package/dist/cdn/{SvGridChart-B4EV8U6t.js → SvGridChart-N7ik0oWy.js} +3 -3
- package/dist/cdn/{SvGridChartBuilder-cHloHobN.js → SvGridChartBuilder-CB_Jnm2k.js} +3 -3
- package/dist/cdn/{SvGridChartBuilder-vOl7C0Za.js → SvGridChartBuilder-Ca7gF8ZK.js} +3 -3
- package/dist/cdn/{SvGridChartPanel-AcqF-Ll5.js → SvGridChartPanel-9GOJRHP7.js} +5 -5
- package/dist/cdn/{SvGridChartPanel-biMtSBPA.js → SvGridChartPanel-Dl-FujN8.js} +5 -5
- package/dist/cdn/{SvGridChartView-I3rDHXAq.js → SvGridChartView-B1LFgoPP.js} +2 -2
- package/dist/cdn/{SvGridChartView-Dx_GmZuL.js → SvGridChartView-By3n7DGZ.js} +2 -2
- package/dist/cdn/{cell-formatting-DVVKOSI9.js → cell-formatting-Mnv84zoE.js} +27 -5
- package/dist/cdn/{chart-DEZxwC0i.js → chart-By7h2Guv.js} +1 -1
- package/dist/cdn/{chart-panel-messages-D12XcTZJ.js → chart-panel-messages-BC-tOgFY.js} +1 -1
- package/dist/cdn/{chart-panel-messages-DguS7gC5.js → chart-panel-messages-DUaXr-sb.js} +1 -1
- package/dist/cdn/{chart-summary-CMAXBGY-.js → chart-summary-BDtrNEiA.js} +1 -1
- package/dist/cdn/{chart-trend-BkEp61Gj.js → chart-trend-C1aOAPBZ.js} +1 -1
- package/dist/cdn/{editor-registry-BhgE3S84.js → editor-registry-BVeHvlZR.js} +49 -50
- package/dist/cdn/{export-format-YzdwBww2.js → export-format-y1wdyCcv.js} +93 -97
- package/dist/cdn/{src-CgoeSM8k.js → src-CleI0g9l.js} +3256 -3198
- package/dist/cdn/{src-DfwxB1TH.js → src-Dz625Fhx.js} +3184 -3126
- package/dist/cdn/svgrid.js +11 -11
- package/dist/cdn/svgrid.svelte-external.js +11 -11
- package/dist/cdn/validate-GEs4ZPSW.js +80 -0
- package/dist/cell-formatting.d.ts +9 -0
- package/dist/cell-formatting.js +34 -0
- package/dist/core.js +124 -21
- package/dist/export-format.d.ts +8 -0
- package/dist/export-format.js +23 -16
- package/dist/headless.d.ts +1 -1
- package/dist/headless.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/keyboard-handlers.js +5 -1
- package/dist/selection.js +14 -4
- package/dist/{subscribe.js → subscribe.svelte.js} +5 -1
- package/dist/validate.js +10 -3
- 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 +1 -1
- package/src/SvGrid.controller.svelte.ts +45 -13
- package/src/SvGrid.css +16 -5
- package/src/SvGrid.helpers.ts +2 -4
- package/src/SvGrid.svelte +4 -1
- package/src/cell-formatting.ts +31 -0
- package/src/core.sort.test.ts +86 -0
- package/src/core.ts +114 -21
- package/src/export-dates.test.ts +114 -0
- package/src/export-format.ts +26 -14
- package/src/headless.reactivity.svelte.test.ts +89 -0
- package/src/headless.ts +1 -1
- package/src/index.ts +1 -1
- package/src/keyboard-handlers.ts +5 -1
- package/src/selection.test.ts +37 -0
- package/src/selection.ts +16 -6
- package/src/{subscribe.test.ts → subscribe.svelte.test.ts} +1 -1
- package/src/{subscribe.ts → subscribe.svelte.ts} +5 -1
- package/src/svgrid.behavior.test.ts +29 -0
- package/src/svgrid.data-push.svelte.test.ts +94 -0
- package/src/svgrid.live-update-paths.svelte.test.ts +4 -2
- package/src/validate.test.ts +16 -0
- package/src/validate.ts +10 -2
- 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/validate-AZoD8BoR.js +0 -80
- /package/dist/{subscribe.d.ts → subscribe.svelte.d.ts} +0 -0
package/src/index.ts
CHANGED
|
@@ -73,7 +73,7 @@ export {
|
|
|
73
73
|
|
|
74
74
|
export { createGrid, createSvGrid, type SvelteGrid } from './createGrid.svelte'
|
|
75
75
|
export { createGridState, createSvGridState } from './createGridState.svelte'
|
|
76
|
-
export { subscribeGrid, subscribeSvGrid } from './subscribe'
|
|
76
|
+
export { subscribeGrid, subscribeSvGrid } from './subscribe.svelte'
|
|
77
77
|
export { default as SvGrid } from './SvGrid.svelte'
|
|
78
78
|
// Kanban board view. The `board` prop + its config types live in the free grid,
|
|
79
79
|
// but the RENDERER (SvGridBoard) ships in @svgrid/enterprise and registers itself
|
package/src/keyboard-handlers.ts
CHANGED
|
@@ -216,7 +216,11 @@ export function createKeyboard<
|
|
|
216
216
|
}
|
|
217
217
|
}
|
|
218
218
|
if (column?.columnDef.editorType === "checkbox") {
|
|
219
|
-
|
|
219
|
+
// Same rule as a click: `editable: false` (or an editable callback
|
|
220
|
+
// saying no) keeps the checkbox read-only.
|
|
221
|
+
if (ctx.isCellEditableAt?.(current.rowIndex, current.colIndex) ?? true) {
|
|
222
|
+
ctx.toggleBooleanCell(current.rowIndex, current.colIndex);
|
|
223
|
+
}
|
|
220
224
|
return;
|
|
221
225
|
}
|
|
222
226
|
ctx.onCellDoubleClick(current.rowIndex, current.colIndex);
|
package/src/selection.test.ts
CHANGED
|
@@ -657,6 +657,43 @@ describe('createSelection - onCellClick', () => {
|
|
|
657
657
|
expect(ctx.selectionRange.anchor).toEqual({ rowIndex: 0, colIndex: 0 })
|
|
658
658
|
})
|
|
659
659
|
|
|
660
|
+
describe('boolean cells that are not declared checkboxes', () => {
|
|
661
|
+
const boolCtx = (over: any = {}, columnDef: any = {}) =>
|
|
662
|
+
clickCtx({
|
|
663
|
+
allRows: [makeDataRow('r0', { done: true })],
|
|
664
|
+
allColumns: [makeColumn('done', { field: 'done', ...columnDef })],
|
|
665
|
+
...over,
|
|
666
|
+
})
|
|
667
|
+
|
|
668
|
+
it('do not toggle (or write the row) while inline editing is off', () => {
|
|
669
|
+
const ctx = boolCtx({ editingEnabled: false })
|
|
670
|
+
createSelection(ctx).onCellClick(0, 0)
|
|
671
|
+
expect(ctx.toggleBooleanCell).not.toHaveBeenCalled()
|
|
672
|
+
expect(ctx.selectionRange.anchor).toEqual({ rowIndex: 0, colIndex: 0 })
|
|
673
|
+
})
|
|
674
|
+
|
|
675
|
+
it('toggle when inline editing is on and the cell is editable', () => {
|
|
676
|
+
const ctx = boolCtx({ editingEnabled: true, isCellEditableAt: () => true })
|
|
677
|
+
createSelection(ctx).onCellClick(0, 0)
|
|
678
|
+
expect(ctx.toggleBooleanCell).toHaveBeenCalledWith(0, 0)
|
|
679
|
+
})
|
|
680
|
+
|
|
681
|
+
it('do not toggle when the column has its own cell renderer', () => {
|
|
682
|
+
const ctx = boolCtx({ editingEnabled: true, isCellEditableAt: () => true }, { cell: () => 'flag' })
|
|
683
|
+
createSelection(ctx).onCellClick(0, 0)
|
|
684
|
+
expect(ctx.toggleBooleanCell).not.toHaveBeenCalled()
|
|
685
|
+
})
|
|
686
|
+
})
|
|
687
|
+
|
|
688
|
+
it('a declared checkbox that is not editable stays put', () => {
|
|
689
|
+
const ctx = clickCtx({
|
|
690
|
+
allColumns: [makeColumn('c0', { editorType: 'checkbox', editable: false })],
|
|
691
|
+
isCellEditableAt: () => false,
|
|
692
|
+
})
|
|
693
|
+
createSelection(ctx).onCellClick(0, 0)
|
|
694
|
+
expect(ctx.toggleBooleanCell).not.toHaveBeenCalled()
|
|
695
|
+
})
|
|
696
|
+
|
|
660
697
|
it('enters edit mode when clicking the previously-active editable cell', () => {
|
|
661
698
|
const ctx = clickCtx({
|
|
662
699
|
editingEnabled: true,
|
package/src/selection.ts
CHANGED
|
@@ -655,12 +655,22 @@ export function createSelection<
|
|
|
655
655
|
row: row.original as TData,
|
|
656
656
|
});
|
|
657
657
|
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
658
|
+
// A declared checkbox editor toggles on click unless the cell is not
|
|
659
|
+
// editable. A value that only HAPPENS to be boolean is drawn as a read-only
|
|
660
|
+
// checkbox, so it toggles only when inline editing is on - it used to flip
|
|
661
|
+
// and write the row on any click, in grids with editing switched off. A
|
|
662
|
+
// column with its own `cell` renderer draws (and handles) the value itself.
|
|
663
|
+
const declaredCheckbox = column.columnDef.editorType === "checkbox";
|
|
664
|
+
const inferredCheckbox =
|
|
665
|
+
!declaredCheckbox &&
|
|
666
|
+
typeof cellValue === "boolean" &&
|
|
667
|
+
column.columnDef.cell == null;
|
|
668
|
+
|
|
669
|
+
if (declaredCheckbox || inferredCheckbox) {
|
|
670
|
+
const editable = ctx.isCellEditableAt?.(rowIndex, colIndex) ?? true;
|
|
671
|
+
if (editable && (declaredCheckbox || ctx.editingEnabled)) {
|
|
672
|
+
ctx.toggleBooleanCell(rowIndex, colIndex);
|
|
673
|
+
}
|
|
664
674
|
setActiveCell(rowIndex, colIndex);
|
|
665
675
|
setSelection(rowIndex, colIndex);
|
|
666
676
|
ctx.editingCell = null;
|
|
@@ -23,7 +23,11 @@ export function subscribeGrid<
|
|
|
23
23
|
grid: SvGrid<TData>,
|
|
24
24
|
selector: (state: Record<string, any>) => TSelected,
|
|
25
25
|
) {
|
|
26
|
-
|
|
26
|
+
// $state, not a plain variable: core.ts keeps a deliberately framework-free
|
|
27
|
+
// store, so this Svelte adapter is where reactivity belongs. Without it a
|
|
28
|
+
// `$derived` reading .current never re-runs and the docs' headless examples
|
|
29
|
+
// render their seeded state forever.
|
|
30
|
+
let current = $state(selector(grid.store.state))
|
|
27
31
|
grid.store.subscribe(() => {
|
|
28
32
|
const next = selector(grid.store.state)
|
|
29
33
|
if (!shallowCompare(current, next)) current = next
|
|
@@ -620,6 +620,35 @@ describe('SvGrid - custom snippet cell', () => {
|
|
|
620
620
|
destroy()
|
|
621
621
|
}
|
|
622
622
|
})
|
|
623
|
+
|
|
624
|
+
it('a boolean column with its own cell renderer draws the renderer, not the built-in checkbox', async () => {
|
|
625
|
+
// The checkbox branch used to run before the `cell` branch, so any boolean
|
|
626
|
+
// value came out as a read-only checkbox and the renderer was never called.
|
|
627
|
+
const flagCols: ColumnDef<typeof fullFeatures, Person>[] = personColumns.map((c) =>
|
|
628
|
+
c.field === 'active'
|
|
629
|
+
? { ...c, cell: (ctx: any) => ((ctx.row.original as Person).active ? 'ON' : 'OFF') }
|
|
630
|
+
: c,
|
|
631
|
+
)
|
|
632
|
+
const { target, destroy } = await mountGrid({ columns: flagCols, showRowSelection: false })
|
|
633
|
+
try {
|
|
634
|
+
await tick()
|
|
635
|
+
expect(target.querySelectorAll('.sv-grid-checkbox-readonly').length).toBe(0)
|
|
636
|
+
expect(target.textContent).toContain('ON')
|
|
637
|
+
expect(target.textContent).toContain('OFF')
|
|
638
|
+
} finally {
|
|
639
|
+
destroy()
|
|
640
|
+
}
|
|
641
|
+
})
|
|
642
|
+
|
|
643
|
+
it('a boolean column without a renderer still draws the read-only checkbox', async () => {
|
|
644
|
+
const { target, destroy } = await mountGrid({ showRowSelection: false })
|
|
645
|
+
try {
|
|
646
|
+
await tick()
|
|
647
|
+
expect(target.querySelectorAll('.sv-grid-checkbox-readonly').length).toBeGreaterThan(0)
|
|
648
|
+
} finally {
|
|
649
|
+
destroy()
|
|
650
|
+
}
|
|
651
|
+
})
|
|
623
652
|
})
|
|
624
653
|
|
|
625
654
|
describe('SvGrid - filterMode shortcuts', () => {
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pushing onto a `$state` array passed as `data`.
|
|
3
|
+
*
|
|
4
|
+
* docs/getting-started/3-data-and-columns.md teaches exactly this: declare
|
|
5
|
+
* `let rows = $state([...])`, hand it to `data`, and an "Add row" button that
|
|
6
|
+
* does nothing but `rows.push(...)`. The grid is supposed to follow.
|
|
7
|
+
*
|
|
8
|
+
* `svgrid.live-update-paths.svelte.test.ts` covers the two MUTATION paths
|
|
9
|
+
* (field write, element replacement) against a sorted grid. This file covers
|
|
10
|
+
* the LENGTH path on the plainest possible grid, because a row appearing is a
|
|
11
|
+
* different question from a cell changing: nothing in the template is reading
|
|
12
|
+
* the new row yet, so the row model has to notice the array grew.
|
|
13
|
+
*
|
|
14
|
+
* Lives in a `.svelte.test.ts` so `$state` is available.
|
|
15
|
+
*/
|
|
16
|
+
import { describe, expect, it } from 'vitest'
|
|
17
|
+
import { mount, unmount, flushSync } from 'svelte'
|
|
18
|
+
import SvGrid from './SvGrid.svelte'
|
|
19
|
+
|
|
20
|
+
type Staff = { id: string; firstName: string; age: number }
|
|
21
|
+
|
|
22
|
+
const staffColumns = [
|
|
23
|
+
{ field: 'firstName', header: 'First name', width: 180 },
|
|
24
|
+
{ field: 'age', header: 'Age', width: 90 },
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
/** The props object is the `$state` proxy itself; a spread would snapshot it. */
|
|
28
|
+
function pushProps(rows: Staff[]) {
|
|
29
|
+
const props = $state({
|
|
30
|
+
data: rows,
|
|
31
|
+
columns: staffColumns,
|
|
32
|
+
containerHeight: 400,
|
|
33
|
+
virtualization: false,
|
|
34
|
+
})
|
|
35
|
+
return props
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function mountGrid(props: ReturnType<typeof pushProps>) {
|
|
39
|
+
const target = document.createElement('div')
|
|
40
|
+
document.body.appendChild(target)
|
|
41
|
+
const app = mount(SvGrid, { target, props: props as never })
|
|
42
|
+
flushSync()
|
|
43
|
+
const names = () =>
|
|
44
|
+
Array.from(
|
|
45
|
+
target.querySelectorAll('tbody.sv-grid-body tr.sv-grid-row:not(.sv-grid-row-spacer)'),
|
|
46
|
+
).map((tr) => tr.querySelector('.sv-grid-cell:not(.sv-grid-selection-cell)')?.textContent?.trim())
|
|
47
|
+
return {
|
|
48
|
+
names,
|
|
49
|
+
done() {
|
|
50
|
+
unmount(app)
|
|
51
|
+
target.remove()
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
describe('data array length changes', () => {
|
|
57
|
+
it('renders the rows it was given', () => {
|
|
58
|
+
const rows = $state<Staff[]>([
|
|
59
|
+
{ id: '1', firstName: 'Ada', age: 36 },
|
|
60
|
+
{ id: '2', firstName: 'Linus', age: 54 },
|
|
61
|
+
])
|
|
62
|
+
const g = mountGrid(pushProps(rows))
|
|
63
|
+
expect(g.names()).toEqual(['Ada', 'Linus'])
|
|
64
|
+
g.done()
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('push onto the $state array adds the row (the docs "Add row" button)', () => {
|
|
68
|
+
const rows = $state<Staff[]>([
|
|
69
|
+
{ id: '1', firstName: 'Ada', age: 36 },
|
|
70
|
+
{ id: '2', firstName: 'Linus', age: 54 },
|
|
71
|
+
])
|
|
72
|
+
const g = mountGrid(pushProps(rows))
|
|
73
|
+
|
|
74
|
+
rows.push({ id: '3', firstName: 'New', age: 0 })
|
|
75
|
+
flushSync()
|
|
76
|
+
|
|
77
|
+
expect(g.names()).toEqual(['Ada', 'Linus', 'New'])
|
|
78
|
+
g.done()
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('splice off the $state array removes the row', () => {
|
|
82
|
+
const rows = $state<Staff[]>([
|
|
83
|
+
{ id: '1', firstName: 'Ada', age: 36 },
|
|
84
|
+
{ id: '2', firstName: 'Linus', age: 54 },
|
|
85
|
+
])
|
|
86
|
+
const g = mountGrid(pushProps(rows))
|
|
87
|
+
|
|
88
|
+
rows.splice(0, 1)
|
|
89
|
+
flushSync()
|
|
90
|
+
|
|
91
|
+
expect(g.names()).toEqual(['Linus'])
|
|
92
|
+
g.done()
|
|
93
|
+
})
|
|
94
|
+
})
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The two ways a live feed can change a row, and what each one reaches.
|
|
3
3
|
*
|
|
4
|
-
* A `$state` array passed as `data` has two update paths
|
|
5
|
-
* interchangeable, which is the whole design problem of a
|
|
4
|
+
* A `$state` array passed as `data` has two update paths for a row's VALUES,
|
|
5
|
+
* and they are not interchangeable, which is the whole design problem of a
|
|
6
|
+
* ticking grid. (Changing the row COUNT is a third path, tracked on its own;
|
|
7
|
+
* it lives in `svgrid.data-push.svelte.test.ts`.)
|
|
6
8
|
*
|
|
7
9
|
* 1. Mutate a field through the proxy (`rows[i].price = x`). The visible
|
|
8
10
|
* cell re-renders because its template reads the field through the
|
package/src/validate.test.ts
CHANGED
|
@@ -58,6 +58,22 @@ describe('unknown field', () => {
|
|
|
58
58
|
expect(run({ data: [...loading, ...data], columns: [{ field: 'naem' }] })).toHaveLength(1)
|
|
59
59
|
})
|
|
60
60
|
|
|
61
|
+
it('does not walk a million placeholders looking for the sample', () => {
|
|
62
|
+
// A row model scrolled to its middle: placeholders up to the loaded block.
|
|
63
|
+
const placeholder = createRowPlaceholder('loading', { index: 0 })
|
|
64
|
+
const big = new Array(1_000_000).fill(placeholder)
|
|
65
|
+
big.push({ name: 'x' })
|
|
66
|
+
let reads = 0
|
|
67
|
+
const counted = new Proxy(big, {
|
|
68
|
+
get(target, key, receiver) {
|
|
69
|
+
if (typeof key === 'string' && /^\d+$/.test(key)) reads += 1
|
|
70
|
+
return Reflect.get(target, key, receiver)
|
|
71
|
+
},
|
|
72
|
+
})
|
|
73
|
+
expect(run({ data: counted, columns: [{ field: 'naem' }] })).toEqual([])
|
|
74
|
+
expect(reads).toBeLessThanOrEqual(1000)
|
|
75
|
+
})
|
|
76
|
+
|
|
61
77
|
it('looks through group columns', () => {
|
|
62
78
|
const msgs = run({ columns: [{ header: 'Group', columns: [{ field: 'nope' }] }] })
|
|
63
79
|
expect(msgs[0]).toContain('"nope" does not exist')
|
package/src/validate.ts
CHANGED
|
@@ -24,6 +24,8 @@ import { rowPlaceholderState } from './server-block-cache'
|
|
|
24
24
|
|
|
25
25
|
/** How many rows to sample when deciding whether a `field` exists. */
|
|
26
26
|
const FIELD_SAMPLE_ROWS = 10
|
|
27
|
+
/** How many rows to look through for that sample, placeholders included. */
|
|
28
|
+
const FIELD_SCAN_ROWS = 1000
|
|
27
29
|
|
|
28
30
|
const DOCS = 'https://svgrid.com/docs/getting-started/3-data-and-columns/'
|
|
29
31
|
|
|
@@ -79,9 +81,15 @@ export function validateGridConfig<
|
|
|
79
81
|
// no data keys at all and say nothing about the columns.
|
|
80
82
|
// Stops at the sample size: this runs on every data change, and a filter
|
|
81
83
|
// over all the rows first was O(n) per tick on a live feed.
|
|
84
|
+
// The scan is bounded too: a row model scrolled to the middle of a million
|
|
85
|
+
// rows, or just purged by a sort, hands over a list that is placeholders
|
|
86
|
+
// up to the loaded block, and walking all of them for ten real rows was
|
|
87
|
+
// O(n) per block landing. Finding none means nothing to judge by.
|
|
82
88
|
const sample: TData[] = []
|
|
83
|
-
|
|
84
|
-
|
|
89
|
+
const data = input.data ?? []
|
|
90
|
+
const scan = Math.min(data.length, FIELD_SCAN_ROWS)
|
|
91
|
+
for (let i = 0; i < scan && sample.length < FIELD_SAMPLE_ROWS; i += 1) {
|
|
92
|
+
const row = data[i]!
|
|
85
93
|
if (!rowPlaceholderState(row)) sample.push(row)
|
|
86
94
|
}
|
|
87
95
|
// Under server-side grouping the first rows are group rows, which carry
|
|
@@ -14,6 +14,7 @@ export function createSvelteVirtualizer(options: VirtualizerOptions) {
|
|
|
14
14
|
return version
|
|
15
15
|
},
|
|
16
16
|
setOptions: virtualizer.setOptions,
|
|
17
|
+
measure: virtualizer.measure,
|
|
17
18
|
setScrollOffset: virtualizer.setScrollOffset,
|
|
18
19
|
setViewportHeight: virtualizer.setViewportHeight,
|
|
19
20
|
scrollToIndex: virtualizer.scrollToIndex,
|
|
@@ -44,4 +44,48 @@ describe('virtualizer', () => {
|
|
|
44
44
|
v.scrollToIndex(9_999)
|
|
45
45
|
expect(v.getState().scrollOffset).toBe(2_700)
|
|
46
46
|
})
|
|
47
|
+
|
|
48
|
+
it('re-measures a stable size function passed back through setOptions (#102)', () => {
|
|
49
|
+
const heights = new Map<number, number>()
|
|
50
|
+
const estimateSize = (i: number) => heights.get(i) ?? 20
|
|
51
|
+
const v = createVirtualizer({ count: 10, estimateSize, viewportHeight: 100 })
|
|
52
|
+
expect(v.getTotalSize()).toBe(200)
|
|
53
|
+
|
|
54
|
+
heights.set(0, 120)
|
|
55
|
+
v.setOptions({ estimateSize })
|
|
56
|
+
expect(v.getSizeForIndex(0)).toBe(120)
|
|
57
|
+
expect(v.getTotalSize()).toBe(300) // 9 x 20 + 120
|
|
58
|
+
expect(v.getOffsetForIndex(5)).toBe(200) // 120 + 4 x 20
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('measure() re-reads sizes and notifies subscribers', () => {
|
|
62
|
+
const heights = new Map<number, number>()
|
|
63
|
+
const v = createVirtualizer({
|
|
64
|
+
count: 10,
|
|
65
|
+
estimateSize: (i: number) => heights.get(i) ?? 20,
|
|
66
|
+
viewportHeight: 100,
|
|
67
|
+
})
|
|
68
|
+
let notified = 0
|
|
69
|
+
v.subscribe(() => (notified += 1))
|
|
70
|
+
|
|
71
|
+
heights.set(3, 50)
|
|
72
|
+
v.measure()
|
|
73
|
+
expect(v.getTotalSize()).toBe(230)
|
|
74
|
+
expect(v.getOffsetForIndex(4)).toBe(110)
|
|
75
|
+
expect(notified).toBe(1)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('keeps the offset cache across scrolls', () => {
|
|
79
|
+
let calls = 0
|
|
80
|
+
const v = createVirtualizer({
|
|
81
|
+
count: 1000,
|
|
82
|
+
estimateSize: () => (calls++, 20),
|
|
83
|
+
viewportHeight: 100,
|
|
84
|
+
})
|
|
85
|
+
const afterBuild = calls
|
|
86
|
+
v.setScrollOffset(500)
|
|
87
|
+
v.setScrollOffset(900)
|
|
88
|
+
v.setViewportHeight(200)
|
|
89
|
+
expect(calls).toBe(afterBuild)
|
|
90
|
+
})
|
|
47
91
|
})
|
|
@@ -261,8 +261,20 @@ export function createVirtualizer(initial: VirtualizerOptions) {
|
|
|
261
261
|
return {
|
|
262
262
|
setOptions(next: Partial<VirtualizerOptions>) {
|
|
263
263
|
const merged = { ...options, ...next }
|
|
264
|
-
|
|
264
|
+
// Passing a size function means "sizes may have changed", even when it is
|
|
265
|
+
// the same reference: the documented variable-height pattern is a stable
|
|
266
|
+
// function reading a mutable map, and keying the cache on identity alone
|
|
267
|
+
// left the offsets and total stale after a row grew (#102).
|
|
268
|
+
const remeasure = typeof next.estimateSize === 'function'
|
|
269
|
+
if (!remeasure && sameOptions(options, merged)) return
|
|
265
270
|
options = merged
|
|
271
|
+
if (remeasure) offsetCache = null
|
|
272
|
+
recalc()
|
|
273
|
+
},
|
|
274
|
+
/** Drop the cached offsets and re-read every size from `estimateSize`.
|
|
275
|
+
* Call after the sizes a stable size function reports have changed. */
|
|
276
|
+
measure() {
|
|
277
|
+
offsetCache = null
|
|
266
278
|
recalc()
|
|
267
279
|
},
|
|
268
280
|
setScrollOffset(scrollOffset: number) {
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import { r as e } from "./server-block-cache-DIqb3VZn.js";
|
|
2
|
-
//#region src/validate.ts
|
|
3
|
-
var t = 10, n = "https://svgrid.com/docs/getting-started/3-data-and-columns/";
|
|
4
|
-
function r(e, t = []) {
|
|
5
|
-
for (let n of e) n.columns?.length ? r(n.columns, t) : t.push(n);
|
|
6
|
-
return t;
|
|
7
|
-
}
|
|
8
|
-
function i(i) {
|
|
9
|
-
let o = [], s = r(i.columns ?? []), c = [];
|
|
10
|
-
for (let n of i.data ?? []) {
|
|
11
|
-
if (c.length >= t) break;
|
|
12
|
-
e(n) || c.push(n);
|
|
13
|
-
}
|
|
14
|
-
if (c.length && !i.serverGroup) {
|
|
15
|
-
let e = /* @__PURE__ */ new Set();
|
|
16
|
-
for (let t of c) if (t && typeof t == "object") for (let n of Object.keys(t)) e.add(n);
|
|
17
|
-
for (let t of s) {
|
|
18
|
-
let r = t.field;
|
|
19
|
-
if (!r || t.fieldFn || e.has(r)) continue;
|
|
20
|
-
let i = a(r, [...e]);
|
|
21
|
-
o.push(`[svgrid] Column field "${r}" does not exist on your row data, so that column renders empty.${i ? ` Did you mean "${i}"?` : ""} Available keys: ${[...e].slice(0, 12).join(", ")}. See ${n}`);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
let l = /* @__PURE__ */ new Map();
|
|
25
|
-
for (let e of s) {
|
|
26
|
-
let t = e.id ?? e.field;
|
|
27
|
-
t && l.set(t, (l.get(t) ?? 0) + 1);
|
|
28
|
-
}
|
|
29
|
-
for (let [e, t] of l) t > 1 && o.push(`[svgrid] ${t} columns share the id "${e}". Column state (sorting, filtering, pinning, width) is keyed by id, so they will act as one. Give each an explicit \`id\`. See ${n}`);
|
|
30
|
-
let u = i.pageable === void 0 && i.showPagination === void 0;
|
|
31
|
-
i.pageSize !== void 0 && u && o.push("[svgrid] `pageSize` is set but pagination was never turned on, so it has no effect and every row renders. Add `pageable` to switch the pager on."), !(i.sortable === !0 || i.features?.rowSortingFeature) && s.some((e) => e.sortable === !0) && o.push("[svgrid] A column sets `sortable: true`, but sorting is not enabled on the grid, so its header does nothing. Add `sortable` to <SvGrid> (it registers the sorting feature for you).");
|
|
32
|
-
let d = /* @__PURE__ */ new Set();
|
|
33
|
-
for (let e of s) {
|
|
34
|
-
let t = e.id ?? e.field;
|
|
35
|
-
t && d.add(t);
|
|
36
|
-
}
|
|
37
|
-
for (let e of i.groupBy ?? []) {
|
|
38
|
-
if (d.has(e)) continue;
|
|
39
|
-
let t = a(e, [...d]);
|
|
40
|
-
o.push(`[svgrid] \`groupBy\` refers to column "${e}", which does not exist, so that grouping level is ignored.${t ? ` Did you mean "${t}"?` : ""} Column ids: ${[...d].slice(0, 12).join(", ")}`);
|
|
41
|
-
}
|
|
42
|
-
let f = i.treeData?.column;
|
|
43
|
-
if (f && !d.has(f) && o.push(`[svgrid] \`treeData.column\` refers to column "${f}", which does not exist, so the expander falls back to the first visible column.`), c.length && i.treeData) {
|
|
44
|
-
let e = /* @__PURE__ */ new Set();
|
|
45
|
-
for (let t of c) if (t && typeof t == "object") for (let n of Object.keys(t)) e.add(n);
|
|
46
|
-
let t = i.treeData.parentField;
|
|
47
|
-
if (t && !e.has(t)) {
|
|
48
|
-
let n = a(t, [...e]);
|
|
49
|
-
o.push(`[svgrid] \`treeData.parentField\` is "${t}", which is not on your row data, so every row becomes a root and no hierarchy appears.${n ? ` Did you mean "${n}"?` : ""}`);
|
|
50
|
-
}
|
|
51
|
-
let n = i.treeData.idField;
|
|
52
|
-
n && !e.has(n) && o.push(`[svgrid] \`treeData.idField\` is "${n}", which is not on your row data, so parent lookups cannot match and the tree stays flat.`);
|
|
53
|
-
}
|
|
54
|
-
return i.gantt && i.treeData && o.push("[svgrid] `gantt` and `treeData` are both set. The Gantt builds its own work-breakdown tree from `gantt.parentField` and needs every descendant row, but `treeData` hides collapsed children from the view, so summary bars will under-report. Remove `treeData` and set `gantt.parentField` instead."), (i.initialColumnPinning?.left?.length ?? 0) + (i.initialColumnPinning?.right?.length ?? 0) > 0 && i.columnVirtualization !== !1 && o.push("[svgrid] `initialColumnPinning` is set while column virtualization is on (its default), so the pinned columns will not stick - the virtualizer recycles column nodes. Add `columnVirtualization={false}`."), i.externalPagination === !0 && i.rowCount === void 0 && o.push("[svgrid] `externalPagination` is on but `rowCount` is not set, so the pager cannot know how many pages exist. Pass the server total as `rowCount`."), i.externalSort === !0 && !i.onSortingChange && o.push("[svgrid] `externalSort` is on but there is no `onSortingChange` handler, so clicking a header changes nothing - the grid stopped sorting and nobody is listening. Add `onSortingChange` and re-fetch in your handler."), i.externalFilter === !0 && !i.onFiltersChange && o.push("[svgrid] `externalFilter` is on but there is no `onFiltersChange` handler, so filtering the grid changes nothing. Add `onFiltersChange` and re-fetch in your handler."), o;
|
|
55
|
-
}
|
|
56
|
-
function a(e, t) {
|
|
57
|
-
let n, r = Infinity;
|
|
58
|
-
for (let i of t) {
|
|
59
|
-
let t = o(e.toLowerCase(), i.toLowerCase());
|
|
60
|
-
t < r && (r = t, n = i);
|
|
61
|
-
}
|
|
62
|
-
let i = Math.min(3, Math.floor(e.length / 3) + 1);
|
|
63
|
-
return r <= i ? n : void 0;
|
|
64
|
-
}
|
|
65
|
-
function o(e, t) {
|
|
66
|
-
if (e === t) return 0;
|
|
67
|
-
let n = Array(t.length + 1);
|
|
68
|
-
for (let e = 0; e <= t.length; e++) n[e] = e;
|
|
69
|
-
for (let r = 1; r <= e.length; r++) {
|
|
70
|
-
let i = n[0];
|
|
71
|
-
n[0] = r;
|
|
72
|
-
for (let a = 1; a <= t.length; a++) {
|
|
73
|
-
let o = Math.min(n[a] + 1, n[a - 1] + 1, i + (e[r - 1] === t[a - 1] ? 0 : 1));
|
|
74
|
-
i = n[a], n[a] = o;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
return n[t.length];
|
|
78
|
-
}
|
|
79
|
-
//#endregion
|
|
80
|
-
export { i as validateGridConfig };
|
|
File without changes
|