@svgrid/grid 3.0.4 → 3.0.6
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 +32 -11
- package/dist/GridMenus.svelte +34 -4
- package/dist/SvGrid.controller.svelte.d.ts +33 -5
- package/dist/SvGrid.controller.svelte.js +397 -58
- package/dist/SvGrid.css +200 -2
- package/dist/SvGrid.svelte +350 -40
- package/dist/SvGrid.types.d.ts +433 -7
- 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 +92 -37
- package/dist/cdn/{GridMenus-BL7ZgQvU.js → GridMenus-C-NtjlrJ.js} +155 -146
- package/dist/cdn/GridMenus-TssHej3q.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-BlyCSp0q.js} +1 -1
- package/dist/cdn/{SvGridCellEditor-D_0q4xAu.js → SvGridCellEditor-ClRRA9lW.js} +1 -1
- package/dist/cdn/{SvGridChart-DzLkSwCH.js → SvGridChart-B4EV8U6t.js} +4 -4
- package/dist/cdn/{SvGridChart-C3EWAZaM.js → SvGridChart-DsmwTyZF.js} +4 -4
- package/dist/cdn/{SvGridChartBuilder-CfII62sZ.js → SvGridChartBuilder-cHloHobN.js} +4 -4
- package/dist/cdn/{SvGridChartBuilder-BE2T1ykB.js → SvGridChartBuilder-vOl7C0Za.js} +4 -4
- package/dist/cdn/{SvGridChartPanel-xf4sWVTo.js → SvGridChartPanel-AcqF-Ll5.js} +5 -5
- package/dist/cdn/{SvGridChartPanel-CCX5_Wgd.js → SvGridChartPanel-biMtSBPA.js} +5 -5
- package/dist/cdn/{SvGridChartView-DYwabQWj.js → SvGridChartView-Dx_GmZuL.js} +2 -2
- package/dist/cdn/{SvGridChartView-CfuXmY5I.js → SvGridChartView-I3rDHXAq.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-DVVKOSI9.js} +1 -1
- package/dist/cdn/{chart-Dz7SqMXH.js → chart-DEZxwC0i.js} +1 -1
- package/dist/cdn/{chart-panel-messages-CUbf2R4i.js → chart-panel-messages-D12XcTZJ.js} +1 -1
- package/dist/cdn/{chart-panel-messages-CmNrMdsr.js → chart-panel-messages-DguS7gC5.js} +1 -1
- package/dist/cdn/{chart-summary-BJW_tg_X.js → chart-summary-CMAXBGY-.js} +1 -1
- package/dist/cdn/{chart-trend-CaN9mDEV.js → chart-trend-BkEp61Gj.js} +1 -1
- package/dist/cdn/{dismissable-DAHetSNk.js → dismissable-Brc4EZU1.js} +5 -1
- package/dist/cdn/{export-format-CUDLy2yn.js → export-format-YzdwBww2.js} +1 -1
- package/dist/cdn/{row-resize-niQCp040.js → row-resize-DtOS1KG6.js} +41 -32
- package/dist/cdn/server-block-cache-DIqb3VZn.js +241 -0
- package/dist/cdn/{src-V1uu8iE9.js → src-CgoeSM8k.js} +5339 -4753
- package/dist/cdn/{src-BKhZ6eXd.js → src-DfwxB1TH.js} +9817 -9231
- package/dist/cdn/svgrid.js +28 -27
- package/dist/cdn/svgrid.svelte-external.js +28 -27
- package/dist/cdn/validate-AZoD8BoR.js +80 -0
- package/dist/cell-formatting.js +18 -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 +401 -38
- package/dist/editing.js +34 -0
- package/dist/gantt-view.svelte.d.ts +24 -0
- package/dist/gantt-view.svelte.js +13 -0
- package/dist/grid-icons.d.ts +2 -2
- package/dist/grid-icons.js +2 -2
- package/dist/grid-messages.d.ts +10 -0
- package/dist/grid-messages.js +9 -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 +5 -4
- package/dist/index.js +3 -3
- 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 +139 -0
- package/dist/row-model.js +16 -0
- package/dist/row-resize.js +22 -1
- package/dist/selection.d.ts +2 -1
- package/dist/selection.js +31 -3
- package/dist/server-block-cache.d.ts +219 -0
- package/dist/server-block-cache.js +539 -0
- package/dist/server-data-source.d.ts +274 -8
- package/dist/server-data-source.js +229 -20
- package/dist/server.d.ts +13 -0
- package/dist/server.js +13 -0
- package/dist/sparkline.js +7 -5
- package/dist/svgrid-wrapper.types.d.ts +33 -0
- package/dist/validate.d.ts +4 -0
- package/dist/validate.js +29 -3
- package/package.json +11 -1
- package/src/GridMenus.svelte +34 -4
- package/src/SvGrid.controller.svelte.ts +397 -61
- package/src/SvGrid.css +200 -2
- package/src/SvGrid.svelte +350 -40
- package/src/SvGrid.types.ts +453 -7
- 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 +96 -31
- package/src/cell-formatting.test.ts +17 -0
- package/src/cell-formatting.ts +18 -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.rowmodel-cache.test.ts +51 -0
- package/src/core.tick-repair.test.ts +287 -0
- package/src/core.ts +407 -38
- package/src/editing.test.ts +52 -0
- package/src/editing.ts +33 -0
- package/src/gantt-stub.test.svelte +38 -0
- package/src/gantt-view.svelte.ts +35 -0
- package/src/grid-icons.ts +2 -2
- package/src/grid-messages.test.ts +7 -0
- package/src/grid-messages.ts +22 -0
- package/src/headless.ts +24 -0
- package/src/history.test.ts +49 -1
- package/src/history.ts +48 -2
- package/src/icon-seam.test.ts +2 -4
- package/src/index.ts +38 -10
- 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 +152 -0
- package/src/row-resize.ts +24 -1
- package/src/selection.test.ts +3 -0
- package/src/selection.ts +33 -3
- package/src/server-block-cache.test.ts +672 -0
- package/src/server-block-cache.ts +690 -0
- package/src/server-data-source.infinite.test.ts +405 -0
- package/src/server-data-source.test.ts +19 -0
- package/src/server-data-source.ts +496 -29
- package/src/server.ts +49 -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.detail-rows.svelte.test.ts +187 -0
- package/src/svgrid.filter-menu-scroll.test.ts +9 -0
- package/src/svgrid.gantt-seam.test.ts +199 -0
- package/src/svgrid.live-update-paths.svelte.test.ts +184 -0
- package/src/svgrid.menu-scroll-close.test.ts +10 -0
- package/src/svgrid.row-model-prop.svelte.test.ts +352 -0
- package/src/svgrid.row-model-seam.svelte.test.ts +305 -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 +8 -5
- package/src/test-setup.ts +12 -0
- package/src/transaction.test.ts +58 -0
- package/src/validate.test.ts +29 -0
- package/src/validate.ts +34 -3
- package/dist/SvGroupCell.svelte +0 -141
- package/dist/SvGroupCell.svelte.d.ts +0 -49
- package/dist/SvRowGroupPanel.svelte +0 -186
- package/dist/SvRowGroupPanel.svelte.d.ts +0 -25
- package/dist/cdn/GridMenus-7kbpnnBW.js +0 -635
- package/dist/cdn/validate-_CDJzgIo.js +0 -75
- package/dist/server-group-model.d.ts +0 -98
- package/dist/server-group-model.js +0 -263
- package/src/SvGroupCell.svelte +0 -141
- package/src/SvRowGroupPanel.svelte +0 -186
- package/src/server-group-model.test.ts +0 -294
- package/src/server-group-model.ts +0 -370
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
import { createBlockCache, rowPlaceholderState, } from './server-block-cache.js';
|
|
2
|
+
import { toServerFilterColumns } from './row-model.js';
|
|
3
|
+
/**
|
|
4
|
+
* The free server row model over a `ServerDataSource`: one page at a time,
|
|
5
|
+
* or one block-cached list in `infinite` mode, with sort, filter, race
|
|
6
|
+
* safety and writes. The result is a `GridRowModel`, so
|
|
7
|
+
* `<SvGrid rowModel={ctl} />` wires every seam.
|
|
8
|
+
*/
|
|
1
9
|
export function createServerDataSource(source, options) {
|
|
2
10
|
const state = {
|
|
3
11
|
rows: [],
|
|
@@ -12,21 +20,113 @@ export function createServerDataSource(source, options) {
|
|
|
12
20
|
filterModel: {},
|
|
13
21
|
expressionUnapplied: false,
|
|
14
22
|
};
|
|
23
|
+
const infinite = options.mode === 'infinite';
|
|
24
|
+
// `onChange` is the one-callback API this controller shipped with;
|
|
25
|
+
// `subscribe` is the many-listener one `GridRowModel` needs. Both fire
|
|
26
|
+
// from `emit`, so a grid driven by `rowModel` and an app reading
|
|
27
|
+
// `onChange` stay in step.
|
|
28
|
+
const subscribers = new Set();
|
|
29
|
+
let cache = null;
|
|
15
30
|
// Monotonic request id: only the latest fetch is allowed to land, so a slow
|
|
16
31
|
// response for an old sort/filter can't clobber a newer one.
|
|
17
32
|
let requestSeq = 0;
|
|
33
|
+
/** The page fetch in flight; a newer one, or dispose, aborts it. */
|
|
34
|
+
let pageAbort = null;
|
|
18
35
|
let disposed = false;
|
|
19
36
|
// Once per controller: a misconfigured backend would otherwise log on every
|
|
20
37
|
// page, scroll and filter change.
|
|
21
38
|
let warnedExpressionUnapplied = false;
|
|
22
39
|
const emit = () => {
|
|
23
40
|
state.pageCount = Math.max(1, Math.ceil(state.total / state.pageSize));
|
|
24
|
-
options.onChange({ ...state });
|
|
41
|
+
options.onChange?.({ ...state });
|
|
42
|
+
for (const notify of subscribers)
|
|
43
|
+
notify();
|
|
25
44
|
};
|
|
45
|
+
/**
|
|
46
|
+
* Pull the block cache's view of the world into `state` and emit.
|
|
47
|
+
*
|
|
48
|
+
* `total` stays a plain number because the pager and the footer have
|
|
49
|
+
* always read it as one; while the end is undiscovered it holds the
|
|
50
|
+
* current optimistic length, which is exactly what the scrollbar needs.
|
|
51
|
+
* `rowCount` is the honest answer, null and all.
|
|
52
|
+
*/
|
|
53
|
+
function emitFromCache() {
|
|
54
|
+
if (!cache || disposed)
|
|
55
|
+
return;
|
|
56
|
+
const rows = cache.rows();
|
|
57
|
+
state.rows = rows;
|
|
58
|
+
state.rowCount = cache.rowCount();
|
|
59
|
+
state.lastRowKnown = cache.lastRowKnown();
|
|
60
|
+
state.total = cache.rowCount() ?? rows.length;
|
|
61
|
+
emit();
|
|
62
|
+
}
|
|
63
|
+
function buildCache() {
|
|
64
|
+
return createBlockCache({
|
|
65
|
+
blockSize: options.blockSize ?? 100,
|
|
66
|
+
maxBlocksInCache: options.maxBlocksInCache,
|
|
67
|
+
maxConcurrentRequests: options.maxConcurrentRequests,
|
|
68
|
+
blockLoadDebounceMs: options.blockLoadDebounceMs,
|
|
69
|
+
initialRowCount: options.initialRowCount,
|
|
70
|
+
fetch: async (startRow, endRow, signal) => {
|
|
71
|
+
const result = await source.getRows({
|
|
72
|
+
startRow,
|
|
73
|
+
endRow,
|
|
74
|
+
// A block is a page of its own size, so a backend that only knows
|
|
75
|
+
// how to page still works unchanged.
|
|
76
|
+
pageIndex: Math.floor(startRow / Math.max(1, endRow - startRow)),
|
|
77
|
+
pageSize: endRow - startRow,
|
|
78
|
+
sortModel: state.sortModel,
|
|
79
|
+
filterModel: state.filterModel,
|
|
80
|
+
groupBy: [],
|
|
81
|
+
groupKeys: [],
|
|
82
|
+
aggregations: [],
|
|
83
|
+
signal,
|
|
84
|
+
});
|
|
85
|
+
noteExpressionApplied(result);
|
|
86
|
+
return { rows: result.rows, rowCount: result.rowCount };
|
|
87
|
+
},
|
|
88
|
+
onChange: (s) => {
|
|
89
|
+
state.loading = s.loading;
|
|
90
|
+
state.failedBlocks = s.failedBlocks;
|
|
91
|
+
emitFromCache();
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
/** Reload from scratch: new sort, new filter, or an explicit purge. */
|
|
96
|
+
function resetCache() {
|
|
97
|
+
if (disposed)
|
|
98
|
+
return;
|
|
99
|
+
cache?.dispose();
|
|
100
|
+
cache = buildCache();
|
|
101
|
+
cache.setViewport(viewStart, viewEnd);
|
|
102
|
+
emitFromCache();
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* An expression was sent but the backend did not acknowledge applying it,
|
|
106
|
+
* so these rows are a SUPERSET of what was asked for. Say so instead of
|
|
107
|
+
* filtering here: filtering one page would turn "3 of 1,000,000 match"
|
|
108
|
+
* into a confident lie and make paging incoherent, since the next page
|
|
109
|
+
* would re-filter a different slice.
|
|
110
|
+
*/
|
|
111
|
+
function noteExpressionApplied(result) {
|
|
112
|
+
state.expressionUnapplied =
|
|
113
|
+
state.filterModel.expression != null && result.appliedExpression !== true;
|
|
114
|
+
if (!state.expressionUnapplied || warnedExpressionUnapplied)
|
|
115
|
+
return;
|
|
116
|
+
warnedExpressionUnapplied = true;
|
|
117
|
+
console.warn('[svgrid] The data source was sent filterModel.expression but did not ' +
|
|
118
|
+
'return `appliedExpression: true`, so the advanced filter is NOT applied ' +
|
|
119
|
+
'and the rows shown are unfiltered. Apply the whole expression and ' +
|
|
120
|
+
'acknowledge it, or clear the advanced filter. ' +
|
|
121
|
+
'See https://svgrid.com/docs/help/server/server-filtering');
|
|
122
|
+
}
|
|
26
123
|
async function fetchPage() {
|
|
27
124
|
if (disposed)
|
|
28
125
|
return;
|
|
29
126
|
const id = ++requestSeq;
|
|
127
|
+
pageAbort?.abort();
|
|
128
|
+
const controller = new AbortController();
|
|
129
|
+
pageAbort = controller;
|
|
30
130
|
state.loading = true;
|
|
31
131
|
state.error = null;
|
|
32
132
|
emit();
|
|
@@ -39,30 +139,20 @@ export function createServerDataSource(source, options) {
|
|
|
39
139
|
pageSize: state.pageSize,
|
|
40
140
|
sortModel: state.sortModel,
|
|
41
141
|
filterModel: state.filterModel,
|
|
42
|
-
// Flat mode: no grouping.
|
|
142
|
+
// Flat mode: no grouping. Server-side grouping and tree data live in
|
|
143
|
+
// `createServerGroupModel` from @svgrid/enterprise, which fills these in.
|
|
43
144
|
groupBy: [],
|
|
44
145
|
groupKeys: [],
|
|
45
146
|
aggregations: [],
|
|
147
|
+
signal: controller.signal,
|
|
46
148
|
});
|
|
47
149
|
if (disposed || id !== requestSeq)
|
|
48
150
|
return; // stale
|
|
49
151
|
state.rows = result.rows;
|
|
50
152
|
state.total = result.rowCount;
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
// "3 of 1,000,000 match" into a confident lie and make paging incoherent,
|
|
55
|
-
// since page 2 would re-filter a different slice.
|
|
56
|
-
state.expressionUnapplied =
|
|
57
|
-
state.filterModel.expression != null && result.appliedExpression !== true;
|
|
58
|
-
if (state.expressionUnapplied && !warnedExpressionUnapplied) {
|
|
59
|
-
warnedExpressionUnapplied = true;
|
|
60
|
-
console.warn('[svgrid] The data source was sent filterModel.expression but did not ' +
|
|
61
|
-
'return `appliedExpression: true`, so the advanced filter is NOT applied ' +
|
|
62
|
-
'and the rows shown are unfiltered. Apply the whole expression and ' +
|
|
63
|
-
'acknowledge it, or clear the advanced filter. ' +
|
|
64
|
-
'See https://svgrid.com/docs/help/server/server-filtering');
|
|
65
|
-
}
|
|
153
|
+
state.rowCount = result.rowCount;
|
|
154
|
+
state.lastRowKnown = true;
|
|
155
|
+
noteExpressionApplied(result);
|
|
66
156
|
state.loading = false;
|
|
67
157
|
emit();
|
|
68
158
|
}
|
|
@@ -90,7 +180,12 @@ export function createServerDataSource(source, options) {
|
|
|
90
180
|
return (async () => {
|
|
91
181
|
try {
|
|
92
182
|
const result = await thunk();
|
|
93
|
-
|
|
183
|
+
// Page mode re-reads the page. Infinite mode re-reads the blocks it
|
|
184
|
+
// holds, in place: the list keeps its length and its scroll.
|
|
185
|
+
if (infinite)
|
|
186
|
+
cache?.refresh();
|
|
187
|
+
else
|
|
188
|
+
await fetchPage();
|
|
94
189
|
return result;
|
|
95
190
|
}
|
|
96
191
|
finally {
|
|
@@ -101,12 +196,40 @@ export function createServerDataSource(source, options) {
|
|
|
101
196
|
}
|
|
102
197
|
const optimistic = !!options.optimistic && !!options.getRowId;
|
|
103
198
|
const getRowId = options.getRowId;
|
|
199
|
+
// The last range the grid reported, so a sort / filter / purge can
|
|
200
|
+
// re-request what the user is actually looking at rather than row 0.
|
|
201
|
+
let viewStart = 0;
|
|
202
|
+
let viewEnd = 0;
|
|
104
203
|
// Optimistic update: patch the local row, reconcile with the server result,
|
|
105
204
|
// roll back on error. Falls back to the plain refresh path when the row
|
|
106
205
|
// isn't on the current page (nothing local to update).
|
|
107
206
|
async function optimisticUpdate(id, patch, fn) {
|
|
108
207
|
if (disposed)
|
|
109
208
|
throw new Error('createServerDataSource: controller is disposed');
|
|
209
|
+
if (infinite && cache) {
|
|
210
|
+
// The cache owns the rows here; patch it, not a copy the next block
|
|
211
|
+
// to land would overwrite.
|
|
212
|
+
const at = cache.findIndex((r) => !rowPlaceholderState(r) && getRowId(r) === id);
|
|
213
|
+
if (at < 0)
|
|
214
|
+
return mutate('updateRow', () => fn(id, patch));
|
|
215
|
+
const prev = cache.getRow(at);
|
|
216
|
+
cache.patch(at, { ...prev, ...patch });
|
|
217
|
+
state.saving = true;
|
|
218
|
+
emit();
|
|
219
|
+
try {
|
|
220
|
+
const result = await fn(id, patch);
|
|
221
|
+
cache.patch(at, result);
|
|
222
|
+
return result;
|
|
223
|
+
}
|
|
224
|
+
catch (err) {
|
|
225
|
+
cache.patch(at, prev);
|
|
226
|
+
throw err;
|
|
227
|
+
}
|
|
228
|
+
finally {
|
|
229
|
+
state.saving = false;
|
|
230
|
+
emit();
|
|
231
|
+
}
|
|
232
|
+
}
|
|
110
233
|
const prevRows = state.rows;
|
|
111
234
|
const idx = prevRows.findIndex((r) => getRowId(r) === id);
|
|
112
235
|
if (idx < 0)
|
|
@@ -132,6 +255,27 @@ export function createServerDataSource(source, options) {
|
|
|
132
255
|
async function optimisticDelete(id, fn) {
|
|
133
256
|
if (disposed)
|
|
134
257
|
throw new Error('createServerDataSource: controller is disposed');
|
|
258
|
+
if (infinite && cache) {
|
|
259
|
+
const at = cache.findIndex((r) => !rowPlaceholderState(r) && getRowId(r) === id);
|
|
260
|
+
if (at < 0)
|
|
261
|
+
return mutate('deleteRow', () => fn(id));
|
|
262
|
+
const prev = cache.getRow(at);
|
|
263
|
+
cache.remove(at, 1);
|
|
264
|
+
state.saving = true;
|
|
265
|
+
emit();
|
|
266
|
+
try {
|
|
267
|
+
await fn(id);
|
|
268
|
+
}
|
|
269
|
+
catch (err) {
|
|
270
|
+
cache.insert(at, [prev]);
|
|
271
|
+
throw err;
|
|
272
|
+
}
|
|
273
|
+
finally {
|
|
274
|
+
state.saving = false;
|
|
275
|
+
emit();
|
|
276
|
+
}
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
135
279
|
const prevRows = state.rows;
|
|
136
280
|
const prevTotal = state.total;
|
|
137
281
|
const next = prevRows.filter((r) => getRowId(r) !== id);
|
|
@@ -154,8 +298,54 @@ export function createServerDataSource(source, options) {
|
|
|
154
298
|
emit();
|
|
155
299
|
}
|
|
156
300
|
}
|
|
301
|
+
// In infinite mode the first blocks are requested as soon as the grid
|
|
302
|
+
// reports a viewport; `refresh()` is what arms the cache before that.
|
|
303
|
+
if (infinite)
|
|
304
|
+
cache = buildCache();
|
|
157
305
|
return {
|
|
158
|
-
refresh:
|
|
306
|
+
refresh: () => {
|
|
307
|
+
if (!infinite)
|
|
308
|
+
return void fetchPage();
|
|
309
|
+
// Keep the count and the scroll position, re-request what is held.
|
|
310
|
+
cache?.refresh();
|
|
311
|
+
},
|
|
312
|
+
setViewport(startIndex, endIndex) {
|
|
313
|
+
viewStart = startIndex;
|
|
314
|
+
viewEnd = endIndex;
|
|
315
|
+
cache?.setViewport(startIndex, endIndex);
|
|
316
|
+
},
|
|
317
|
+
retryLoads: () => cache?.retryFailed(),
|
|
318
|
+
purge: () => cache?.purge(),
|
|
319
|
+
getCacheState: () => cache?.getCacheState() ?? [],
|
|
320
|
+
// --- GridRowModel -----------------------------------------------
|
|
321
|
+
subscribe(onChange) {
|
|
322
|
+
subscribers.add(onChange);
|
|
323
|
+
return () => {
|
|
324
|
+
subscribers.delete(onChange);
|
|
325
|
+
};
|
|
326
|
+
},
|
|
327
|
+
getRows: () => state.rows,
|
|
328
|
+
isLoading: () => state.loading,
|
|
329
|
+
getRowId: options.getRowId ? (row) => options.getRowId(row) : undefined,
|
|
330
|
+
// Only infinite mode has unloaded rows to stand in for.
|
|
331
|
+
rowPlaceholder: infinite ? (row) => rowPlaceholderState(row) : undefined,
|
|
332
|
+
retryRow: infinite ? (_row, rowIndex) => cache?.retryFailedAt(rowIndex) : undefined,
|
|
333
|
+
/**
|
|
334
|
+
* A getter, not a snapshot: the grid re-reads this after every change
|
|
335
|
+
* notification, and a frozen object would leave the pager on page 1
|
|
336
|
+
* forever. Absent in infinite mode, where there are no pages.
|
|
337
|
+
*/
|
|
338
|
+
get pagination() {
|
|
339
|
+
if (infinite)
|
|
340
|
+
return undefined;
|
|
341
|
+
return {
|
|
342
|
+
pageIndex: state.pageIndex,
|
|
343
|
+
pageSize: state.pageSize,
|
|
344
|
+
rowCount: state.total,
|
|
345
|
+
setPage: (pageIndex) => this.setPage(pageIndex),
|
|
346
|
+
setPageSize: (pageSize) => this.setPageSize(pageSize),
|
|
347
|
+
};
|
|
348
|
+
},
|
|
159
349
|
createRow: (input) => mutate('createRow', source.createRow ? () => source.createRow(input) : null),
|
|
160
350
|
updateRow: (id, patch) => {
|
|
161
351
|
if (!source.updateRow)
|
|
@@ -172,14 +362,27 @@ export function createServerDataSource(source, options) {
|
|
|
172
362
|
setSort(sortModel) {
|
|
173
363
|
state.sortModel = sortModel;
|
|
174
364
|
state.pageIndex = 0;
|
|
365
|
+
if (infinite)
|
|
366
|
+
return resetCache();
|
|
175
367
|
void fetchPage();
|
|
176
368
|
},
|
|
177
369
|
setFilter(filterModel) {
|
|
178
|
-
state.filterModel = filterModel
|
|
370
|
+
state.filterModel = Array.isArray(filterModel.columns)
|
|
371
|
+
? {
|
|
372
|
+
global: filterModel.global,
|
|
373
|
+
columns: toServerFilterColumns(filterModel),
|
|
374
|
+
}
|
|
375
|
+
: filterModel;
|
|
179
376
|
state.pageIndex = 0;
|
|
377
|
+
if (infinite)
|
|
378
|
+
return resetCache();
|
|
180
379
|
void fetchPage();
|
|
181
380
|
},
|
|
182
381
|
setPage(pageIndex) {
|
|
382
|
+
// There are no pages to move between when the whole result is one
|
|
383
|
+
// scrollable list; the grid scrolls instead.
|
|
384
|
+
if (infinite)
|
|
385
|
+
return;
|
|
183
386
|
const clamped = Math.max(0, pageIndex);
|
|
184
387
|
if (clamped === state.pageIndex)
|
|
185
388
|
return;
|
|
@@ -189,11 +392,17 @@ export function createServerDataSource(source, options) {
|
|
|
189
392
|
setPageSize(pageSize) {
|
|
190
393
|
state.pageSize = Math.max(1, pageSize);
|
|
191
394
|
state.pageIndex = 0;
|
|
395
|
+
if (infinite)
|
|
396
|
+
return;
|
|
192
397
|
void fetchPage();
|
|
193
398
|
},
|
|
194
399
|
getState: () => ({ ...state }),
|
|
195
400
|
dispose() {
|
|
196
401
|
disposed = true;
|
|
402
|
+
pageAbort?.abort();
|
|
403
|
+
pageAbort = null;
|
|
404
|
+
cache?.dispose();
|
|
405
|
+
cache = null;
|
|
197
406
|
// An in-flight fetch's resolution short-circuits on `disposed`, so it
|
|
198
407
|
// never clears `loading`. Clear it here (and emit) so a disposed
|
|
199
408
|
// controller doesn't report a permanent loading state.
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@svgrid/grid/server` - the server-side data primitives, with no Svelte in
|
|
3
|
+
* the import graph.
|
|
4
|
+
*
|
|
5
|
+
* Everything here is also on the package root. This entry point exists for
|
|
6
|
+
* code that must not pull `SvGrid.svelte` into its bundle or its test
|
|
7
|
+
* runner: a SvelteKit `+server.ts` shaping a `ServerRequest` into a query,
|
|
8
|
+
* a worker feeding a block cache, or `@svgrid/enterprise`'s own row model,
|
|
9
|
+
* whose unit tests run without the Svelte compiler.
|
|
10
|
+
*/
|
|
11
|
+
export { createServerDataSource, type ServerDataSource, type ServerSelectionRule, type ServerRequest, type ServerResult, type ServerController, type ServerControllerOptions, type ServerState, type ServerSortModel, type ServerFilterModel, type ServerAggFn, type ServerAggregation, type ServerGroupRow, type ServerLeafRow, type ServerMoreRow, type ServerFooterRow, type ServerSkeletonRow, type ServerGrandTotalRow, type ServerPlaceholderRow, type ServerDisplayRow, type ServerDetailRow, } from './server-data-source.js';
|
|
12
|
+
export { createBlockCache, createRowPlaceholder, rowPlaceholderState, type BlockCache, type BlockCacheOptions, type BlockCacheState, type BlockFetchResult, type BlockState, } from './server-block-cache.js';
|
|
13
|
+
export { toServerFilterColumns, type GridRowModel, type GridFilterState, type GridRowModelSort, } from './row-model.js';
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@svgrid/grid/server` - the server-side data primitives, with no Svelte in
|
|
3
|
+
* the import graph.
|
|
4
|
+
*
|
|
5
|
+
* Everything here is also on the package root. This entry point exists for
|
|
6
|
+
* code that must not pull `SvGrid.svelte` into its bundle or its test
|
|
7
|
+
* runner: a SvelteKit `+server.ts` shaping a `ServerRequest` into a query,
|
|
8
|
+
* a worker feeding a block cache, or `@svgrid/enterprise`'s own row model,
|
|
9
|
+
* whose unit tests run without the Svelte compiler.
|
|
10
|
+
*/
|
|
11
|
+
export { createServerDataSource, } from './server-data-source.js';
|
|
12
|
+
export { createBlockCache, createRowPlaceholder, rowPlaceholderState, } from './server-block-cache.js';
|
|
13
|
+
export { toServerFilterColumns, } from './row-model.js';
|
package/dist/sparkline.js
CHANGED
|
@@ -38,13 +38,15 @@ export function buildSparkline(values, cfg = {}) {
|
|
|
38
38
|
const barW = Math.max(1, slot - gap);
|
|
39
39
|
const mid = height / 2;
|
|
40
40
|
const h = innerH / 2;
|
|
41
|
-
const bars =
|
|
41
|
+
const bars = [];
|
|
42
|
+
values.forEach((v, i) => {
|
|
42
43
|
const x = pad + i * slot + (slot - barW) / 2;
|
|
44
|
+
// A win/loss sparkline marks a win up and a loss down; a zero is a
|
|
45
|
+
// blank gap, no mark at all, the way Excel draws one.
|
|
43
46
|
if (v > 0)
|
|
44
|
-
|
|
45
|
-
if (v < 0)
|
|
46
|
-
|
|
47
|
-
return { x, y: mid - 1, w: barW, h: 2, negative: false };
|
|
47
|
+
bars.push({ x, y: mid - h, w: barW, h, negative: false });
|
|
48
|
+
else if (v < 0)
|
|
49
|
+
bars.push({ x, y: mid, w: barW, h, negative: true });
|
|
48
50
|
});
|
|
49
51
|
return { ...base, linePath: '', areaPath: '', bars, lastPoint: null };
|
|
50
52
|
}
|
|
@@ -151,6 +151,12 @@ export type SvGridTransactionResult = {
|
|
|
151
151
|
* the component.
|
|
152
152
|
*/
|
|
153
153
|
export type SvGridApi<TFeatures extends TableFeatures, TData extends RowData> = {
|
|
154
|
+
/**
|
|
155
|
+
* The grid's root element (`.sv-grid-root`), or null before mount and
|
|
156
|
+
* after unmount. For measuring, focusing, or reading the theme the grid
|
|
157
|
+
* is drawn with - the PDF export takes its colours from here.
|
|
158
|
+
*/
|
|
159
|
+
getElement(): HTMLElement | null;
|
|
154
160
|
/** Read a cell value from the underlying data at `rowIndex`. */
|
|
155
161
|
getCellValue(rowIndex: number, columnId: string): unknown;
|
|
156
162
|
/** Write a cell value through the column's field. */
|
|
@@ -413,6 +419,15 @@ export type SvGridApi<TFeatures extends TableFeatures, TData extends RowData> =
|
|
|
413
419
|
getDisplayedRows(): ReadonlyArray<TData>;
|
|
414
420
|
/** Snapshot of the current data array (pre-pipeline). */
|
|
415
421
|
getData(): ReadonlyArray<TData>;
|
|
422
|
+
/**
|
|
423
|
+
* The stable id the grid keys a row by: what the `getRowId` prop
|
|
424
|
+
* computed, or the row's position when no `getRowId` is set. Pass a
|
|
425
|
+
* display index (the space `getActiveCell`, `scrollToRow` and the cell
|
|
426
|
+
* methods use) or a row object; returns null for an unknown index or a
|
|
427
|
+
* row that is not in the data. Pair with `selectRows`, `toggleRowSelected`
|
|
428
|
+
* or an `applyTransaction` remove, all of which take these ids.
|
|
429
|
+
*/
|
|
430
|
+
getRowId(row: number | TData): string | null;
|
|
416
431
|
/**
|
|
417
432
|
* Snapshot of every column the grid currently knows about, in visual
|
|
418
433
|
* order, with the human-readable header label. Use this when exporting
|
|
@@ -560,6 +575,24 @@ export type SvGridApi<TFeatures extends TableFeatures, TData extends RowData> =
|
|
|
560
575
|
canRedo(): boolean;
|
|
561
576
|
/** Wipe both stacks (e.g. after a server save commits the buffer). */
|
|
562
577
|
clearHistory(): void;
|
|
578
|
+
/**
|
|
579
|
+
* Mark every step recorded from now on with `tag`, until the next call;
|
|
580
|
+
* `undefined` stops the marking. The grid never reads the tag. It is for a
|
|
581
|
+
* consumer that shows one of several data sets through the grid (a
|
|
582
|
+
* workbook's sheets, say) and has to know which one the next Ctrl+Z would
|
|
583
|
+
* change before it lets the grid apply it: it tags the steps with the data
|
|
584
|
+
* set they were made on and asks `peekUndo` first.
|
|
585
|
+
*/
|
|
586
|
+
setHistoryTag(tag: unknown): void;
|
|
587
|
+
/** The step the next `undo()` would apply, or null when there is none. A
|
|
588
|
+
* multi-cell action carries one tag, so it reports as one. */
|
|
589
|
+
peekUndo(): {
|
|
590
|
+
tag: unknown;
|
|
591
|
+
} | null;
|
|
592
|
+
/** The step the next `redo()` would apply, or null when there is none. */
|
|
593
|
+
peekRedo(): {
|
|
594
|
+
tag: unknown;
|
|
595
|
+
} | null;
|
|
563
596
|
/**
|
|
564
597
|
* The same `GridCommandContext` a registered shortcut handler receives.
|
|
565
598
|
*
|
package/dist/validate.d.ts
CHANGED
|
@@ -35,6 +35,10 @@ export type ValidateInput<TFeatures extends TableFeatures, TData extends RowData
|
|
|
35
35
|
idField?: string;
|
|
36
36
|
column?: string;
|
|
37
37
|
};
|
|
38
|
+
/** Gantt mode. Only its presence matters here - see rule 6b. */
|
|
39
|
+
gantt?: unknown;
|
|
40
|
+
/** Server-side grouping: group rows carry their key and aggregates, not the leaf fields. */
|
|
41
|
+
serverGroup?: unknown;
|
|
38
42
|
initialColumnPinning?: {
|
|
39
43
|
left?: ReadonlyArray<string>;
|
|
40
44
|
right?: ReadonlyArray<string>;
|
package/dist/validate.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { rowPlaceholderState } from './server-block-cache.js';
|
|
1
2
|
/** How many rows to sample when deciding whether a `field` exists. */
|
|
2
3
|
const FIELD_SAMPLE_ROWS = 10;
|
|
3
4
|
const DOCS = 'https://svgrid.com/docs/getting-started/3-data-and-columns/';
|
|
@@ -17,9 +18,21 @@ export function validateGridConfig(input) {
|
|
|
17
18
|
// ---- 1. A `field` that does not exist on the data -------------------------
|
|
18
19
|
// The single most expensive silent failure: the column renders, and every
|
|
19
20
|
// cell in it is blank. Sampled across several rows so genuinely sparse data
|
|
20
|
-
// (a key absent from row 0 but present later) does not trip it.
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
// (a key absent from row 0 but present later) does not trip it. A row
|
|
22
|
+
// model shows placeholder rows before its first block lands; those carry
|
|
23
|
+
// no data keys at all and say nothing about the columns.
|
|
24
|
+
// Stops at the sample size: this runs on every data change, and a filter
|
|
25
|
+
// over all the rows first was O(n) per tick on a live feed.
|
|
26
|
+
const sample = [];
|
|
27
|
+
for (const row of input.data ?? []) {
|
|
28
|
+
if (sample.length >= FIELD_SAMPLE_ROWS)
|
|
29
|
+
break;
|
|
30
|
+
if (!rowPlaceholderState(row))
|
|
31
|
+
sample.push(row);
|
|
32
|
+
}
|
|
33
|
+
// Under server-side grouping the first rows are group rows, which carry
|
|
34
|
+
// a key and aggregates and none of the leaf fields; nothing to judge by.
|
|
35
|
+
if (sample.length && !input.serverGroup) {
|
|
23
36
|
const known = new Set();
|
|
24
37
|
for (const row of sample) {
|
|
25
38
|
if (row && typeof row === 'object')
|
|
@@ -122,6 +135,19 @@ export function validateGridConfig(input) {
|
|
|
122
135
|
`data, so parent lookups cannot match and the tree stays flat.`);
|
|
123
136
|
}
|
|
124
137
|
}
|
|
138
|
+
// ---- 6b. gantt + treeData: two trees over one set of rows -----------------
|
|
139
|
+
// The Gantt builds its own work-breakdown tree from `gantt.parentField`,
|
|
140
|
+
// because it needs every descendant - a collapsed phase still contributes to
|
|
141
|
+
// its summary bar. `treeData` hides collapsed children from the view before
|
|
142
|
+
// the renderer sees them, so the two together silently drop tasks out of
|
|
143
|
+
// their phase's rollup the moment anything is collapsed.
|
|
144
|
+
if (input.gantt && input.treeData) {
|
|
145
|
+
messages.push('[svgrid] `gantt` and `treeData` are both set. The Gantt builds its own ' +
|
|
146
|
+
'work-breakdown tree from `gantt.parentField` and needs every ' +
|
|
147
|
+
'descendant row, but `treeData` hides collapsed children from the view, ' +
|
|
148
|
+
'so summary bars will under-report. Remove `treeData` and set ' +
|
|
149
|
+
'`gantt.parentField` instead.');
|
|
150
|
+
}
|
|
125
151
|
// ---- 7. Pinning that column virtualization will hide ----------------------
|
|
126
152
|
// Documented incompatibility: the virtualizer recycles column DOM nodes, so
|
|
127
153
|
// sticky pinning cannot survive it. `columnVirtualization` defaults to ON,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svgrid/grid",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.6",
|
|
4
4
|
"description": "Svelte 5-native data grid and data table. Headless-first engine with virtual scrolling for 100k+ rows, Excel-style filtering, inline editing, grouping, tree data, pivot, and server-side data. Drop-in <SvGrid> component. TypeScript, SSR-ready.",
|
|
5
5
|
"author": "jQWidgets <boikom@jqwidgets.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -94,11 +94,21 @@
|
|
|
94
94
|
"svelte": "./dist/filtering/excel-filters.js",
|
|
95
95
|
"import": "./dist/filtering/excel-filters.js"
|
|
96
96
|
},
|
|
97
|
+
"./sparkline": {
|
|
98
|
+
"types": "./dist/sparkline.d.ts",
|
|
99
|
+
"svelte": "./dist/sparkline.js",
|
|
100
|
+
"import": "./dist/sparkline.js"
|
|
101
|
+
},
|
|
97
102
|
"./shortcuts": {
|
|
98
103
|
"types": "./dist/shortcut-registry.d.ts",
|
|
99
104
|
"svelte": "./dist/shortcut-registry.js",
|
|
100
105
|
"import": "./dist/shortcut-registry.js"
|
|
101
106
|
},
|
|
107
|
+
"./server": {
|
|
108
|
+
"types": "./dist/server.d.ts",
|
|
109
|
+
"svelte": "./dist/server.js",
|
|
110
|
+
"import": "./dist/server.js"
|
|
111
|
+
},
|
|
102
112
|
"./flex-render": {
|
|
103
113
|
"types": "./dist/flex-render.d.ts",
|
|
104
114
|
"svelte": "./dist/flex-render.js",
|
package/src/GridMenus.svelte
CHANGED
|
@@ -214,6 +214,19 @@
|
|
|
214
214
|
});
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
+
/**
|
|
218
|
+
* Whether a column's active filter operator is one of the set operators,
|
|
219
|
+
* read at event time from component state: the condition input's focus
|
|
220
|
+
* and blur handlers cannot depend on a template `@const`, because blur
|
|
221
|
+
* also fires while the popover is being torn down, after that const's
|
|
222
|
+
* effect is gone.
|
|
223
|
+
*/
|
|
224
|
+
function isSetOperatorFor(colId: string): boolean {
|
|
225
|
+
const column = allColumns.find((c) => c.id === colId);
|
|
226
|
+
const op = filterMenuValues[colId]?.operator ?? defaultOperatorFor(column);
|
|
227
|
+
return op === "in" || op === "notIn";
|
|
228
|
+
}
|
|
229
|
+
|
|
217
230
|
/** Enter anywhere in the panel means "done" - filtering is already live. */
|
|
218
231
|
function onFilterPanelKeydown(event: KeyboardEvent) {
|
|
219
232
|
if (event.key !== "Enter" || event.shiftKey) return;
|
|
@@ -286,8 +299,23 @@
|
|
|
286
299
|
const saveComment = $derived(ctrl.saveComment);
|
|
287
300
|
const removeComment = $derived(ctrl.removeComment);
|
|
288
301
|
const closeCommentEditor = $derived(ctrl.closeCommentEditor);
|
|
302
|
+
|
|
303
|
+
function onContextMenuKeydown(event: KeyboardEvent) {
|
|
304
|
+
// Escape closes the context menu, the way every spreadsheet and desktop
|
|
305
|
+
// app does. The backdrop closes it on a click, but a keyboard user who
|
|
306
|
+
// opened it (Shift+F10, the Menu key) otherwise has no way out, and the
|
|
307
|
+
// menu sits over the sheet swallowing every other key. Handled at the
|
|
308
|
+
// window so it works whatever holds focus while the menu is open.
|
|
309
|
+
if (contextMenuFor && (event.key === "Escape" || event.key === "Esc")) {
|
|
310
|
+
event.preventDefault();
|
|
311
|
+
event.stopPropagation();
|
|
312
|
+
closeContextMenu();
|
|
313
|
+
}
|
|
314
|
+
}
|
|
289
315
|
</script>
|
|
290
316
|
|
|
317
|
+
<svelte:window onkeydown={onContextMenuKeydown} />
|
|
318
|
+
|
|
291
319
|
{#if tooltip}
|
|
292
320
|
<!-- Custom hover tooltip. Floats above the grid; positioned by
|
|
293
321
|
showTooltipFor(). Notes and column tooltips both surface here. -->
|
|
@@ -396,10 +424,12 @@
|
|
|
396
424
|
: menuActiveOperator === "regex"
|
|
397
425
|
? "Pattern..."
|
|
398
426
|
: "Filter value..."}
|
|
399
|
-
onfocus={
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
onblur={
|
|
427
|
+
onfocus={(event) => {
|
|
428
|
+
if (isSetOperatorFor(colId)) onSetOpFilterFocus(event.currentTarget as HTMLInputElement, colId);
|
|
429
|
+
}}
|
|
430
|
+
onblur={() => {
|
|
431
|
+
if (isSetOperatorFor(colId)) closeInSuggest();
|
|
432
|
+
}}
|
|
403
433
|
oninput={(event) => {
|
|
404
434
|
const input = event.currentTarget as HTMLInputElement;
|
|
405
435
|
updateFilterMenuValue(colId, input.value);
|