@svgrid/grid 3.0.5 → 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 (148) hide show
  1. package/README.md +28 -7
  2. package/dist/GridMenus.svelte +34 -4
  3. package/dist/SvGrid.controller.svelte.d.ts +21 -3
  4. package/dist/SvGrid.controller.svelte.js +205 -50
  5. package/dist/SvGrid.css +127 -2
  6. package/dist/SvGrid.svelte +223 -38
  7. package/dist/SvGrid.types.d.ts +77 -4
  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 +81 -31
  15. package/dist/cdn/{GridMenus-CID62rlI.js → GridMenus-C-NtjlrJ.js} +150 -141
  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-CGoWz-87.js → server-block-cache-DIqb3VZn.js} +4 -0
  45. package/dist/cdn/{src-jcFMkBnN.js → src-CgoeSM8k.js} +4697 -4131
  46. package/dist/cdn/{src-Bklofsad.js → src-DfwxB1TH.js} +4727 -4161
  47. package/dist/cdn/svgrid.js +17 -17
  48. package/dist/cdn/svgrid.svelte-external.js +17 -17
  49. package/dist/cdn/{validate-C7Rn_Bns.js → validate-AZoD8BoR.js} +6 -2
  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 +365 -25
  59. package/dist/editing.js +24 -0
  60. package/dist/grid-messages.d.ts +4 -0
  61. package/dist/grid-messages.js +4 -0
  62. package/dist/headless.d.ts +5 -0
  63. package/dist/headless.js +12 -0
  64. package/dist/history.d.ts +21 -0
  65. package/dist/history.js +34 -4
  66. package/dist/index.d.ts +1 -1
  67. package/dist/keyboard-handlers.js +24 -1
  68. package/dist/keyboard.js +4 -0
  69. package/dist/row-drag-touch.d.ts +1 -1
  70. package/dist/row-drag.js +35 -1
  71. package/dist/row-model.d.ts +6 -0
  72. package/dist/row-resize.js +22 -1
  73. package/dist/selection.js +1 -1
  74. package/dist/server-block-cache.d.ts +5 -0
  75. package/dist/server-block-cache.js +8 -0
  76. package/dist/server-data-source.d.ts +35 -3
  77. package/dist/server-data-source.js +11 -2
  78. package/dist/server.d.ts +1 -1
  79. package/dist/sparkline.js +7 -5
  80. package/dist/svgrid-wrapper.types.d.ts +33 -0
  81. package/dist/validate.js +9 -1
  82. package/package.json +6 -1
  83. package/src/GridMenus.svelte +34 -4
  84. package/src/SvGrid.controller.svelte.ts +206 -53
  85. package/src/SvGrid.css +127 -2
  86. package/src/SvGrid.svelte +223 -38
  87. package/src/SvGrid.types.ts +77 -4
  88. package/src/SvGridDropdown.svelte +20 -0
  89. package/src/SvModal.svelte +22 -0
  90. package/src/SvModal.test.ts +30 -1
  91. package/src/a11y/dismissable.test.ts +10 -1
  92. package/src/a11y/dismissable.ts +22 -2
  93. package/src/build-api.ts +85 -27
  94. package/src/cell-formatting.test.ts +17 -0
  95. package/src/cell-formatting.ts +18 -1
  96. package/src/cell-render.test.ts +24 -2
  97. package/src/cell-render.ts +24 -8
  98. package/src/clipboard.test.ts +107 -0
  99. package/src/clipboard.ts +62 -23
  100. package/src/columns.test.ts +4 -4
  101. package/src/columns.ts +7 -2
  102. package/src/conditional-formatting.test.ts +8 -0
  103. package/src/conditional-formatting.ts +3 -0
  104. package/src/core.tick-repair.test.ts +287 -0
  105. package/src/core.ts +374 -26
  106. package/src/editing.test.ts +52 -0
  107. package/src/editing.ts +25 -0
  108. package/src/grid-messages.test.ts +7 -0
  109. package/src/grid-messages.ts +10 -0
  110. package/src/headless.ts +24 -0
  111. package/src/history.test.ts +49 -1
  112. package/src/history.ts +48 -2
  113. package/src/index.ts +2 -0
  114. package/src/keyboard-handlers.coverage.test.ts +65 -0
  115. package/src/keyboard-handlers.ts +26 -1
  116. package/src/keyboard.ts +3 -0
  117. package/src/row-drag-touch.ts +2 -2
  118. package/src/row-drag.test.ts +51 -0
  119. package/src/row-drag.ts +35 -2
  120. package/src/row-model.ts +6 -0
  121. package/src/row-resize.ts +24 -1
  122. package/src/selection.ts +1 -1
  123. package/src/server-block-cache.test.ts +27 -0
  124. package/src/server-block-cache.ts +13 -0
  125. package/src/server-data-source.infinite.test.ts +62 -0
  126. package/src/server-data-source.test.ts +19 -0
  127. package/src/server-data-source.ts +48 -4
  128. package/src/server.ts +2 -0
  129. package/src/sparkline.test.ts +7 -0
  130. package/src/sparkline.ts +6 -4
  131. package/src/svgrid-wrapper.types.ts +32 -0
  132. package/src/svgrid.api.test.ts +21 -0
  133. package/src/svgrid.charting.test.ts +12 -4
  134. package/src/svgrid.context-menu.test.ts +12 -0
  135. package/src/svgrid.detail-rows.svelte.test.ts +187 -0
  136. package/src/svgrid.filter-menu-scroll.test.ts +9 -0
  137. package/src/svgrid.gantt-seam.test.ts +1 -1
  138. package/src/svgrid.live-update-paths.svelte.test.ts +184 -0
  139. package/src/svgrid.menu-scroll-close.test.ts +10 -0
  140. package/src/svgrid.row-model-seam.svelte.test.ts +16 -0
  141. package/src/svgrid.row-pinning.test.ts +15 -1
  142. package/src/svgrid.selection-bar-seam.test.ts +1 -1
  143. package/src/svgrid.sticky-groups.svelte.test.ts +145 -0
  144. package/src/svgrid.upsell-license.test.ts +1 -1
  145. package/src/test-setup.ts +12 -0
  146. package/src/transaction.test.ts +58 -0
  147. package/src/validate.ts +7 -1
  148. package/dist/cdn/GridMenus-DLL-qZeu.js +0 -635
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
package/dist/server.d.ts CHANGED
@@ -8,6 +8,6 @@
8
8
  * a worker feeding a block cache, or `@svgrid/enterprise`'s own row model,
9
9
  * whose unit tests run without the Svelte compiler.
10
10
  */
11
- 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';
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
12
  export { createBlockCache, createRowPlaceholder, rowPlaceholderState, type BlockCache, type BlockCacheOptions, type BlockCacheState, type BlockFetchResult, type BlockState, } from './server-block-cache.js';
13
13
  export { toServerFilterColumns, type GridRowModel, type GridFilterState, type GridRowModelSort, } 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
  *
package/dist/validate.js CHANGED
@@ -21,7 +21,15 @@ export function validateGridConfig(input) {
21
21
  // (a key absent from row 0 but present later) does not trip it. A row
22
22
  // model shows placeholder rows before its first block lands; those carry
23
23
  // no data keys at all and say nothing about the columns.
24
- const sample = (input.data ?? []).filter((row) => !rowPlaceholderState(row)).slice(0, FIELD_SAMPLE_ROWS);
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
+ }
25
33
  // Under server-side grouping the first rows are group rows, which carry
26
34
  // a key and aggregates and none of the leaf fields; nothing to judge by.
27
35
  if (sample.length && !input.serverGroup) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svgrid/grid",
3
- "version": "3.0.5",
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,6 +94,11 @@
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",
@@ -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);