@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.
Files changed (166) hide show
  1. package/README.md +28 -7
  2. package/dist/GridMenus.svelte +34 -4
  3. package/dist/SvGrid.controller.svelte.d.ts +23 -3
  4. package/dist/SvGrid.controller.svelte.js +235 -54
  5. package/dist/SvGrid.css +143 -7
  6. package/dist/SvGrid.helpers.js +2 -4
  7. package/dist/SvGrid.svelte +223 -38
  8. package/dist/SvGrid.types.d.ts +77 -4
  9. package/dist/SvGridDropdown.svelte +20 -0
  10. package/dist/SvGridDropdown.svelte.d.ts +12 -0
  11. package/dist/SvModal.svelte +22 -0
  12. package/dist/SvModal.svelte.d.ts +7 -0
  13. package/dist/a11y/dismissable.d.ts +10 -0
  14. package/dist/a11y/dismissable.js +24 -2
  15. package/dist/build-api.js +81 -31
  16. package/dist/cdn/{GridMenus-CID62rlI.js → GridMenus-BgUS-W3o.js} +151 -142
  17. package/dist/cdn/GridMenus-CcwU8I4t.js +644 -0
  18. package/dist/cdn/{SvChartMenu-FBSMINA6.js → SvChartMenu-BFSTpvB-.js} +1 -1
  19. package/dist/cdn/{SvChartMenu-Bl6PBbkT.js → SvChartMenu-BZ1Iwv0y.js} +1 -1
  20. package/dist/cdn/{SvDateTimePicker-DQwt4UAs.js → SvDateTimePicker-D9u_cOf8.js} +1 -1
  21. package/dist/cdn/{SvDateTimePicker-vNU6bZ-q.js → SvDateTimePicker-s_6IUpI7.js} +1 -1
  22. package/dist/cdn/{SvGridCellEditor-B1p-vCK5.js → SvGridCellEditor-9yz-SbdZ.js} +2 -2
  23. package/dist/cdn/{SvGridCellEditor-D_0q4xAu.js → SvGridCellEditor-D7-ql1Hd.js} +2 -2
  24. package/dist/cdn/{SvGridChart-C3EWAZaM.js → SvGridChart-DMUOFxOe.js} +4 -4
  25. package/dist/cdn/{SvGridChart-DzLkSwCH.js → SvGridChart-N7ik0oWy.js} +4 -4
  26. package/dist/cdn/{SvGridChartBuilder-CfII62sZ.js → SvGridChartBuilder-CB_Jnm2k.js} +4 -4
  27. package/dist/cdn/{SvGridChartBuilder-BE2T1ykB.js → SvGridChartBuilder-Ca7gF8ZK.js} +4 -4
  28. package/dist/cdn/{SvGridChartPanel-xf4sWVTo.js → SvGridChartPanel-9GOJRHP7.js} +5 -5
  29. package/dist/cdn/{SvGridChartPanel-CCX5_Wgd.js → SvGridChartPanel-Dl-FujN8.js} +5 -5
  30. package/dist/cdn/{SvGridChartView-CfuXmY5I.js → SvGridChartView-B1LFgoPP.js} +2 -2
  31. package/dist/cdn/{SvGridChartView-DYwabQWj.js → SvGridChartView-By3n7DGZ.js} +2 -2
  32. package/dist/cdn/{SvGridDropdown-D0VdjeR8.js → SvGridDropdown-BWeM3lMe.js} +46 -46
  33. package/dist/cdn/{SvGridDropdown-D13MtJ2j.js → SvGridDropdown-CJ6QqTxu.js} +67 -67
  34. package/dist/cdn/{SvModal-CIZWCcad.js → SvModal-ClxGmpAc.js} +187 -186
  35. package/dist/cdn/{SvModal-BAE-pjZX.js → SvModal-DhB_VMFR.js} +148 -147
  36. package/dist/cdn/{cell-formatting-C2Hf5gqd.js → cell-formatting-Mnv84zoE.js} +28 -6
  37. package/dist/cdn/{chart-Dz7SqMXH.js → chart-By7h2Guv.js} +1 -1
  38. package/dist/cdn/{chart-panel-messages-CUbf2R4i.js → chart-panel-messages-BC-tOgFY.js} +1 -1
  39. package/dist/cdn/{chart-panel-messages-CmNrMdsr.js → chart-panel-messages-DUaXr-sb.js} +1 -1
  40. package/dist/cdn/{chart-summary-BJW_tg_X.js → chart-summary-BDtrNEiA.js} +1 -1
  41. package/dist/cdn/{chart-trend-CaN9mDEV.js → chart-trend-C1aOAPBZ.js} +1 -1
  42. package/dist/cdn/{dismissable-DAHetSNk.js → dismissable-Brc4EZU1.js} +5 -1
  43. package/dist/cdn/{editor-registry-BhgE3S84.js → editor-registry-BVeHvlZR.js} +49 -50
  44. package/dist/cdn/{export-format-CUDLy2yn.js → export-format-y1wdyCcv.js} +93 -97
  45. package/dist/cdn/{row-resize-niQCp040.js → row-resize-DtOS1KG6.js} +41 -32
  46. package/dist/cdn/{server-block-cache-CGoWz-87.js → server-block-cache-DIqb3VZn.js} +4 -0
  47. package/dist/cdn/{src-jcFMkBnN.js → src-25JmJ_7g.js} +4768 -4149
  48. package/dist/cdn/{src-Bklofsad.js → src-kE9B98r6.js} +4796 -4177
  49. package/dist/cdn/svgrid.js +18 -18
  50. package/dist/cdn/svgrid.svelte-external.js +18 -18
  51. package/dist/cdn/{validate-C7Rn_Bns.js → validate-AZoD8BoR.js} +6 -2
  52. package/dist/cell-formatting.d.ts +9 -0
  53. package/dist/cell-formatting.js +52 -1
  54. package/dist/cell-render.js +25 -6
  55. package/dist/clipboard.d.ts +2 -2
  56. package/dist/clipboard.js +72 -23
  57. package/dist/columns.js +7 -2
  58. package/dist/conditional-formatting.d.ts +2 -0
  59. package/dist/conditional-formatting.js +2 -0
  60. package/dist/core.d.ts +22 -6
  61. package/dist/core.js +478 -40
  62. package/dist/editing.js +24 -0
  63. package/dist/export-format.d.ts +8 -0
  64. package/dist/export-format.js +23 -16
  65. package/dist/grid-messages.d.ts +4 -0
  66. package/dist/grid-messages.js +4 -0
  67. package/dist/headless.d.ts +5 -0
  68. package/dist/headless.js +12 -0
  69. package/dist/history.d.ts +21 -0
  70. package/dist/history.js +34 -4
  71. package/dist/index.d.ts +1 -1
  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 +6 -0
  77. package/dist/row-resize.js +22 -1
  78. package/dist/selection.js +1 -1
  79. package/dist/server-block-cache.d.ts +5 -0
  80. package/dist/server-block-cache.js +8 -0
  81. package/dist/server-data-source.d.ts +35 -3
  82. package/dist/server-data-source.js +11 -2
  83. package/dist/server.d.ts +1 -1
  84. package/dist/sparkline.js +7 -5
  85. package/dist/svgrid-wrapper.types.d.ts +33 -0
  86. package/dist/validate.js +9 -1
  87. package/dist/virtualization/column-virtualizer.d.ts +1 -0
  88. package/dist/virtualization/svelte-virtualizer.svelte.d.ts +1 -0
  89. package/dist/virtualization/svelte-virtualizer.svelte.js +1 -0
  90. package/dist/virtualization/virtualizer.d.ts +3 -0
  91. package/dist/virtualization/virtualizer.js +14 -1
  92. package/package.json +6 -1
  93. package/src/GridMenus.svelte +34 -4
  94. package/src/SvGrid.controller.svelte.ts +236 -57
  95. package/src/SvGrid.css +143 -7
  96. package/src/SvGrid.helpers.ts +2 -4
  97. package/src/SvGrid.svelte +223 -38
  98. package/src/SvGrid.types.ts +77 -4
  99. package/src/SvGridDropdown.svelte +20 -0
  100. package/src/SvModal.svelte +22 -0
  101. package/src/SvModal.test.ts +30 -1
  102. package/src/a11y/dismissable.test.ts +10 -1
  103. package/src/a11y/dismissable.ts +22 -2
  104. package/src/build-api.ts +85 -27
  105. package/src/cell-formatting.test.ts +17 -0
  106. package/src/cell-formatting.ts +49 -1
  107. package/src/cell-render.test.ts +24 -2
  108. package/src/cell-render.ts +24 -8
  109. package/src/clipboard.test.ts +107 -0
  110. package/src/clipboard.ts +62 -23
  111. package/src/columns.test.ts +4 -4
  112. package/src/columns.ts +7 -2
  113. package/src/conditional-formatting.test.ts +8 -0
  114. package/src/conditional-formatting.ts +3 -0
  115. package/src/core.sort.test.ts +86 -0
  116. package/src/core.tick-repair.test.ts +287 -0
  117. package/src/core.ts +478 -40
  118. package/src/editing.test.ts +52 -0
  119. package/src/editing.ts +25 -0
  120. package/src/export-dates.test.ts +114 -0
  121. package/src/export-format.ts +26 -14
  122. package/src/grid-messages.test.ts +7 -0
  123. package/src/grid-messages.ts +10 -0
  124. package/src/headless.ts +24 -0
  125. package/src/history.test.ts +49 -1
  126. package/src/history.ts +48 -2
  127. package/src/index.ts +2 -0
  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 +6 -0
  135. package/src/row-resize.ts +24 -1
  136. package/src/selection.ts +1 -1
  137. package/src/server-block-cache.test.ts +27 -0
  138. package/src/server-block-cache.ts +13 -0
  139. package/src/server-data-source.infinite.test.ts +62 -0
  140. package/src/server-data-source.test.ts +19 -0
  141. package/src/server-data-source.ts +48 -4
  142. package/src/server.ts +2 -0
  143. package/src/sparkline.test.ts +7 -0
  144. package/src/sparkline.ts +6 -4
  145. package/src/svgrid-wrapper.types.ts +32 -0
  146. package/src/svgrid.api.test.ts +21 -0
  147. package/src/svgrid.charting.test.ts +12 -4
  148. package/src/svgrid.context-menu.test.ts +12 -0
  149. package/src/svgrid.data-push.svelte.test.ts +94 -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 +1 -1
  153. package/src/svgrid.live-update-paths.svelte.test.ts +186 -0
  154. package/src/svgrid.menu-scroll-close.test.ts +10 -0
  155. package/src/svgrid.row-model-seam.svelte.test.ts +16 -0
  156. package/src/svgrid.row-pinning.test.ts +15 -1
  157. package/src/svgrid.selection-bar-seam.test.ts +1 -1
  158. package/src/svgrid.sticky-groups.svelte.test.ts +145 -0
  159. package/src/svgrid.upsell-license.test.ts +1 -1
  160. package/src/test-setup.ts +12 -0
  161. package/src/transaction.test.ts +58 -0
  162. package/src/validate.ts +7 -1
  163. package/src/virtualization/svelte-virtualizer.svelte.ts +1 -0
  164. package/src/virtualization/virtualizer.test.ts +44 -0
  165. package/src/virtualization/virtualizer.ts +13 -1
  166. package/dist/cdn/GridMenus-DLL-qZeu.js +0 -635
