@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.
Files changed (176) hide show
  1. package/README.md +32 -11
  2. package/dist/GridMenus.svelte +34 -4
  3. package/dist/SvGrid.controller.svelte.d.ts +33 -5
  4. package/dist/SvGrid.controller.svelte.js +397 -58
  5. package/dist/SvGrid.css +200 -2
  6. package/dist/SvGrid.svelte +350 -40
  7. package/dist/SvGrid.types.d.ts +433 -7
  8. package/dist/SvGridDropdown.svelte +20 -0
  9. package/dist/SvGridDropdown.svelte.d.ts +12 -0
  10. package/dist/SvModal.svelte +22 -0
  11. package/dist/SvModal.svelte.d.ts +7 -0
  12. package/dist/a11y/dismissable.d.ts +10 -0
  13. package/dist/a11y/dismissable.js +24 -2
  14. package/dist/build-api.js +92 -37
  15. package/dist/cdn/{GridMenus-BL7ZgQvU.js → GridMenus-C-NtjlrJ.js} +155 -146
  16. package/dist/cdn/GridMenus-TssHej3q.js +644 -0
  17. package/dist/cdn/{SvChartMenu-FBSMINA6.js → SvChartMenu-BFSTpvB-.js} +1 -1
  18. package/dist/cdn/{SvChartMenu-Bl6PBbkT.js → SvChartMenu-BZ1Iwv0y.js} +1 -1
  19. package/dist/cdn/{SvDateTimePicker-DQwt4UAs.js → SvDateTimePicker-D9u_cOf8.js} +1 -1
  20. package/dist/cdn/{SvDateTimePicker-vNU6bZ-q.js → SvDateTimePicker-s_6IUpI7.js} +1 -1
  21. package/dist/cdn/{SvGridCellEditor-B1p-vCK5.js → SvGridCellEditor-BlyCSp0q.js} +1 -1
  22. package/dist/cdn/{SvGridCellEditor-D_0q4xAu.js → SvGridCellEditor-ClRRA9lW.js} +1 -1
  23. package/dist/cdn/{SvGridChart-DzLkSwCH.js → SvGridChart-B4EV8U6t.js} +4 -4
  24. package/dist/cdn/{SvGridChart-C3EWAZaM.js → SvGridChart-DsmwTyZF.js} +4 -4
  25. package/dist/cdn/{SvGridChartBuilder-CfII62sZ.js → SvGridChartBuilder-cHloHobN.js} +4 -4
  26. package/dist/cdn/{SvGridChartBuilder-BE2T1ykB.js → SvGridChartBuilder-vOl7C0Za.js} +4 -4
  27. package/dist/cdn/{SvGridChartPanel-xf4sWVTo.js → SvGridChartPanel-AcqF-Ll5.js} +5 -5
  28. package/dist/cdn/{SvGridChartPanel-CCX5_Wgd.js → SvGridChartPanel-biMtSBPA.js} +5 -5
  29. package/dist/cdn/{SvGridChartView-DYwabQWj.js → SvGridChartView-Dx_GmZuL.js} +2 -2
  30. package/dist/cdn/{SvGridChartView-CfuXmY5I.js → SvGridChartView-I3rDHXAq.js} +2 -2
  31. package/dist/cdn/{SvGridDropdown-D0VdjeR8.js → SvGridDropdown-BWeM3lMe.js} +46 -46
  32. package/dist/cdn/{SvGridDropdown-D13MtJ2j.js → SvGridDropdown-CJ6QqTxu.js} +67 -67
  33. package/dist/cdn/{SvModal-CIZWCcad.js → SvModal-ClxGmpAc.js} +187 -186
  34. package/dist/cdn/{SvModal-BAE-pjZX.js → SvModal-DhB_VMFR.js} +148 -147
  35. package/dist/cdn/{cell-formatting-C2Hf5gqd.js → cell-formatting-DVVKOSI9.js} +1 -1
  36. package/dist/cdn/{chart-Dz7SqMXH.js → chart-DEZxwC0i.js} +1 -1
  37. package/dist/cdn/{chart-panel-messages-CUbf2R4i.js → chart-panel-messages-D12XcTZJ.js} +1 -1
  38. package/dist/cdn/{chart-panel-messages-CmNrMdsr.js → chart-panel-messages-DguS7gC5.js} +1 -1
  39. package/dist/cdn/{chart-summary-BJW_tg_X.js → chart-summary-CMAXBGY-.js} +1 -1
  40. package/dist/cdn/{chart-trend-CaN9mDEV.js → chart-trend-BkEp61Gj.js} +1 -1
  41. package/dist/cdn/{dismissable-DAHetSNk.js → dismissable-Brc4EZU1.js} +5 -1
  42. package/dist/cdn/{export-format-CUDLy2yn.js → export-format-YzdwBww2.js} +1 -1
  43. package/dist/cdn/{row-resize-niQCp040.js → row-resize-DtOS1KG6.js} +41 -32
  44. package/dist/cdn/server-block-cache-DIqb3VZn.js +241 -0
  45. package/dist/cdn/{src-V1uu8iE9.js → src-CgoeSM8k.js} +5339 -4753
  46. package/dist/cdn/{src-BKhZ6eXd.js → src-DfwxB1TH.js} +9817 -9231
  47. package/dist/cdn/svgrid.js +28 -27
  48. package/dist/cdn/svgrid.svelte-external.js +28 -27
  49. package/dist/cdn/validate-AZoD8BoR.js +80 -0
  50. package/dist/cell-formatting.js +18 -1
  51. package/dist/cell-render.js +25 -6
  52. package/dist/clipboard.d.ts +2 -2
  53. package/dist/clipboard.js +72 -23
  54. package/dist/columns.js +7 -2
  55. package/dist/conditional-formatting.d.ts +2 -0
  56. package/dist/conditional-formatting.js +2 -0
  57. package/dist/core.d.ts +22 -6
  58. package/dist/core.js +401 -38
  59. package/dist/editing.js +34 -0
  60. package/dist/gantt-view.svelte.d.ts +24 -0
  61. package/dist/gantt-view.svelte.js +13 -0
  62. package/dist/grid-icons.d.ts +2 -2
  63. package/dist/grid-icons.js +2 -2
  64. package/dist/grid-messages.d.ts +10 -0
  65. package/dist/grid-messages.js +9 -0
  66. package/dist/headless.d.ts +5 -0
  67. package/dist/headless.js +12 -0
  68. package/dist/history.d.ts +21 -0
  69. package/dist/history.js +34 -4
  70. package/dist/index.d.ts +5 -4
  71. package/dist/index.js +3 -3
  72. package/dist/keyboard-handlers.js +24 -1
  73. package/dist/keyboard.js +4 -0
  74. package/dist/row-drag-touch.d.ts +1 -1
  75. package/dist/row-drag.js +35 -1
  76. package/dist/row-model.d.ts +139 -0
  77. package/dist/row-model.js +16 -0
  78. package/dist/row-resize.js +22 -1
  79. package/dist/selection.d.ts +2 -1
  80. package/dist/selection.js +31 -3
  81. package/dist/server-block-cache.d.ts +219 -0
  82. package/dist/server-block-cache.js +539 -0
  83. package/dist/server-data-source.d.ts +274 -8
  84. package/dist/server-data-source.js +229 -20
  85. package/dist/server.d.ts +13 -0
  86. package/dist/server.js +13 -0
  87. package/dist/sparkline.js +7 -5
  88. package/dist/svgrid-wrapper.types.d.ts +33 -0
  89. package/dist/validate.d.ts +4 -0
  90. package/dist/validate.js +29 -3
  91. package/package.json +11 -1
  92. package/src/GridMenus.svelte +34 -4
  93. package/src/SvGrid.controller.svelte.ts +397 -61
  94. package/src/SvGrid.css +200 -2
  95. package/src/SvGrid.svelte +350 -40
  96. package/src/SvGrid.types.ts +453 -7
  97. package/src/SvGridDropdown.svelte +20 -0
  98. package/src/SvModal.svelte +22 -0
  99. package/src/SvModal.test.ts +30 -1
  100. package/src/a11y/dismissable.test.ts +10 -1
  101. package/src/a11y/dismissable.ts +22 -2
  102. package/src/build-api.ts +96 -31
  103. package/src/cell-formatting.test.ts +17 -0
  104. package/src/cell-formatting.ts +18 -1
  105. package/src/cell-render.test.ts +24 -2
  106. package/src/cell-render.ts +24 -8
  107. package/src/clipboard.test.ts +107 -0
  108. package/src/clipboard.ts +62 -23
  109. package/src/columns.test.ts +4 -4
  110. package/src/columns.ts +7 -2
  111. package/src/conditional-formatting.test.ts +8 -0
  112. package/src/conditional-formatting.ts +3 -0
  113. package/src/core.rowmodel-cache.test.ts +51 -0
  114. package/src/core.tick-repair.test.ts +287 -0
  115. package/src/core.ts +407 -38
  116. package/src/editing.test.ts +52 -0
  117. package/src/editing.ts +33 -0
  118. package/src/gantt-stub.test.svelte +38 -0
  119. package/src/gantt-view.svelte.ts +35 -0
  120. package/src/grid-icons.ts +2 -2
  121. package/src/grid-messages.test.ts +7 -0
  122. package/src/grid-messages.ts +22 -0
  123. package/src/headless.ts +24 -0
  124. package/src/history.test.ts +49 -1
  125. package/src/history.ts +48 -2
  126. package/src/icon-seam.test.ts +2 -4
  127. package/src/index.ts +38 -10
  128. package/src/keyboard-handlers.coverage.test.ts +65 -0
  129. package/src/keyboard-handlers.ts +26 -1
  130. package/src/keyboard.ts +3 -0
  131. package/src/row-drag-touch.ts +2 -2
  132. package/src/row-drag.test.ts +51 -0
  133. package/src/row-drag.ts +35 -2
  134. package/src/row-model.ts +152 -0
  135. package/src/row-resize.ts +24 -1
  136. package/src/selection.test.ts +3 -0
  137. package/src/selection.ts +33 -3
  138. package/src/server-block-cache.test.ts +672 -0
  139. package/src/server-block-cache.ts +690 -0
  140. package/src/server-data-source.infinite.test.ts +405 -0
  141. package/src/server-data-source.test.ts +19 -0
  142. package/src/server-data-source.ts +496 -29
  143. package/src/server.ts +49 -0
  144. package/src/sparkline.test.ts +7 -0
  145. package/src/sparkline.ts +6 -4
  146. package/src/svgrid-wrapper.types.ts +32 -0
  147. package/src/svgrid.api.test.ts +21 -0
  148. package/src/svgrid.charting.test.ts +12 -4
  149. package/src/svgrid.context-menu.test.ts +12 -0
  150. package/src/svgrid.detail-rows.svelte.test.ts +187 -0
  151. package/src/svgrid.filter-menu-scroll.test.ts +9 -0
  152. package/src/svgrid.gantt-seam.test.ts +199 -0
  153. package/src/svgrid.live-update-paths.svelte.test.ts +184 -0
  154. package/src/svgrid.menu-scroll-close.test.ts +10 -0
  155. package/src/svgrid.row-model-prop.svelte.test.ts +352 -0
  156. package/src/svgrid.row-model-seam.svelte.test.ts +305 -0
  157. package/src/svgrid.row-pinning.test.ts +15 -1
  158. package/src/svgrid.selection-bar-seam.test.ts +1 -1
  159. package/src/svgrid.sticky-groups.svelte.test.ts +145 -0
  160. package/src/svgrid.upsell-license.test.ts +8 -5
  161. package/src/test-setup.ts +12 -0
  162. package/src/transaction.test.ts +58 -0
  163. package/src/validate.test.ts +29 -0
  164. package/src/validate.ts +34 -3
  165. package/dist/SvGroupCell.svelte +0 -141
  166. package/dist/SvGroupCell.svelte.d.ts +0 -49
  167. package/dist/SvRowGroupPanel.svelte +0 -186
  168. package/dist/SvRowGroupPanel.svelte.d.ts +0 -25
  169. package/dist/cdn/GridMenus-7kbpnnBW.js +0 -635
  170. package/dist/cdn/validate-_CDJzgIo.js +0 -75
  171. package/dist/server-group-model.d.ts +0 -98
  172. package/dist/server-group-model.js +0 -263
  173. package/src/SvGroupCell.svelte +0 -141
  174. package/src/SvRowGroupPanel.svelte +0 -186
  175. package/src/server-group-model.test.ts +0 -294
  176. 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. Group mode lives in createServerGroupModel.
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
- // An expression was sent but the backend did not acknowledge applying it,
52
- // so these rows are a SUPERSET of what was asked for. Say so instead of
53
- // filtering the loaded page here: filtering one page would turn
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
- await fetchPage();
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: fetchPage,
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.
@@ -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 = values.map((v, i) => {
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
- return { x, y: mid - h, w: barW, h, negative: false };
45
- if (v < 0)
46
- return { x, y: mid, w: barW, h, negative: true };
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
  *
@@ -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
- const sample = (input.data ?? []).slice(0, FIELD_SAMPLE_ROWS);
22
- if (sample.length) {
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.4",
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",
@@ -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={isSetOp
400
- ? (event) => onSetOpFilterFocus(event.currentTarget as HTMLInputElement, colId)
401
- : undefined}
402
- onblur={isSetOp ? () => closeInSuggest() : undefined}
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);