@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/keyboard.ts
CHANGED
|
@@ -50,6 +50,9 @@ export function getKeyboardIntent(event: KeyboardEvent): GridKeyboardIntent {
|
|
|
50
50
|
// Tab → move right (Shift+Tab → left), wraps at row boundaries
|
|
51
51
|
// Delete → clear contents of the selected cells
|
|
52
52
|
// F2 / Space → start editing the active cell
|
|
53
|
+
// Ctrl+Enter acts on the row rather than moving: a group opens or closes,
|
|
54
|
+
// a row with a detail panel shows or hides it (see the activate branch).
|
|
55
|
+
if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) return 'activate'
|
|
53
56
|
if (event.key === 'Enter') return event.shiftKey ? 'moveUp' : 'moveDown'
|
|
54
57
|
if (event.key === 'Tab') return event.shiftKey ? 'tabPrev' : 'tabNext'
|
|
55
58
|
if (event.key === 'F2') return 'activate'
|
package/src/row-drag-touch.ts
CHANGED
|
@@ -23,7 +23,7 @@ export type TouchDragDeps = {
|
|
|
23
23
|
ctx: any;
|
|
24
24
|
managed: () => boolean;
|
|
25
25
|
originalAt: (rowIndex: number) => unknown;
|
|
26
|
-
commitDropOnRow: (rowIndex: number, side: "before" | "after") => void;
|
|
26
|
+
commitDropOnRow: (rowIndex: number, side: "before" | "after" | "into") => void;
|
|
27
27
|
clearIndicators: () => void;
|
|
28
28
|
cancelPendingLeave: () => void;
|
|
29
29
|
/** Publish the dragged row on the shared cross-grid bus. */
|
|
@@ -135,7 +135,7 @@ export function createTouchDrag(deps: TouchDragDeps): TouchDragHandle {
|
|
|
135
135
|
if (commit && ctx.rowDropIndex != null) {
|
|
136
136
|
deps.commitDropOnRow(
|
|
137
137
|
ctx.rowDropIndex as number,
|
|
138
|
-
(ctx.rowDropSide ?? "before") as "before" | "after",
|
|
138
|
+
(ctx.rowDropSide ?? "before") as "before" | "after" | "into",
|
|
139
139
|
);
|
|
140
140
|
} else {
|
|
141
141
|
deps.clearIndicators();
|
package/src/row-drag.test.ts
CHANGED
|
@@ -73,6 +73,57 @@ describe('createRowDrag - same-grid reorder', () => {
|
|
|
73
73
|
})
|
|
74
74
|
})
|
|
75
75
|
|
|
76
|
+
describe('createRowDrag - a custom drop (onRowDrop)', () => {
|
|
77
|
+
it('reports the drop with its side and leaves the data alone', () => {
|
|
78
|
+
const rows: Task[] = [
|
|
79
|
+
{ id: 1, title: 'a' },
|
|
80
|
+
{ id: 2, title: 'b' },
|
|
81
|
+
{ id: 3, title: 'c' },
|
|
82
|
+
]
|
|
83
|
+
const onRowDrop = vi.fn()
|
|
84
|
+
const ctx = makeCtx(rows, { ...P, onRowDrop })
|
|
85
|
+
const h = createRowDrag(ctx)
|
|
86
|
+
h.onRowDragStart(fakeEvent(), 0) // drag 'a'
|
|
87
|
+
h.onRowDragOver(fakeEvent(35, 0, 40), 2) // low on c: after
|
|
88
|
+
expect(ctx.rowDropSide).toBe('after')
|
|
89
|
+
h.onRowDrop(fakeEvent(), 2)
|
|
90
|
+
expect(onRowDrop).toHaveBeenCalledWith({ row: rows[0], target: rows[2], targetIndex: 2, side: 'after' })
|
|
91
|
+
expect((ctx.internalData as Task[]).map((r) => r.title)).toEqual(['a', 'b', 'c'])
|
|
92
|
+
// The empty space below the rows reports no target.
|
|
93
|
+
h.onRowDragStart(fakeEvent(), 1)
|
|
94
|
+
h.onRowsContainerDrop(fakeEvent())
|
|
95
|
+
expect(onRowDrop).toHaveBeenLastCalledWith({ row: rows[1], target: null, targetIndex: null, side: 'after' })
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('offers "into" over the middle of a group row, and only then', () => {
|
|
99
|
+
const rows: Task[] = [
|
|
100
|
+
{ id: 1, title: 'folder' },
|
|
101
|
+
{ id: 2, title: 'file' },
|
|
102
|
+
]
|
|
103
|
+
const ctx = makeCtx(rows, {
|
|
104
|
+
...P,
|
|
105
|
+
onRowDrop: vi.fn(),
|
|
106
|
+
serverGroup: { isGroup: (r: Task) => r.title === 'folder', level: () => 0, onToggle: () => {} },
|
|
107
|
+
})
|
|
108
|
+
const h = createRowDrag(ctx)
|
|
109
|
+
h.onRowDragStart(fakeEvent(), 1)
|
|
110
|
+
h.onRowDragOver(fakeEvent(20, 0, 40), 0) // the middle of the folder
|
|
111
|
+
expect(ctx.rowDropSide).toBe('into')
|
|
112
|
+
h.onRowDragOver(fakeEvent(3, 0, 40), 0) // its top edge
|
|
113
|
+
expect(ctx.rowDropSide).toBe('before')
|
|
114
|
+
h.onRowDragOver(fakeEvent(19, 0, 40), 1) // the middle of a file: no nesting, just the halves
|
|
115
|
+
expect(ctx.rowDropSide).toBe('before')
|
|
116
|
+
h.onRowDragOver(fakeEvent(21, 0, 40), 1)
|
|
117
|
+
expect(ctx.rowDropSide).toBe('after')
|
|
118
|
+
// Without a custom handler the middle band does not exist.
|
|
119
|
+
const managed = makeCtx(rows, { ...P, serverGroup: ctx.props.serverGroup })
|
|
120
|
+
const hm = createRowDrag(managed)
|
|
121
|
+
hm.onRowDragStart(fakeEvent(), 1)
|
|
122
|
+
hm.onRowDragOver(fakeEvent(20, 0, 40), 0)
|
|
123
|
+
expect(managed.rowDropSide).toBe('after')
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
76
127
|
describe('createRowDrag - grid-to-grid', () => {
|
|
77
128
|
it('moves a row from source into target when groups match', () => {
|
|
78
129
|
const a = makeCtx([{ id: 1, title: 'a1' }, { id: 2, title: 'a2' }], { ...P, rowDragGroup: 'g' })
|
package/src/row-drag.ts
CHANGED
|
@@ -116,7 +116,25 @@ export function createRowDrag<TFeatures, TData>(ctx: any) {
|
|
|
116
116
|
if (e.dataTransfer) e.dataTransfer.dropEffect = "move";
|
|
117
117
|
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
|
118
118
|
ctx.rowDropIndex = rowIndex;
|
|
119
|
-
ctx.rowDropSide =
|
|
119
|
+
ctx.rowDropSide = sideAt(rowIndex, rect, e.clientY);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Which edge the pointer is on. A custom drop handler also gets the middle
|
|
124
|
+
* of a group or tree row as "into" (the row becomes its child); the
|
|
125
|
+
* managed reorder only knows before and after.
|
|
126
|
+
*/
|
|
127
|
+
function sideAt(rowIndex: number, rect: DOMRect, y: number): "before" | "after" | "into" {
|
|
128
|
+
const rel = (y - rect.top) / Math.max(rect.height, 1);
|
|
129
|
+
if (ctx.props.onRowDrop && rel >= 0.25 && rel <= 0.75 && canNest(rowIndex)) return "into";
|
|
130
|
+
return rel < 0.5 ? "before" : "after";
|
|
131
|
+
}
|
|
132
|
+
function canNest(rowIndex: number): boolean {
|
|
133
|
+
const row = ctx.allRows[rowIndex];
|
|
134
|
+
if (!row) return false;
|
|
135
|
+
const sg = ctx.props.serverGroup;
|
|
136
|
+
if (sg) return !!sg.isGroup(row.original);
|
|
137
|
+
return typeof row.getCanExpand === "function" && row.getCanExpand();
|
|
120
138
|
}
|
|
121
139
|
|
|
122
140
|
function onRowDragLeave(rowIndex: number) {
|
|
@@ -147,7 +165,7 @@ export function createRowDrag<TFeatures, TData>(ctx: any) {
|
|
|
147
165
|
* both land here, so the reorder, the cross-grid hand-off and the
|
|
148
166
|
* `onRowDragEnd` payload have exactly one implementation.
|
|
149
167
|
*/
|
|
150
|
-
function commitDropOnRow(rowIndex: number, side: "before" | "after") {
|
|
168
|
+
function commitDropOnRow(rowIndex: number, side: "before" | "after" | "into") {
|
|
151
169
|
if (!canAccept() || !bus) return;
|
|
152
170
|
const dragged = bus.row;
|
|
153
171
|
const target = originalAt(rowIndex);
|
|
@@ -155,6 +173,15 @@ export function createRowDrag<TFeatures, TData>(ctx: any) {
|
|
|
155
173
|
const sameGrid = sourceGridId === gridId;
|
|
156
174
|
clearIndicators();
|
|
157
175
|
|
|
176
|
+
// A custom handler owns the drop: the grid reports it and leaves its
|
|
177
|
+
// data alone (a row model's rows are not the grid's to splice).
|
|
178
|
+
if (sameGrid && ctx.props.onRowDrop) {
|
|
179
|
+
bus = null;
|
|
180
|
+
ctx.props.onRowDrop({ row: dragged, target, targetIndex: rowIndex, side });
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (side === "into") side = "after";
|
|
184
|
+
|
|
158
185
|
const data = (ctx.internalData as unknown[]).slice();
|
|
159
186
|
if (sameGrid) {
|
|
160
187
|
const from = data.indexOf(dragged);
|
|
@@ -198,6 +225,12 @@ export function createRowDrag<TFeatures, TData>(ctx: any) {
|
|
|
198
225
|
const sameGrid = sourceGridId === gridId;
|
|
199
226
|
clearIndicators();
|
|
200
227
|
|
|
228
|
+
if (sameGrid && ctx.props.onRowDrop) {
|
|
229
|
+
bus = null;
|
|
230
|
+
ctx.props.onRowDrop({ row: dragged, target: null, targetIndex: null, side: "after" });
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
201
234
|
const data = (ctx.internalData as unknown[]).slice();
|
|
202
235
|
if (sameGrid) {
|
|
203
236
|
const from = data.indexOf(dragged);
|
package/src/row-model.ts
CHANGED
|
@@ -84,6 +84,12 @@ export type GridRowModel<TData> = {
|
|
|
84
84
|
level: (row: TData) => number
|
|
85
85
|
expanded?: (row: TData) => boolean
|
|
86
86
|
onToggle: (row: TData) => void
|
|
87
|
+
/** Open or close the detail panel under a leaf. Ctrl+Enter on a row calls it when `renderDetailRow` is set, and so does the `showDetailToggle` chevron. */
|
|
88
|
+
toggleDetail?: (row: TData) => void
|
|
89
|
+
/** Whether the detail under a leaf is open: the chevron's direction. */
|
|
90
|
+
detailOpen?: (row: TData) => boolean
|
|
91
|
+
/** Whether a row can open a detail; the chevron is left out where this is false. */
|
|
92
|
+
hasDetail?: (row: TData) => boolean
|
|
87
93
|
}
|
|
88
94
|
/** Selection the grid does not own - see the `rowSelectionModel` prop. */
|
|
89
95
|
selection?: {
|
package/src/row-resize.ts
CHANGED
|
@@ -69,6 +69,20 @@ export function rowResize(node: HTMLElement, opts: RowResizeOptions) {
|
|
|
69
69
|
/** The last press on a strip, for telling a double-click from two drags. */
|
|
70
70
|
let lastPress: { rowIndex: number; at: number } | null = null
|
|
71
71
|
|
|
72
|
+
/**
|
|
73
|
+
* A focusable `role="separator"` is a splitter, and a splitter carries its
|
|
74
|
+
* position: axe's `aria-required-attr` fails one without `aria-valuenow`,
|
|
75
|
+
* and a screen reader reading the strip says nothing useful without it. The
|
|
76
|
+
* value is the row's height in pixels, between the action's min and max.
|
|
77
|
+
*/
|
|
78
|
+
function setStripValue(strip: HTMLElement, height: number): void {
|
|
79
|
+
const min = current.min ?? 20
|
|
80
|
+
const max = current.max ?? 320
|
|
81
|
+
strip.setAttribute('aria-valuemin', String(min))
|
|
82
|
+
strip.setAttribute('aria-valuemax', String(max))
|
|
83
|
+
strip.setAttribute('aria-valuenow', String(Math.round(Math.max(min, Math.min(max, height || min)))))
|
|
84
|
+
}
|
|
85
|
+
|
|
72
86
|
function rowIndexOf(tr: HTMLTableRowElement): number {
|
|
73
87
|
const dataRow = tr.querySelector<HTMLElement>('[data-svgrid-row]')
|
|
74
88
|
const val = dataRow?.dataset.svgridRow
|
|
@@ -81,6 +95,7 @@ export function rowResize(node: HTMLElement, opts: RowResizeOptions) {
|
|
|
81
95
|
const max = current.max ?? 320
|
|
82
96
|
const next = Math.max(min, Math.min(max, drag.startHeight + (e.clientY - drag.startY)))
|
|
83
97
|
drag.trEl.style.height = `${Math.round(next)}px`
|
|
98
|
+
setStripValue(drag.strip, next)
|
|
84
99
|
current.onResizeMove?.(drag.rowIndex, Math.round(next))
|
|
85
100
|
}
|
|
86
101
|
function onPointerUp(e: PointerEvent) {
|
|
@@ -124,6 +139,7 @@ export function rowResize(node: HTMLElement, opts: RowResizeOptions) {
|
|
|
124
139
|
Math.max(min, Math.min(max, tr.getBoundingClientRect().height + delta)),
|
|
125
140
|
)
|
|
126
141
|
tr.style.height = `${next}px`
|
|
142
|
+
setStripValue(t, next)
|
|
127
143
|
current.onResizeMove?.(rowIndex, next)
|
|
128
144
|
current.onResize(rowIndex, next)
|
|
129
145
|
}
|
|
@@ -189,7 +205,13 @@ export function rowResize(node: HTMLElement, opts: RowResizeOptions) {
|
|
|
189
205
|
? tr.querySelector<HTMLTableCellElement>('.sv-grid-cell')
|
|
190
206
|
: null)
|
|
191
207
|
if (!gutter) continue
|
|
192
|
-
|
|
208
|
+
const already = gutter.querySelector<HTMLElement>(`:scope > .${STRIP_CLASS}`)
|
|
209
|
+
if (already) {
|
|
210
|
+
// The row may have been resized from outside the action (a saved
|
|
211
|
+
// height, a double-click autofit); keep the separator's value honest.
|
|
212
|
+
setStripValue(already, tr.getBoundingClientRect().height)
|
|
213
|
+
continue
|
|
214
|
+
}
|
|
193
215
|
// Anchor the strip to the gutter cell + allow it to overflow
|
|
194
216
|
// downward into the row gap so the hit-area straddles the
|
|
195
217
|
// row boundary the way Excel does.
|
|
@@ -203,6 +225,7 @@ export function rowResize(node: HTMLElement, opts: RowResizeOptions) {
|
|
|
203
225
|
// A separator used as an interactive splitter has to be focusable so
|
|
204
226
|
// keyboard-only users can reach it and drive `onKeyDown` (#79).
|
|
205
227
|
strip.tabIndex = 0
|
|
228
|
+
setStripValue(strip, tr.getBoundingClientRect().height)
|
|
206
229
|
// Inline the geometry + pointer-events so the strip works even
|
|
207
230
|
// before SvGrid.css is parsed; the hover tint still comes from
|
|
208
231
|
// the stylesheet. We keep the strip fully inside the gutter
|
package/src/selection.ts
CHANGED
|
@@ -548,7 +548,7 @@ export function createSelection<
|
|
|
548
548
|
ctx.stopEdgeScroll();
|
|
549
549
|
// Also commit a fill-handle drag if one was in progress - the user
|
|
550
550
|
// released the mouse, time to apply the pattern.
|
|
551
|
-
if (ctx.fillDrag) ctx.onFillPointerUp();
|
|
551
|
+
if (ctx.fillDrag) ctx.onFillPointerUp(event);
|
|
552
552
|
if (ctx.moveDrag) ctx.onMovePointerUp(event);
|
|
553
553
|
}
|
|
554
554
|
|
|
@@ -294,6 +294,33 @@ describe('createBlockCache', () => {
|
|
|
294
294
|
cache.dispose()
|
|
295
295
|
})
|
|
296
296
|
|
|
297
|
+
it('retries one failed block by a row index, leaving the other failed blocks alone', async () => {
|
|
298
|
+
const server = scriptedFetch({ failBlocks: new Set([1, 3]) })
|
|
299
|
+
const cache = createBlockCache<Row>({
|
|
300
|
+
blockSize: 100,
|
|
301
|
+
fetch: server.fetch,
|
|
302
|
+
onChange: () => {},
|
|
303
|
+
schedule: sync,
|
|
304
|
+
})
|
|
305
|
+
cache.setViewport(100, 399)
|
|
306
|
+
await server.flush()
|
|
307
|
+
expect(cache.getCacheState().filter((b) => b.status === 'failed').map((b) => b.blockIndex)).toEqual([1, 3])
|
|
308
|
+
|
|
309
|
+
server.failBlocks.clear()
|
|
310
|
+
// The Retry on a row in block 3: block 1 stays failed, one request goes out.
|
|
311
|
+
const before = server.startsRequested().length
|
|
312
|
+
cache.retryFailedAt(350)
|
|
313
|
+
await server.flush()
|
|
314
|
+
expect(server.startsRequested().length).toBe(before + 1)
|
|
315
|
+
expect(cache.getCacheState().find((b) => b.blockIndex === 3)!.status).toBe('loaded')
|
|
316
|
+
expect(cache.getCacheState().find((b) => b.blockIndex === 1)!.status).toBe('failed')
|
|
317
|
+
// A row whose block is fine is a no-op.
|
|
318
|
+
cache.retryFailedAt(250)
|
|
319
|
+
await server.flush()
|
|
320
|
+
expect(server.startsRequested().length).toBe(before + 1)
|
|
321
|
+
cache.dispose()
|
|
322
|
+
})
|
|
323
|
+
|
|
297
324
|
it('keeps the first block at its skeleton length when it fails with no count', async () => {
|
|
298
325
|
// Nothing is known about the list yet, so the failed rows are the few
|
|
299
326
|
// the level showed while loading, not a hundred and one of them.
|
|
@@ -177,6 +177,11 @@ export type BlockCache<TData> = {
|
|
|
177
177
|
lastRowKnown(): boolean
|
|
178
178
|
/** Re-fetch the blocks that failed. */
|
|
179
179
|
retryFailed(): void
|
|
180
|
+
/**
|
|
181
|
+
* Re-fetch the failed block holding `rowIndex`, and only that one; the
|
|
182
|
+
* Retry on a failed row. A row whose block has not failed is a no-op.
|
|
183
|
+
*/
|
|
184
|
+
retryFailedAt(rowIndex: number): void
|
|
180
185
|
/**
|
|
181
186
|
* Re-fetch the loaded blocks in place, keeping the row count and scroll
|
|
182
187
|
* position. What you want after a mutation lands on the server.
|
|
@@ -502,6 +507,14 @@ export function createBlockCache<TData>(options: BlockCacheOptions<TData>): Bloc
|
|
|
502
507
|
if (any) emit()
|
|
503
508
|
},
|
|
504
509
|
|
|
510
|
+
retryFailedAt(rowIndex) {
|
|
511
|
+
const index = blockOf(Math.max(0, rowIndex))
|
|
512
|
+
if (blocks.get(index)?.status !== 'failed') return
|
|
513
|
+
blocks.delete(index)
|
|
514
|
+
ensureBlock(index)
|
|
515
|
+
emit()
|
|
516
|
+
},
|
|
517
|
+
|
|
505
518
|
refresh() {
|
|
506
519
|
// Keep the count and the scroll position: drop the DATA, re-request the
|
|
507
520
|
// same blocks. The user sees skeletons where they were, not a jump home.
|
|
@@ -167,6 +167,68 @@ describe('createServerDataSource in infinite mode', () => {
|
|
|
167
167
|
ctl.dispose()
|
|
168
168
|
})
|
|
169
169
|
|
|
170
|
+
it('retries only the block under the row the grid asks about', async () => {
|
|
171
|
+
let failing = true
|
|
172
|
+
const starts: number[] = []
|
|
173
|
+
const source: ServerDataSource<Row> = {
|
|
174
|
+
async getRows(req) {
|
|
175
|
+
starts.push(req.startRow)
|
|
176
|
+
if (failing && req.startRow >= 100) throw new Error('backend down')
|
|
177
|
+
const rows: Row[] = []
|
|
178
|
+
for (let i = req.startRow; i < Math.min(req.endRow, 1000); i += 1) {
|
|
179
|
+
rows.push({ id: i, name: `Row ${i}` })
|
|
180
|
+
}
|
|
181
|
+
return { rows, rowCount: 1000 }
|
|
182
|
+
},
|
|
183
|
+
}
|
|
184
|
+
let view!: ServerState<Row>
|
|
185
|
+
const ctl = createServerDataSource<Row>(source, {
|
|
186
|
+
mode: 'infinite',
|
|
187
|
+
blockSize: 100,
|
|
188
|
+
onChange: (s) => (view = s),
|
|
189
|
+
})
|
|
190
|
+
ctl.setViewport(100, 299)
|
|
191
|
+
await settle()
|
|
192
|
+
expect(view.failedBlocks).toEqual([1, 2])
|
|
193
|
+
|
|
194
|
+
failing = false
|
|
195
|
+
const before = starts.length
|
|
196
|
+
// The GridRowModel seam the failed row's Retry button goes through.
|
|
197
|
+
ctl.retryRow!(view.rows[250]!, 250)
|
|
198
|
+
await settle()
|
|
199
|
+
expect(starts.slice(before)).toEqual([200])
|
|
200
|
+
expect(view.failedBlocks).toEqual([1])
|
|
201
|
+
ctl.dispose()
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('hands each block a signal that purge, a sort change and dispose abort', async () => {
|
|
205
|
+
const signals: AbortSignal[] = []
|
|
206
|
+
const source: ServerDataSource<Row> = {
|
|
207
|
+
async getRows(req) {
|
|
208
|
+
signals.push(req.signal!)
|
|
209
|
+
// A server that never answers: only the abort can end these.
|
|
210
|
+
return new Promise(() => {})
|
|
211
|
+
},
|
|
212
|
+
}
|
|
213
|
+
const ctl = createServerDataSource<Row>(source, { mode: 'infinite', blockSize: 100, onChange: () => {} })
|
|
214
|
+
ctl.setViewport(0, 20)
|
|
215
|
+
await settle()
|
|
216
|
+
expect(signals).toHaveLength(1)
|
|
217
|
+
expect(signals[0]!.aborted).toBe(false)
|
|
218
|
+
|
|
219
|
+
ctl.purge()
|
|
220
|
+
await settle()
|
|
221
|
+
expect(signals[0]!.aborted).toBe(true)
|
|
222
|
+
expect(signals).toHaveLength(2)
|
|
223
|
+
|
|
224
|
+
ctl.setSort([{ id: 'id', desc: true }])
|
|
225
|
+
await settle()
|
|
226
|
+
expect(signals[1]!.aborted).toBe(true)
|
|
227
|
+
|
|
228
|
+
ctl.dispose()
|
|
229
|
+
expect(signals.at(-1)!.aborted).toBe(true)
|
|
230
|
+
})
|
|
231
|
+
|
|
170
232
|
it('refresh keeps the count and the scroll position, purge does not', async () => {
|
|
171
233
|
const { source, starts } = tableOf(1000)
|
|
172
234
|
let view!: ServerState<Row>
|
|
@@ -86,6 +86,25 @@ describe('createServerDataSource', () => {
|
|
|
86
86
|
expect(ctl.getState().rows[0]).toEqual({ id: 30 })
|
|
87
87
|
})
|
|
88
88
|
|
|
89
|
+
it('aborts the page fetch a newer one supersedes, and the last one on dispose', async () => {
|
|
90
|
+
const signals: AbortSignal[] = []
|
|
91
|
+
const src: ServerDataSource<Row> = {
|
|
92
|
+
getRows: async (req) => {
|
|
93
|
+
signals.push(req.signal!)
|
|
94
|
+
return new Promise(() => {})
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
const ctl = createServerDataSource(src, { pageSize: 10, onChange: () => {} })
|
|
98
|
+
ctl.refresh()
|
|
99
|
+
ctl.setPage(3)
|
|
100
|
+
await new Promise((r) => setTimeout(r, 5))
|
|
101
|
+
expect(signals).toHaveLength(2)
|
|
102
|
+
expect(signals[0]!.aborted).toBe(true)
|
|
103
|
+
expect(signals[1]!.aborted).toBe(false)
|
|
104
|
+
ctl.dispose()
|
|
105
|
+
expect(signals[1]!.aborted).toBe(true)
|
|
106
|
+
})
|
|
107
|
+
|
|
89
108
|
it('dispose stops further updates', async () => {
|
|
90
109
|
const src = makeSource(50, 20)
|
|
91
110
|
const onChange = vi.fn()
|
|
@@ -63,8 +63,17 @@ export type ServerFilterModel = {
|
|
|
63
63
|
expression?: GridPredicateExpr
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
/**
|
|
67
|
-
export type
|
|
66
|
+
/** The aggregate functions every backend helper understands. */
|
|
67
|
+
export type ServerAggFn = 'sum' | 'avg' | 'min' | 'max' | 'count'
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* One value column to roll up per group. `fn` is one of the five built-ins
|
|
71
|
+
* or any name your backend knows (`median`, `countDistinct`, a domain
|
|
72
|
+
* aggregate): the request carries it untouched. The SvelteKit planner emits
|
|
73
|
+
* only the built-ins plus the names it was told to allow, since `fn` ends
|
|
74
|
+
* up in SQL.
|
|
75
|
+
*/
|
|
76
|
+
export type ServerAggregation = { col: string; fn: ServerAggFn | (string & {}) }
|
|
68
77
|
|
|
69
78
|
/**
|
|
70
79
|
* One range of rows as the grid asks for it. A flat request carries the
|
|
@@ -122,6 +131,16 @@ export type ServerRequest = {
|
|
|
122
131
|
* transport posts the whole request to your endpoint.
|
|
123
132
|
*/
|
|
124
133
|
context?: unknown
|
|
134
|
+
/**
|
|
135
|
+
* Aborted when the grid no longer wants the answer: the block was purged
|
|
136
|
+
* or evicted, the sort or filter changed, a group was collapsed, the
|
|
137
|
+
* controller was disposed. Hand it to `fetch` and a query the user has
|
|
138
|
+
* scrolled past is cancelled at the server instead of completing for
|
|
139
|
+
* nothing; a rejection carrying the abort is ignored by the grid. Not
|
|
140
|
+
* serialisable - the SvelteKit transport reads it and leaves it out of
|
|
141
|
+
* the body.
|
|
142
|
+
*/
|
|
143
|
+
signal?: AbortSignal
|
|
125
144
|
}
|
|
126
145
|
|
|
127
146
|
/**
|
|
@@ -236,6 +255,21 @@ export type ServerPlaceholderRow = {
|
|
|
236
255
|
readonly state: 'loading' | 'failed'
|
|
237
256
|
}
|
|
238
257
|
|
|
258
|
+
/**
|
|
259
|
+
* The detail panel under a leaf whose detail is open: master-detail on a
|
|
260
|
+
* server-side row model. The grid draws it through `renderDetailRow`
|
|
261
|
+
* (mark it with `isDetailRow`); `master` is the leaf it belongs to.
|
|
262
|
+
*/
|
|
263
|
+
export type ServerDetailRow<TData> = {
|
|
264
|
+
kind: 'detail'
|
|
265
|
+
id: string
|
|
266
|
+
level: number
|
|
267
|
+
route?: string[]
|
|
268
|
+
/** The leaf's id, as `getRowId` names it. */
|
|
269
|
+
masterId: string
|
|
270
|
+
master: TData
|
|
271
|
+
}
|
|
272
|
+
|
|
239
273
|
/**
|
|
240
274
|
* Every row a server row model can put on screen. Each carries an `id` and
|
|
241
275
|
* a `level`. The block-cached model adds {@link ServerPlaceholderRow} for
|
|
@@ -244,6 +278,7 @@ export type ServerPlaceholderRow = {
|
|
|
244
278
|
export type ServerDisplayRow<TData> =
|
|
245
279
|
| ServerGroupRow<TData>
|
|
246
280
|
| ServerLeafRow<TData>
|
|
281
|
+
| ServerDetailRow<TData>
|
|
247
282
|
| ServerMoreRow
|
|
248
283
|
| ServerFooterRow<TData>
|
|
249
284
|
| ServerSkeletonRow
|
|
@@ -523,6 +558,8 @@ export function createServerDataSource<TData>(
|
|
|
523
558
|
// Monotonic request id: only the latest fetch is allowed to land, so a slow
|
|
524
559
|
// response for an old sort/filter can't clobber a newer one.
|
|
525
560
|
let requestSeq = 0
|
|
561
|
+
/** The page fetch in flight; a newer one, or dispose, aborts it. */
|
|
562
|
+
let pageAbort: AbortController | null = null
|
|
526
563
|
let disposed = false
|
|
527
564
|
// Once per controller: a misconfigured backend would otherwise log on every
|
|
528
565
|
// page, scroll and filter change.
|
|
@@ -559,7 +596,7 @@ export function createServerDataSource<TData>(
|
|
|
559
596
|
maxConcurrentRequests: options.maxConcurrentRequests,
|
|
560
597
|
blockLoadDebounceMs: options.blockLoadDebounceMs,
|
|
561
598
|
initialRowCount: options.initialRowCount,
|
|
562
|
-
fetch: async (startRow, endRow) => {
|
|
599
|
+
fetch: async (startRow, endRow, signal) => {
|
|
563
600
|
const result = await source.getRows({
|
|
564
601
|
startRow,
|
|
565
602
|
endRow,
|
|
@@ -572,6 +609,7 @@ export function createServerDataSource<TData>(
|
|
|
572
609
|
groupBy: [],
|
|
573
610
|
groupKeys: [],
|
|
574
611
|
aggregations: [],
|
|
612
|
+
signal,
|
|
575
613
|
})
|
|
576
614
|
noteExpressionApplied(result)
|
|
577
615
|
return { rows: result.rows, rowCount: result.rowCount }
|
|
@@ -617,6 +655,9 @@ export function createServerDataSource<TData>(
|
|
|
617
655
|
async function fetchPage() {
|
|
618
656
|
if (disposed) return
|
|
619
657
|
const id = ++requestSeq
|
|
658
|
+
pageAbort?.abort()
|
|
659
|
+
const controller = new AbortController()
|
|
660
|
+
pageAbort = controller
|
|
620
661
|
state.loading = true
|
|
621
662
|
state.error = null
|
|
622
663
|
emit()
|
|
@@ -634,6 +675,7 @@ export function createServerDataSource<TData>(
|
|
|
634
675
|
groupBy: [],
|
|
635
676
|
groupKeys: [],
|
|
636
677
|
aggregations: [],
|
|
678
|
+
signal: controller.signal,
|
|
637
679
|
})
|
|
638
680
|
if (disposed || id !== requestSeq) return // stale
|
|
639
681
|
state.rows = result.rows
|
|
@@ -815,7 +857,7 @@ export function createServerDataSource<TData>(
|
|
|
815
857
|
getRowId: options.getRowId ? (row: TData) => options.getRowId!(row) : undefined,
|
|
816
858
|
// Only infinite mode has unloaded rows to stand in for.
|
|
817
859
|
rowPlaceholder: infinite ? (row: TData) => rowPlaceholderState(row) : undefined,
|
|
818
|
-
retryRow: infinite ? () => cache?.
|
|
860
|
+
retryRow: infinite ? (_row: TData, rowIndex: number) => cache?.retryFailedAt(rowIndex) : undefined,
|
|
819
861
|
/**
|
|
820
862
|
* A getter, not a snapshot: the grid re-reads this after every change
|
|
821
863
|
* notification, and a frozen object would leave the pager on page 1
|
|
@@ -878,6 +920,8 @@ export function createServerDataSource<TData>(
|
|
|
878
920
|
getState: () => ({ ...state }),
|
|
879
921
|
dispose() {
|
|
880
922
|
disposed = true
|
|
923
|
+
pageAbort?.abort()
|
|
924
|
+
pageAbort = null
|
|
881
925
|
cache?.dispose()
|
|
882
926
|
cache = null
|
|
883
927
|
// An in-flight fetch's resolution short-circuits on `disposed`, so it
|
package/src/server.ts
CHANGED
|
@@ -19,6 +19,7 @@ export {
|
|
|
19
19
|
type ServerState,
|
|
20
20
|
type ServerSortModel,
|
|
21
21
|
type ServerFilterModel,
|
|
22
|
+
type ServerAggFn,
|
|
22
23
|
type ServerAggregation,
|
|
23
24
|
type ServerGroupRow,
|
|
24
25
|
type ServerLeafRow,
|
|
@@ -28,6 +29,7 @@ export {
|
|
|
28
29
|
type ServerGrandTotalRow,
|
|
29
30
|
type ServerPlaceholderRow,
|
|
30
31
|
type ServerDisplayRow,
|
|
32
|
+
type ServerDetailRow,
|
|
31
33
|
} from './server-data-source'
|
|
32
34
|
export {
|
|
33
35
|
createBlockCache,
|
package/src/sparkline.test.ts
CHANGED
|
@@ -59,6 +59,13 @@ describe('buildSparkline', () => {
|
|
|
59
59
|
expect(g.bars[0]!.h).toBeCloseTo(g.bars[1]!.h)
|
|
60
60
|
})
|
|
61
61
|
|
|
62
|
+
it('leaves a blank gap for a zero, marking only wins and losses', () => {
|
|
63
|
+
const g = buildSparkline([1, 0, -1, 0, 1], { type: 'winloss', height: 20 })!
|
|
64
|
+
// Two wins and a loss draw; the two zeros are gaps, as Excel draws them.
|
|
65
|
+
expect(g.bars).toHaveLength(3)
|
|
66
|
+
expect(g.bars.map((b) => b.negative)).toEqual([false, true, false])
|
|
67
|
+
})
|
|
68
|
+
|
|
62
69
|
it('honors a fixed min/max scale', () => {
|
|
63
70
|
const a = buildSparkline([5, 5, 5], { type: 'line', min: 0, max: 10 })!
|
|
64
71
|
// flat series on a 0..10 scale sits at the vertical middle, not the top
|
package/src/sparkline.ts
CHANGED
|
@@ -102,11 +102,13 @@ export function buildSparkline(
|
|
|
102
102
|
const barW = Math.max(1, slot - gap)
|
|
103
103
|
const mid = height / 2
|
|
104
104
|
const h = innerH / 2
|
|
105
|
-
const bars: SparklineBar[] =
|
|
105
|
+
const bars: SparklineBar[] = []
|
|
106
|
+
values.forEach((v, i) => {
|
|
106
107
|
const x = pad + i * slot + (slot - barW) / 2
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
108
|
+
// A win/loss sparkline marks a win up and a loss down; a zero is a
|
|
109
|
+
// blank gap, no mark at all, the way Excel draws one.
|
|
110
|
+
if (v > 0) bars.push({ x, y: mid - h, w: barW, h, negative: false })
|
|
111
|
+
else if (v < 0) bars.push({ x, y: mid, w: barW, h, negative: true })
|
|
110
112
|
})
|
|
111
113
|
return { ...base, linePath: '', areaPath: '', bars, lastPoint: null }
|
|
112
114
|
}
|
|
@@ -146,6 +146,14 @@ export type SvGridApi<
|
|
|
146
146
|
TFeatures extends TableFeatures,
|
|
147
147
|
TData extends RowData,
|
|
148
148
|
> = {
|
|
149
|
+
// ----- DOM -----
|
|
150
|
+
/**
|
|
151
|
+
* The grid's root element (`.sv-grid-root`), or null before mount and
|
|
152
|
+
* after unmount. For measuring, focusing, or reading the theme the grid
|
|
153
|
+
* is drawn with - the PDF export takes its colours from here.
|
|
154
|
+
*/
|
|
155
|
+
getElement(): HTMLElement | null
|
|
156
|
+
|
|
149
157
|
// ----- Cells -----
|
|
150
158
|
/** Read a cell value from the underlying data at `rowIndex`. */
|
|
151
159
|
getCellValue(rowIndex: number, columnId: string): unknown
|
|
@@ -430,6 +438,16 @@ export type SvGridApi<
|
|
|
430
438
|
/** Snapshot of the current data array (pre-pipeline). */
|
|
431
439
|
getData(): ReadonlyArray<TData>
|
|
432
440
|
|
|
441
|
+
/**
|
|
442
|
+
* The stable id the grid keys a row by: what the `getRowId` prop
|
|
443
|
+
* computed, or the row's position when no `getRowId` is set. Pass a
|
|
444
|
+
* display index (the space `getActiveCell`, `scrollToRow` and the cell
|
|
445
|
+
* methods use) or a row object; returns null for an unknown index or a
|
|
446
|
+
* row that is not in the data. Pair with `selectRows`, `toggleRowSelected`
|
|
447
|
+
* or an `applyTransaction` remove, all of which take these ids.
|
|
448
|
+
*/
|
|
449
|
+
getRowId(row: number | TData): string | null
|
|
450
|
+
|
|
433
451
|
/**
|
|
434
452
|
* Snapshot of every column the grid currently knows about, in visual
|
|
435
453
|
* order, with the human-readable header label. Use this when exporting
|
|
@@ -580,6 +598,20 @@ export type SvGridApi<
|
|
|
580
598
|
canRedo(): boolean
|
|
581
599
|
/** Wipe both stacks (e.g. after a server save commits the buffer). */
|
|
582
600
|
clearHistory(): void
|
|
601
|
+
/**
|
|
602
|
+
* Mark every step recorded from now on with `tag`, until the next call;
|
|
603
|
+
* `undefined` stops the marking. The grid never reads the tag. It is for a
|
|
604
|
+
* consumer that shows one of several data sets through the grid (a
|
|
605
|
+
* workbook's sheets, say) and has to know which one the next Ctrl+Z would
|
|
606
|
+
* change before it lets the grid apply it: it tags the steps with the data
|
|
607
|
+
* set they were made on and asks `peekUndo` first.
|
|
608
|
+
*/
|
|
609
|
+
setHistoryTag(tag: unknown): void
|
|
610
|
+
/** The step the next `undo()` would apply, or null when there is none. A
|
|
611
|
+
* multi-cell action carries one tag, so it reports as one. */
|
|
612
|
+
peekUndo(): { tag: unknown } | null
|
|
613
|
+
/** The step the next `redo()` would apply, or null when there is none. */
|
|
614
|
+
peekRedo(): { tag: unknown } | null
|
|
583
615
|
|
|
584
616
|
// ----- Keyboard commands -----
|
|
585
617
|
/**
|
package/src/svgrid.api.test.ts
CHANGED
|
@@ -104,6 +104,27 @@ describe('SvGridApi - getData / getDisplayedRows', () => {
|
|
|
104
104
|
}
|
|
105
105
|
})
|
|
106
106
|
|
|
107
|
+
it('getRowId resolves a display index and a row object, and returns null for the unknown', async () => {
|
|
108
|
+
const { api, destroy } = await mountGrid()
|
|
109
|
+
try {
|
|
110
|
+
// No getRowId prop: the id is the row's index as a string.
|
|
111
|
+
expect(api.getRowId(0)).toBe('0')
|
|
112
|
+
expect(api.getRowId(4)).toBe('4')
|
|
113
|
+
expect(api.getRowId(99)).toBeNull()
|
|
114
|
+
const third = api.getData()[2]!
|
|
115
|
+
expect(api.getRowId(third)).toBe('2')
|
|
116
|
+
expect(api.getRowId({ id: 999, name: 'x', team: 'x', salary: 0 })).toBeNull()
|
|
117
|
+
// After a sort the display index follows the new order.
|
|
118
|
+
api.setSort('salary', 'desc')
|
|
119
|
+
await Promise.resolve()
|
|
120
|
+
const topId = api.getRowId(0)
|
|
121
|
+
expect(topId).not.toBeNull()
|
|
122
|
+
expect(api.getData()[Number(topId)]!.salary).toBe(Math.max(...sampleRows.map((r) => r.salary)))
|
|
123
|
+
} finally {
|
|
124
|
+
destroy()
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
|
|
107
128
|
it('getDisplayedRows returns the post-pipeline visible rows', async () => {
|
|
108
129
|
const { api, destroy } = await mountGrid()
|
|
109
130
|
try {
|