package/dist/editing.js CHANGED
@@ -133,6 +133,22 @@ export function createEditing(ctx) {
133
133
  // belongs to the same Ctrl+Z as the value.
134
134
  runHistoryGroup(ctx, () => commitEditingCell());
135
135
  }
136
+ /**
137
+ * True when a column with `rejectInvalid` refuses `value`: its `validate`
138
+ * hook returns `false` or a message. Without the flag nothing is refused.
139
+ */
140
+ function editRejected(column, value, rowData) {
141
+ const def = column?.columnDef;
142
+ if (!def?.rejectInvalid || typeof def.validate !== "function")
143
+ return false;
144
+ const verdict = def.validate({
145
+ value,
146
+ row: rowData,
147
+ rowIndex: ctx.internalData.indexOf(rowData),
148
+ column,
149
+ });
150
+ return verdict === false || (typeof verdict === "string" && verdict.length > 0);
151
+ }
136
152
  function commitEditingCell() {
137
153
  if (!ctx.editingCell)
138
154
  return;
@@ -165,6 +181,12 @@ export function createEditing(ctx) {
165
181
  columnId: ctx.editingCell.columnId,
166
182
  });
167
183
  }
184
+ // A refused edit closes the editor and leaves the row, the overlay,
185
+ // the history and the consumer untouched.
186
+ if (editRejected(column, finalValue, rowData)) {
187
+ ctx.editingCell = null;
188
+ return;
189
+ }
168
190
  rowData[column.columnDef.field] = finalValue;
169
191
  }
170
192
  const key = getCellKey(ctx.editingCell.rowId, ctx.editingCell.columnId);
@@ -764,6 +786,8 @@ export function createEditing(ctx) {
764
786
  columnId: column.id,
765
787
  })
766
788
  : parsed;
789
+ if (editRejected(column, finalValue, row.original))
790
+ continue;
767
791
  if (oldValue !== finalValue) {
768
792
  row.original[field] = finalValue;
769
793
  const key = getCellKey(fr.rowId, column.id);
@@ -21,6 +21,14 @@ export { computeColumnStat, contrastText, formatsNeedingStats, resolveCellFormat
21
21
  /** Best-effort coercion of a cell value to a Date. Accepts Date, epoch ms
22
22
  * numbers, and parseable date strings. Returns null when it isn't a date. */
23
23
  export declare function coerceExportDate(value: unknown): Date | null;
24
+ /**
25
+ * A Date as ISO 8601 in LOCAL time with its offset, e.g.
26
+ * `2024-01-15T00:00:00.000+02:00`. Text exports used `toISOString()`, which
27
+ * is UTC: local midnight on the 15th came out as `2024-01-14T22:00:00.000Z`
28
+ * east of UTC, and anything reading the date part got the wrong day (#103).
29
+ * Same instant, but the date part is the one the grid showed.
30
+ */
31
+ export declare function toLocalIsoString(d: Date): string;
24
32
  /**
25
33
  * Format a raw cell value to its on-screen display string using the column's
26
34
  * `format` config. Mirrors the grid's own numeric / currency / percent / date
@@ -1,4 +1,4 @@
1
- import { formatNumericWithConfig, getDateFormatter, resolveDatePattern, } from './cell-formatting.js';
1
+ import { formatNumericWithConfig, getDateFormatter, parseDateValue, resolveDatePattern, } from './cell-formatting.js';
2
2
  // Re-export the pure conditional-formatting engine here too, so the
3
3
  // Svelte-free `@svgrid/grid/format` subpath carries it for @svgrid/enterprise's
4
4
  // export code (which must not pull in SvGrid.svelte).
@@ -6,17 +6,24 @@ export { computeColumnStat, contrastText, formatsNeedingStats, resolveCellFormat
6
6
  /** Best-effort coercion of a cell value to a Date. Accepts Date, epoch ms
7
7
  * numbers, and parseable date strings. Returns null when it isn't a date. */
8
8
  export function coerceExportDate(value) {
9
- if (value instanceof Date)
10
- return Number.isNaN(value.getTime()) ? null : value;
11
- if (typeof value === 'number' && Number.isFinite(value)) {
12
- const d = new Date(value);
13
- return Number.isNaN(d.getTime()) ? null : d;
14
- }
15
- if (typeof value === 'string' && value.trim() !== '') {
16
- const d = new Date(value);
17
- return Number.isNaN(d.getTime()) ? null : d;
18
- }
19
- return null;
9
+ return parseDateValue(value);
10
+ }
11
+ /**
12
+ * A Date as ISO 8601 in LOCAL time with its offset, e.g.
13
+ * `2024-01-15T00:00:00.000+02:00`. Text exports used `toISOString()`, which
14
+ * is UTC: local midnight on the 15th came out as `2024-01-14T22:00:00.000Z`
15
+ * east of UTC, and anything reading the date part got the wrong day (#103).
16
+ * Same instant, but the date part is the one the grid showed.
17
+ */
18
+ export function toLocalIsoString(d) {
19
+ const pad = (n, w = 2) => String(Math.abs(n)).padStart(w, '0');
20
+ const offset = -d.getTimezoneOffset();
21
+ const sign = offset < 0 ? '-' : '+';
22
+ const year = d.getFullYear();
23
+ return ((year < 0 ? '-' + pad(year, 6) : pad(year, 4)) +
24
+ `-${pad(d.getMonth() + 1)}-${pad(d.getDate())}` +
25
+ `T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${pad(d.getMilliseconds(), 3)}` +
26
+ `${sign}${pad(Math.floor(Math.abs(offset) / 60))}:${pad(Math.abs(offset) % 60)}`);
20
27
  }
21
28
  /**
22
29
  * Format a raw cell value to its on-screen display string using the column's
@@ -190,7 +197,7 @@ function csvCell(value, delimiter) {
190
197
  const s = value == null
191
198
  ? ''
192
199
  : value instanceof Date
193
- ? value.toISOString()
200
+ ? toLocalIsoString(value)
194
201
  : String(value);
195
202
  if (s === '')
196
203
  return s;
@@ -231,7 +238,7 @@ const HTML_ESCAPE = {
231
238
  "'": '&#39;',
232
239
  };
233
240
  function htmlCell(value) {
234
- const s = value == null ? '' : value instanceof Date ? value.toISOString() : String(value);
241
+ const s = value == null ? '' : value instanceof Date ? toLocalIsoString(value) : String(value);
235
242
  return s.replace(/[&<>"']/g, (c) => HTML_ESCAPE[c]);
236
243
  }
237
244
  /**
@@ -311,7 +318,7 @@ export async function serializeJson(rows, fields, opts = {}) {
311
318
  }
312
319
  /** Escape a Markdown table cell: pipes and newlines would break the row. */
313
320
  function mdCell(value) {
314
- const s = value == null ? '' : value instanceof Date ? value.toISOString() : String(value);
321
+ const s = value == null ? '' : value instanceof Date ? toLocalIsoString(value) : String(value);
315
322
  return s.replace(/\\/g, '\\\\').replace(/\|/g, '\\|').replace(/\r?\n/g, '<br>');
316
323
  }
317
324
  /**
@@ -351,7 +358,7 @@ const XML_ESCAPE = {
351
358
  "'": '&apos;',
352
359
  };
353
360
  function xmlText(value) {
354
- const s = value == null ? '' : value instanceof Date ? value.toISOString() : String(value);
361
+ const s = value == null ? '' : value instanceof Date ? toLocalIsoString(value) : String(value);
355
362
  // Stripping control characters is the point: XML 1.0 forbids them outright,
356
363
  // so a stray \x00 from a data source would produce an unopenable file.
357
364
  // eslint-disable-next-line no-control-regex
@@ -76,6 +76,10 @@ export type GridChromeMessages = {
76
76
  chartRange: string;
77
77
  rowLoadFailed: string;
78
78
  rowLoadRetry: string;
79
+ expandGroup: string;
80
+ collapseGroup: string;
81
+ openDetail: string;
82
+ closeDetail: string;
79
83
  pivotUpsellTitle: string;
80
84
  pivotUpsellBody: string;
81
85
  ganttUpsellTitle: string;
@@ -86,6 +86,10 @@ export const defaultGridMessages = {
86
86
  chartRange: 'Chart selected range',
87
87
  rowLoadFailed: 'Could not load these rows.',
88
88
  rowLoadRetry: 'Retry',
89
+ expandGroup: 'Expand group',
90
+ collapseGroup: 'Collapse group',
91
+ openDetail: 'Open details',
92
+ closeDetail: 'Close details',
89
93
  pivotUpsellTitle: 'Pivot mode',
90
94
  pivotUpsellBody: 'Pivot mode is an Enterprise feature. Install @svgrid/enterprise and call enablePivot() to use it.',
91
95
  ganttUpsellTitle: 'Gantt view',
@@ -27,3 +27,8 @@ export { columnFilteringFeature, columnGroupingFeature, createCoreRowModel, crea
27
27
  export { createGrid, createSvGrid, type SvelteGrid } from './createGrid.svelte.js';
28
28
  export { createGridState, createSvGridState } from './createGridState.svelte.js';
29
29
  export { subscribeGrid, subscribeSvGrid } from './subscribe.js';
30
+ export { createVirtualizer } from './virtualization/virtualizer.js';
31
+ export { createSvelteVirtualizer } from './virtualization/svelte-virtualizer.svelte.js';
32
+ export { createColumnVirtualizer } from './virtualization/column-virtualizer.js';
33
+ export type { VirtualItem, VirtualizerOptions, VirtualizerState } from './virtualization/types.js';
34
+ export { getGridCellDomId, getGridCellA11yProps, getGridHeaderA11yProps, getGridRootA11yProps, getGridRowA11yProps, type GridCellA11yInput, type GridColumnA11yInput, type GridSortDirection, } from './a11y.js';
package/dist/headless.js CHANGED
@@ -27,3 +27,15 @@ export { columnFilteringFeature, columnGroupingFeature, createCoreRowModel, crea
27
27
  export { createGrid, createSvGrid } from './createGrid.svelte.js';
28
28
  export { createGridState, createSvGridState } from './createGridState.svelte.js';
29
29
  export { subscribeGrid, subscribeSvGrid } from './subscribe.js';
30
+ // The virtualizers are pure window arithmetic (scroll offset in, visible
31
+ // range out) with no DOM of their own, and a custom renderer over 50k rows
32
+ // needs one. They were only on the barrel, so the Headless demos imported
33
+ // the barrel to reach them and paid for the whole thing.
34
+ export { createVirtualizer } from './virtualization/virtualizer.js';
35
+ export { createSvelteVirtualizer } from './virtualization/svelte-virtualizer.svelte.js';
36
+ export { createColumnVirtualizer } from './virtualization/column-virtualizer.js';
37
+ // The WAI-ARIA attribute factories: plain objects to spread onto your own
38
+ // markup (`role`, `aria-sort`, `aria-rowindex`, ...). No DOM is touched here
39
+ // either; a custom renderer that wants keyboard navigation reads
40
+ // `activeCell` from the store and spreads these.
41
+ export { getGridCellDomId, getGridCellA11yProps, getGridHeaderA11yProps, getGridRootA11yProps, getGridRowA11yProps, } from './a11y.js';
package/dist/history.d.ts CHANGED
@@ -38,6 +38,15 @@ export type HistoryStep = {
38
38
  undo: () => void;
39
39
  redo: () => void;
40
40
  };
41
+ /**
42
+ * The consumer's own mark on the step, copied from `api.setHistoryTag` at
43
+ * the moment it is recorded. The grid never reads it: it is there for a
44
+ * consumer that shows one of several data sets through the grid and has
45
+ * to know which one a step belongs to before it lets Ctrl+Z apply it (a
46
+ * workbook with several sheets, say). Kept as given, so an object tag
47
+ * compares by identity. Not serialised.
48
+ */
49
+ tag?: unknown;
41
50
  };
42
51
  /** Ctx surface these helpers touch. The controller satisfies it structurally. */
43
52
  type HistoryCtx = {
@@ -49,8 +58,20 @@ type HistoryCtx = {
49
58
  * inside it joins that group, so a command does not have to thread a group
50
59
  * id down through the writers it calls. */
51
60
  historyGroupId: string | undefined;
61
+ /** Stamped on every step recorded while it is set; see `HistoryStep.tag`. */
62
+ historyTag?: unknown;
52
63
  applyHistoryStep(step: HistoryStep, direction: 'undo' | 'redo'): void;
53
64
  };
65
+ /** What the next undo would apply: the tag of the step Ctrl+Z takes next
66
+ * (a group carries one tag, so its last step's is the group's). Null when
67
+ * there is nothing to undo. */
68
+ export declare function peekUndoHistory(ctx: HistoryCtx): {
69
+ tag: unknown;
70
+ } | null;
71
+ /** The redo counterpart of `peekUndoHistory`. */
72
+ export declare function peekRedoHistory(ctx: HistoryCtx): {
73
+ tag: unknown;
74
+ } | null;
54
75
  /** Undo one action. Returns false when there is nothing left to undo. */
55
76
  export declare function undoHistory(ctx: HistoryCtx): boolean;
56
77
  /** Redo one action. Returns false when there is nothing left to redo. */
package/dist/history.js CHANGED
@@ -41,6 +41,34 @@ function groupEnd(history, start) {
41
41
  i += 1;
42
42
  return i;
43
43
  }
44
+ /**
45
+ * The last `limit` ACTIONS of a history, whole groups kept. The cap used
46
+ * to count steps, and a sort of 5,000 rows is 20,000 steps in one group:
47
+ * it cut the group to its last 200 cells, so Ctrl+Z put a fifth of a
48
+ * column back and left the rest sorted. An action is one press of Ctrl+Z,
49
+ * so that is what the buffer counts; the steps inside are the cost of it.
50
+ */
51
+ function lastActions(history, limit) {
52
+ let i = history.length;
53
+ let actions = 0;
54
+ while (i > 0 && actions < limit) {
55
+ i = groupStart(history, i - 1);
56
+ actions += 1;
57
+ }
58
+ return i === 0 ? history : history.slice(i);
59
+ }
60
+ /** What the next undo would apply: the tag of the step Ctrl+Z takes next
61
+ * (a group carries one tag, so its last step's is the group's). Null when
62
+ * there is nothing to undo. */
63
+ export function peekUndoHistory(ctx) {
64
+ const step = ctx.historyPtr >= 0 ? ctx.history[ctx.historyPtr] : undefined;
65
+ return step ? { tag: step.tag } : null;
66
+ }
67
+ /** The redo counterpart of `peekUndoHistory`. */
68
+ export function peekRedoHistory(ctx) {
69
+ const step = ctx.historyPtr < ctx.history.length - 1 ? ctx.history[ctx.historyPtr + 1] : undefined;
70
+ return step ? { tag: step.tag } : null;
71
+ }
44
72
  /** Undo one action. Returns false when there is nothing left to undo. */
45
73
  export function undoHistory(ctx) {
46
74
  if (ctx.historyPtr < 0)
@@ -94,11 +122,13 @@ export function pushHistory(ctx, steps, groupId) {
94
122
  // Outside one, a group of one is just a step. Tagging it would make undo do
95
123
  // the same work through a slower path and would show up in serialized state.
96
124
  const id = ctx.historyGroupId ?? (groupId && steps.length > 1 ? groupId : undefined);
125
+ const tag = ctx.historyTag;
97
126
  let next = ctx.history.slice(0, ctx.historyPtr + 1);
98
- for (const step of steps)
99
- next.push(id ? { ...step, groupId: id } : step);
100
- if (next.length > ctx.UNDO_LIMIT)
101
- next = next.slice(next.length - ctx.UNDO_LIMIT);
127
+ for (const step of steps) {
128
+ const stamped = tag === undefined ? step : { ...step, tag };
129
+ next.push(id ? { ...stamped, groupId: id } : stamped);
130
+ }
131
+ next = lastActions(next, ctx.UNDO_LIMIT);
102
132
  ctx.history = next;
103
133
  ctx.historyPtr = ctx.history.length - 1;
104
134
  ctx.historyVersion += 1;
package/dist/index.d.ts CHANGED
@@ -213,7 +213,7 @@ export { resolveColumnTypes, inferCellDataType, type CellDataType } from './colu
213
213
  export { computeColumnGroupMeta, hiddenLeavesForCollapse, type ColumnGroupMeta, type ColumnGroupShow, } from './column-groups.js';
214
214
  export { createHyperFormulaSheet, type HyperFormulaSheet, type HyperFormulaSheetConfig, type HyperFormulaInstance, } from './hyperformula-adapter.js';
215
215
  export { createCollaboration, broadcastChannelTransport, type CollabUser, type CollabCell, type CollabPresence, type CollabMessage, type CollabTransport, type Collaboration, } from './collaboration.js';
216
- export { createServerDataSource, type ServerDataSource, type ServerSelectionRule, type ServerRequest, type ServerResult, type ServerController, type ServerControllerOptions, type ServerState, type ServerSortModel, type ServerFilterModel, type ServerAggregation, type ServerGroupRow, type ServerLeafRow, type ServerMoreRow, type ServerFooterRow, type ServerSkeletonRow, type ServerGrandTotalRow, type ServerPlaceholderRow, type ServerDisplayRow, } from './server-data-source.js';
216
+ 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';
217
217
  export { createBlockCache, createRowPlaceholder, rowPlaceholderState, type BlockCache, type BlockCacheOptions, type BlockCacheState, type BlockFetchResult, type BlockState, } from './server-block-cache.js';
218
218
  export { toServerFilterColumns, type GridRowModel, type GridFilterState, type GridRowModelSort, } from './row-model.js';
219
219
  export { createNamedViews, memoryViews, localStorageViews, attachAutoSavedView, type SavedView, type ViewStorage, type NamedViews, type AutoSavedViewOptions, } from './named-views.js';
@@ -166,6 +166,28 @@ export function createKeyboard(ctx) {
166
166
  row.toggleExpanded?.();
167
167
  return;
168
168
  }
169
+ // The same key on a server-side tree: a group opens or closes, a leaf
170
+ // opens or closes its detail panel when the grid draws one.
171
+ const sg = ctx.props.serverGroup;
172
+ if (event.ctrlKey && sg && row) {
173
+ const data = row.original;
174
+ if (sg.isGroup(data)) {
175
+ sg.onToggle(data);
176
+ return;
177
+ }
178
+ if (sg.toggleDetail && ctx.props.renderDetailRow && (sg.hasDetail?.(data) ?? true)) {
179
+ sg.toggleDetail(data);
180
+ return;
181
+ }
182
+ }
183
+ // ...and on a client-side master-detail: the app's own toggle.
184
+ if (event.ctrlKey && row && ctx.props.onDetailToggle && ctx.props.renderDetailRow) {
185
+ const data = row.original;
186
+ if (!ctx.props.isDetailRow?.(data, current.rowIndex) && (ctx.props.hasDetail?.(data) ?? true)) {
187
+ ctx.props.onDetailToggle(data, current.rowIndex);
188
+ return;
189
+ }
190
+ }
169
191
  if (column?.columnDef.editorType === "checkbox") {
170
192
  ctx.toggleBooleanCell(current.rowIndex, current.colIndex);
171
193
  return;
@@ -257,7 +279,8 @@ export function createKeyboard(ctx) {
257
279
  ctx.setSelection(next.rowIndex, next.colIndex);
258
280
  }
259
281
  function onWindowKeydown(event) {
260
- if (event.key === "Escape" && (ctx.columnMenuFor || ctx.operatorMenuFor)) {
282
+ if (event.key === "Escape" &&
283
+ (ctx.columnMenuFor || ctx.operatorMenuFor || ctx.filterMenuFor || ctx.inSuggestFor || ctx.chooseColumnsPos)) {
261
284
  ctx.closeMenus();
262
285
  }
263
286
  }
package/dist/keyboard.js CHANGED
@@ -37,6 +37,10 @@ export function getKeyboardIntent(event) {
37
37
  // Tab → move right (Shift+Tab → left), wraps at row boundaries
38
38
  // Delete → clear contents of the selected cells
39
39
  // F2 / Space → start editing the active cell
40
+ // Ctrl+Enter acts on the row rather than moving: a group opens or closes,
41
+ // a row with a detail panel shows or hides it (see the activate branch).
42
+ if (event.key === 'Enter' && (event.ctrlKey || event.metaKey))
43
+ return 'activate';
40
44
  if (event.key === 'Enter')
41
45
  return event.shiftKey ? 'moveUp' : 'moveDown';
42
46
  if (event.key === 'Tab')
@@ -3,7 +3,7 @@ export type TouchDragDeps = {
3
3
  ctx: any;
4
4
  managed: () => boolean;
5
5
  originalAt: (rowIndex: number) => unknown;
6
- commitDropOnRow: (rowIndex: number, side: "before" | "after") => void;
6
+ commitDropOnRow: (rowIndex: number, side: "before" | "after" | "into") => void;
7
7
  clearIndicators: () => void;
8
8
  cancelPendingLeave: () => void;
9
9
  /** Publish the dragged row on the shared cross-grid bus. */
package/dist/row-drag.js CHANGED
@@ -98,7 +98,27 @@ export function createRowDrag(ctx) {
98
98
  e.dataTransfer.dropEffect = "move";
99
99
  const rect = e.currentTarget.getBoundingClientRect();
100
100
  ctx.rowDropIndex = rowIndex;
101
- ctx.rowDropSide = e.clientY < rect.top + rect.height / 2 ? "before" : "after";
101
+ ctx.rowDropSide = sideAt(rowIndex, rect, e.clientY);
102
+ }
103
+ /**
104
+ * Which edge the pointer is on. A custom drop handler also gets the middle
105
+ * of a group or tree row as "into" (the row becomes its child); the
106
+ * managed reorder only knows before and after.
107
+ */
108
+ function sideAt(rowIndex, rect, y) {
109
+ const rel = (y - rect.top) / Math.max(rect.height, 1);
110
+ if (ctx.props.onRowDrop && rel >= 0.25 && rel <= 0.75 && canNest(rowIndex))
111
+ return "into";
112
+ return rel < 0.5 ? "before" : "after";
113
+ }
114
+ function canNest(rowIndex) {
115
+ const row = ctx.allRows[rowIndex];
116
+ if (!row)
117
+ return false;
118
+ const sg = ctx.props.serverGroup;
119
+ if (sg)
120
+ return !!sg.isGroup(row.original);
121
+ return typeof row.getCanExpand === "function" && row.getCanExpand();
102
122
  }
103
123
  function onRowDragLeave(rowIndex) {
104
124
  // A leave from a row that is not the current target is already stale.
@@ -137,6 +157,15 @@ export function createRowDrag(ctx) {
137
157
  const sourceGridId = bus.gridId;
138
158
  const sameGrid = sourceGridId === gridId;
139
159
  clearIndicators();
160
+ // A custom handler owns the drop: the grid reports it and leaves its
161
+ // data alone (a row model's rows are not the grid's to splice).
162
+ if (sameGrid && ctx.props.onRowDrop) {
163
+ bus = null;
164
+ ctx.props.onRowDrop({ row: dragged, target, targetIndex: rowIndex, side });
165
+ return;
166
+ }
167
+ if (side === "into")
168
+ side = "after";
140
169
  const data = ctx.internalData.slice();
141
170
  if (sameGrid) {
142
171
  const from = data.indexOf(dragged);
@@ -183,6 +212,11 @@ export function createRowDrag(ctx) {
183
212
  const sourceGridId = bus.gridId;
184
213
  const sameGrid = sourceGridId === gridId;
185
214
  clearIndicators();
215
+ if (sameGrid && ctx.props.onRowDrop) {
216
+ bus = null;
217
+ ctx.props.onRowDrop({ row: dragged, target: null, targetIndex: null, side: "after" });
218
+ return;
219
+ }
186
220
  const data = ctx.internalData.slice();
187
221
  if (sameGrid) {
188
222
  const from = data.indexOf(dragged);
@@ -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?: {
@@ -31,6 +31,19 @@ export function rowResize(node, opts) {
31
31
  let drag = null;
32
32
  /** The last press on a strip, for telling a double-click from two drags. */
33
33
  let lastPress = null;
34
+ /**
35
+ * A focusable `role="separator"` is a splitter, and a splitter carries its
36
+ * position: axe's `aria-required-attr` fails one without `aria-valuenow`,
37
+ * and a screen reader reading the strip says nothing useful without it. The
38
+ * value is the row's height in pixels, between the action's min and max.
39
+ */
40
+ function setStripValue(strip, height) {
41
+ const min = current.min ?? 20;
42
+ const max = current.max ?? 320;
43
+ strip.setAttribute('aria-valuemin', String(min));
44
+ strip.setAttribute('aria-valuemax', String(max));
45
+ strip.setAttribute('aria-valuenow', String(Math.round(Math.max(min, Math.min(max, height || min)))));
46
+ }
34
47
  function rowIndexOf(tr) {
35
48
  const dataRow = tr.querySelector('[data-svgrid-row]');
36
49
  const val = dataRow?.dataset.svgridRow;
@@ -43,6 +56,7 @@ export function rowResize(node, opts) {
43
56
  const max = current.max ?? 320;
44
57
  const next = Math.max(min, Math.min(max, drag.startHeight + (e.clientY - drag.startY)));
45
58
  drag.trEl.style.height = `${Math.round(next)}px`;
59
+ setStripValue(drag.strip, next);
46
60
  current.onResizeMove?.(drag.rowIndex, Math.round(next));
47
61
  }
48
62
  function onPointerUp(e) {
@@ -94,6 +108,7 @@ export function rowResize(node, opts) {
94
108
  const max = current.max ?? 320;
95
109
  const next = Math.round(Math.max(min, Math.min(max, tr.getBoundingClientRect().height + delta)));
96
110
  tr.style.height = `${next}px`;
111
+ setStripValue(t, next);
97
112
  current.onResizeMove?.(rowIndex, next);
98
113
  current.onResize(rowIndex, next);
99
114
  }
@@ -164,8 +179,13 @@ export function rowResize(node, opts) {
164
179
  : null);
165
180
  if (!gutter)
166
181
  continue;
167
- if (gutter.querySelector(`:scope > .${STRIP_CLASS}`))
182
+ const already = gutter.querySelector(`:scope > .${STRIP_CLASS}`);
183
+ if (already) {
184
+ // The row may have been resized from outside the action (a saved
185
+ // height, a double-click autofit); keep the separator's value honest.
186
+ setStripValue(already, tr.getBoundingClientRect().height);
168
187
  continue;
188
+ }
169
189
  // Anchor the strip to the gutter cell + allow it to overflow
170
190
  // downward into the row gap so the hit-area straddles the
171
191
  // row boundary the way Excel does.
@@ -180,6 +200,7 @@ export function rowResize(node, opts) {
180
200
  // A separator used as an interactive splitter has to be focusable so
181
201
  // keyboard-only users can reach it and drive `onKeyDown` (#79).
182
202
  strip.tabIndex = 0;
203
+ setStripValue(strip, tr.getBoundingClientRect().height);
183
204
  // Inline the geometry + pointer-events so the strip works even
184
205
  // before SvGrid.css is parsed; the hover tint still comes from
185
206
  // the stylesheet. We keep the strip fully inside the gutter
package/dist/selection.js CHANGED
@@ -522,7 +522,7 @@ export function createSelection(ctx) {
522
522
  // Also commit a fill-handle drag if one was in progress - the user
523
523
  // released the mouse, time to apply the pattern.
524
524
  if (ctx.fillDrag)
525
- ctx.onFillPointerUp();
525
+ ctx.onFillPointerUp(event);
526
526
  if (ctx.moveDrag)
527
527
  ctx.onMovePointerUp(event);
528
528
  }
@@ -161,6 +161,11 @@ export type BlockCache<TData> = {
161
161
  lastRowKnown(): boolean;
162
162
  /** Re-fetch the blocks that failed. */
163
163
  retryFailed(): void;
164
+ /**
165
+ * Re-fetch the failed block holding `rowIndex`, and only that one; the
166
+ * Retry on a failed row. A row whose block has not failed is a no-op.
167
+ */
168
+ retryFailedAt(rowIndex: number): void;
164
169
  /**
165
170
  * Re-fetch the loaded blocks in place, keeping the row count and scroll
166
171
  * position. What you want after a mutation lands on the server.
@@ -353,6 +353,14 @@ export function createBlockCache(options) {
353
353
  if (any)
354
354
  emit();
355
355
  },
356
+ retryFailedAt(rowIndex) {
357
+ const index = blockOf(Math.max(0, rowIndex));
358
+ if (blocks.get(index)?.status !== 'failed')
359
+ return;
360
+ blocks.delete(index);
361
+ ensureBlock(index);
362
+ emit();
363
+ },
356
364
  refresh() {
357
365
  // Keep the count and the scroll position: drop the DATA, re-request the
358
366
  // same blocks. The user sees skeletons where they were, not a jump home.
@@ -60,10 +60,18 @@ export type ServerFilterModel = {
60
60
  */
61
61
  expression?: GridPredicateExpr;
62
62
  };
63
- /** A value column to roll up per group. */
63
+ /** The aggregate functions every backend helper understands. */
64
+ export type ServerAggFn = 'sum' | 'avg' | 'min' | 'max' | 'count';
65
+ /**
66
+ * One value column to roll up per group. `fn` is one of the five built-ins
67
+ * or any name your backend knows (`median`, `countDistinct`, a domain
68
+ * aggregate): the request carries it untouched. The SvelteKit planner emits
69
+ * only the built-ins plus the names it was told to allow, since `fn` ends
70
+ * up in SQL.
71
+ */
64
72
  export type ServerAggregation = {
65
73
  col: string;
66
- fn: 'sum' | 'avg' | 'min' | 'max' | 'count';
74
+ fn: ServerAggFn | (string & {});
67
75
  };
68
76
  /**
69
77
  * One range of rows as the grid asks for it. A flat request carries the
@@ -121,6 +129,16 @@ export type ServerRequest = {
121
129
  * transport posts the whole request to your endpoint.
122
130
  */
123
131
  context?: unknown;
132
+ /**
133
+ * Aborted when the grid no longer wants the answer: the block was purged
134
+ * or evicted, the sort or filter changed, a group was collapsed, the
135
+ * controller was disposed. Hand it to `fetch` and a query the user has
136
+ * scrolled past is cancelled at the server instead of completing for
137
+ * nothing; a rejection carrying the abort is ignored by the grid. Not
138
+ * serialisable - the SvelteKit transport reads it and leaves it out of
139
+ * the body.
140
+ */
141
+ signal?: AbortSignal;
124
142
  };
125
143
  /**
126
144
  * A group row in the server-side group/tree model - one distinct key at a
@@ -227,12 +245,26 @@ export type ServerPlaceholderRow = {
227
245
  readonly kind: 'placeholder';
228
246
  readonly state: 'loading' | 'failed';
229
247
  };
248
+ /**
249
+ * The detail panel under a leaf whose detail is open: master-detail on a
250
+ * server-side row model. The grid draws it through `renderDetailRow`
251
+ * (mark it with `isDetailRow`); `master` is the leaf it belongs to.
252
+ */
253
+ export type ServerDetailRow<TData> = {
254
+ kind: 'detail';
255
+ id: string;
256
+ level: number;
257
+ route?: string[];
258
+ /** The leaf's id, as `getRowId` names it. */
259
+ masterId: string;
260
+ master: TData;
261
+ };
230
262
  /**
231
263
  * Every row a server row model can put on screen. Each carries an `id` and
232
264
  * a `level`. The block-cached model adds {@link ServerPlaceholderRow} for
233
265
  * rows not yet loaded - see its own `ServerRowModelDisplayRow`.
234
266
  */
235
- export type ServerDisplayRow<TData> = ServerGroupRow<TData> | ServerLeafRow<TData> | ServerMoreRow | ServerFooterRow<TData> | ServerSkeletonRow | ServerGrandTotalRow<TData>;
267
+ export type ServerDisplayRow<TData> = ServerGroupRow<TData> | ServerLeafRow<TData> | ServerDetailRow<TData> | ServerMoreRow | ServerFooterRow<TData> | ServerSkeletonRow | ServerGrandTotalRow<TData>;
236
268
  /**
237
269
  * What `getRows` answers with: the rows for the requested range and the
238
270
  * count after filtering, plus the grand total and the pivot fields when
@@ -30,6 +30,8 @@ export function createServerDataSource(source, options) {
30
30
  // Monotonic request id: only the latest fetch is allowed to land, so a slow
31
31
  // response for an old sort/filter can't clobber a newer one.
32
32
  let requestSeq = 0;
33
+ /** The page fetch in flight; a newer one, or dispose, aborts it. */
34
+ let pageAbort = null;
33
35
  let disposed = false;
34
36
  // Once per controller: a misconfigured backend would otherwise log on every
35
37
  // page, scroll and filter change.
@@ -65,7 +67,7 @@ export function createServerDataSource(source, options) {
65
67
  maxConcurrentRequests: options.maxConcurrentRequests,
66
68
  blockLoadDebounceMs: options.blockLoadDebounceMs,
67
69
  initialRowCount: options.initialRowCount,
68
- fetch: async (startRow, endRow) => {
70
+ fetch: async (startRow, endRow, signal) => {
69
71
  const result = await source.getRows({
70
72
  startRow,
71
73
  endRow,
@@ -78,6 +80,7 @@ export function createServerDataSource(source, options) {
78
80
  groupBy: [],
79
81
  groupKeys: [],
80
82
  aggregations: [],
83
+ signal,
81
84
  });
82
85
  noteExpressionApplied(result);
83
86
  return { rows: result.rows, rowCount: result.rowCount };
@@ -121,6 +124,9 @@ export function createServerDataSource(source, options) {
121
124
  if (disposed)
122
125
  return;
123
126
  const id = ++requestSeq;
127
+ pageAbort?.abort();
128
+ const controller = new AbortController();
129
+ pageAbort = controller;
124
130
  state.loading = true;
125
131
  state.error = null;
126
132
  emit();
@@ -138,6 +144,7 @@ export function createServerDataSource(source, options) {
138
144
  groupBy: [],
139
145
  groupKeys: [],
140
146
  aggregations: [],
147
+ signal: controller.signal,
141
148
  });
142
149
  if (disposed || id !== requestSeq)
143
150
  return; // stale
@@ -322,7 +329,7 @@ export function createServerDataSource(source, options) {
322
329
  getRowId: options.getRowId ? (row) => options.getRowId(row) : undefined,
323
330
  // Only infinite mode has unloaded rows to stand in for.
324
331
  rowPlaceholder: infinite ? (row) => rowPlaceholderState(row) : undefined,
325
- retryRow: infinite ? () => cache?.retryFailed() : undefined,
332
+ retryRow: infinite ? (_row, rowIndex) => cache?.retryFailedAt(rowIndex) : undefined,
326
333
  /**
327
334
  * A getter, not a snapshot: the grid re-reads this after every change
328
335
  * notification, and a frozen object would leave the pager on page 1
@@ -392,6 +399,8 @@ export function createServerDataSource(source, options) {
392
399
  getState: () => ({ ...state }),
393
400
  dispose() {
394
401
  disposed = true;
402
+ pageAbort?.abort();
403
+ pageAbort = null;
395
404
  cache?.dispose();
396
405
  cache = null;
397
406
  // An in-flight fetch's resolution short-circuits on `disposed`, so it