@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.
- package/README.md +28 -7
- package/dist/GridMenus.svelte +34 -4
- package/dist/SvGrid.controller.svelte.d.ts +21 -3
- package/dist/SvGrid.controller.svelte.js +205 -50
- package/dist/SvGrid.css +127 -2
- package/dist/SvGrid.svelte +223 -38
- package/dist/SvGrid.types.d.ts +77 -4
- package/dist/SvGridDropdown.svelte +20 -0
- package/dist/SvGridDropdown.svelte.d.ts +12 -0
- package/dist/SvModal.svelte +22 -0
- package/dist/SvModal.svelte.d.ts +7 -0
- package/dist/a11y/dismissable.d.ts +10 -0
- package/dist/a11y/dismissable.js +24 -2
- package/dist/build-api.js +81 -31
- package/dist/cdn/{GridMenus-CID62rlI.js → GridMenus-C-NtjlrJ.js} +150 -141
- package/dist/cdn/GridMenus-TssHej3q.js +644 -0
- package/dist/cdn/{SvChartMenu-FBSMINA6.js → SvChartMenu-BFSTpvB-.js} +1 -1
- package/dist/cdn/{SvChartMenu-Bl6PBbkT.js → SvChartMenu-BZ1Iwv0y.js} +1 -1
- package/dist/cdn/{SvDateTimePicker-DQwt4UAs.js → SvDateTimePicker-D9u_cOf8.js} +1 -1
- package/dist/cdn/{SvDateTimePicker-vNU6bZ-q.js → SvDateTimePicker-s_6IUpI7.js} +1 -1
- package/dist/cdn/{SvGridCellEditor-B1p-vCK5.js → SvGridCellEditor-BlyCSp0q.js} +1 -1
- package/dist/cdn/{SvGridCellEditor-D_0q4xAu.js → SvGridCellEditor-ClRRA9lW.js} +1 -1
- package/dist/cdn/{SvGridChart-DzLkSwCH.js → SvGridChart-B4EV8U6t.js} +4 -4
- package/dist/cdn/{SvGridChart-C3EWAZaM.js → SvGridChart-DsmwTyZF.js} +4 -4
- package/dist/cdn/{SvGridChartBuilder-CfII62sZ.js → SvGridChartBuilder-cHloHobN.js} +4 -4
- package/dist/cdn/{SvGridChartBuilder-BE2T1ykB.js → SvGridChartBuilder-vOl7C0Za.js} +4 -4
- package/dist/cdn/{SvGridChartPanel-xf4sWVTo.js → SvGridChartPanel-AcqF-Ll5.js} +5 -5
- package/dist/cdn/{SvGridChartPanel-CCX5_Wgd.js → SvGridChartPanel-biMtSBPA.js} +5 -5
- package/dist/cdn/{SvGridChartView-DYwabQWj.js → SvGridChartView-Dx_GmZuL.js} +2 -2
- package/dist/cdn/{SvGridChartView-CfuXmY5I.js → SvGridChartView-I3rDHXAq.js} +2 -2
- package/dist/cdn/{SvGridDropdown-D0VdjeR8.js → SvGridDropdown-BWeM3lMe.js} +46 -46
- package/dist/cdn/{SvGridDropdown-D13MtJ2j.js → SvGridDropdown-CJ6QqTxu.js} +67 -67
- package/dist/cdn/{SvModal-CIZWCcad.js → SvModal-ClxGmpAc.js} +187 -186
- package/dist/cdn/{SvModal-BAE-pjZX.js → SvModal-DhB_VMFR.js} +148 -147
- package/dist/cdn/{cell-formatting-C2Hf5gqd.js → cell-formatting-DVVKOSI9.js} +1 -1
- package/dist/cdn/{chart-Dz7SqMXH.js → chart-DEZxwC0i.js} +1 -1
- package/dist/cdn/{chart-panel-messages-CUbf2R4i.js → chart-panel-messages-D12XcTZJ.js} +1 -1
- package/dist/cdn/{chart-panel-messages-CmNrMdsr.js → chart-panel-messages-DguS7gC5.js} +1 -1
- package/dist/cdn/{chart-summary-BJW_tg_X.js → chart-summary-CMAXBGY-.js} +1 -1
- package/dist/cdn/{chart-trend-CaN9mDEV.js → chart-trend-BkEp61Gj.js} +1 -1
- package/dist/cdn/{dismissable-DAHetSNk.js → dismissable-Brc4EZU1.js} +5 -1
- package/dist/cdn/{export-format-CUDLy2yn.js → export-format-YzdwBww2.js} +1 -1
- package/dist/cdn/{row-resize-niQCp040.js → row-resize-DtOS1KG6.js} +41 -32
- package/dist/cdn/{server-block-cache-CGoWz-87.js → server-block-cache-DIqb3VZn.js} +4 -0
- package/dist/cdn/{src-jcFMkBnN.js → src-CgoeSM8k.js} +4697 -4131
- package/dist/cdn/{src-Bklofsad.js → src-DfwxB1TH.js} +4727 -4161
- package/dist/cdn/svgrid.js +17 -17
- package/dist/cdn/svgrid.svelte-external.js +17 -17
- package/dist/cdn/{validate-C7Rn_Bns.js → validate-AZoD8BoR.js} +6 -2
- package/dist/cell-formatting.js +18 -1
- package/dist/cell-render.js +25 -6
- package/dist/clipboard.d.ts +2 -2
- package/dist/clipboard.js +72 -23
- package/dist/columns.js +7 -2
- package/dist/conditional-formatting.d.ts +2 -0
- package/dist/conditional-formatting.js +2 -0
- package/dist/core.d.ts +22 -6
- package/dist/core.js +365 -25
- package/dist/editing.js +24 -0
- package/dist/grid-messages.d.ts +4 -0
- package/dist/grid-messages.js +4 -0
- package/dist/headless.d.ts +5 -0
- package/dist/headless.js +12 -0
- package/dist/history.d.ts +21 -0
- package/dist/history.js +34 -4
- package/dist/index.d.ts +1 -1
- package/dist/keyboard-handlers.js +24 -1
- package/dist/keyboard.js +4 -0
- package/dist/row-drag-touch.d.ts +1 -1
- package/dist/row-drag.js +35 -1
- package/dist/row-model.d.ts +6 -0
- package/dist/row-resize.js +22 -1
- package/dist/selection.js +1 -1
- package/dist/server-block-cache.d.ts +5 -0
- package/dist/server-block-cache.js +8 -0
- package/dist/server-data-source.d.ts +35 -3
- package/dist/server-data-source.js +11 -2
- package/dist/server.d.ts +1 -1
- package/dist/sparkline.js +7 -5
- package/dist/svgrid-wrapper.types.d.ts +33 -0
- package/dist/validate.js +9 -1
- package/package.json +6 -1
- package/src/GridMenus.svelte +34 -4
- package/src/SvGrid.controller.svelte.ts +206 -53
- package/src/SvGrid.css +127 -2
- package/src/SvGrid.svelte +223 -38
- package/src/SvGrid.types.ts +77 -4
- package/src/SvGridDropdown.svelte +20 -0
- package/src/SvModal.svelte +22 -0
- package/src/SvModal.test.ts +30 -1
- package/src/a11y/dismissable.test.ts +10 -1
- package/src/a11y/dismissable.ts +22 -2
- package/src/build-api.ts +85 -27
- package/src/cell-formatting.test.ts +17 -0
- package/src/cell-formatting.ts +18 -1
- package/src/cell-render.test.ts +24 -2
- package/src/cell-render.ts +24 -8
- package/src/clipboard.test.ts +107 -0
- package/src/clipboard.ts +62 -23
- package/src/columns.test.ts +4 -4
- package/src/columns.ts +7 -2
- package/src/conditional-formatting.test.ts +8 -0
- package/src/conditional-formatting.ts +3 -0
- package/src/core.tick-repair.test.ts +287 -0
- package/src/core.ts +374 -26
- package/src/editing.test.ts +52 -0
- package/src/editing.ts +25 -0
- package/src/grid-messages.test.ts +7 -0
- package/src/grid-messages.ts +10 -0
- package/src/headless.ts +24 -0
- package/src/history.test.ts +49 -1
- package/src/history.ts +48 -2
- package/src/index.ts +2 -0
- package/src/keyboard-handlers.coverage.test.ts +65 -0
- package/src/keyboard-handlers.ts +26 -1
- package/src/keyboard.ts +3 -0
- package/src/row-drag-touch.ts +2 -2
- package/src/row-drag.test.ts +51 -0
- package/src/row-drag.ts +35 -2
- package/src/row-model.ts +6 -0
- package/src/row-resize.ts +24 -1
- package/src/selection.ts +1 -1
- package/src/server-block-cache.test.ts +27 -0
- package/src/server-block-cache.ts +13 -0
- package/src/server-data-source.infinite.test.ts +62 -0
- package/src/server-data-source.test.ts +19 -0
- package/src/server-data-source.ts +48 -4
- package/src/server.ts +2 -0
- package/src/sparkline.test.ts +7 -0
- package/src/sparkline.ts +6 -4
- package/src/svgrid-wrapper.types.ts +32 -0
- package/src/svgrid.api.test.ts +21 -0
- package/src/svgrid.charting.test.ts +12 -4
- package/src/svgrid.context-menu.test.ts +12 -0
- package/src/svgrid.detail-rows.svelte.test.ts +187 -0
- package/src/svgrid.filter-menu-scroll.test.ts +9 -0
- package/src/svgrid.gantt-seam.test.ts +1 -1
- package/src/svgrid.live-update-paths.svelte.test.ts +184 -0
- package/src/svgrid.menu-scroll-close.test.ts +10 -0
- package/src/svgrid.row-model-seam.svelte.test.ts +16 -0
- package/src/svgrid.row-pinning.test.ts +15 -1
- package/src/svgrid.selection-bar-seam.test.ts +1 -1
- package/src/svgrid.sticky-groups.svelte.test.ts +145 -0
- package/src/svgrid.upsell-license.test.ts +1 -1
- package/src/test-setup.ts +12 -0
- package/src/transaction.test.ts +58 -0
- package/src/validate.ts +7 -1
- package/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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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" &&
|
|
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')
|
package/dist/row-drag-touch.d.ts
CHANGED
|
@@ -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 =
|
|
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);
|
package/dist/row-model.d.ts
CHANGED
|
@@ -84,6 +84,12 @@ export type GridRowModel<TData> = {
|
|
|
84
84
|
level: (row: TData) => number;
|
|
85
85
|
expanded?: (row: TData) => boolean;
|
|
86
86
|
onToggle: (row: TData) => void;
|
|
87
|
+
/** Open or close the detail panel under a leaf. Ctrl+Enter on a row calls it when `renderDetailRow` is set, and so does the `showDetailToggle` chevron. */
|
|
88
|
+
toggleDetail?: (row: TData) => void;
|
|
89
|
+
/** Whether the detail under a leaf is open: the chevron's direction. */
|
|
90
|
+
detailOpen?: (row: TData) => boolean;
|
|
91
|
+
/** Whether a row can open a detail; the chevron is left out where this is false. */
|
|
92
|
+
hasDetail?: (row: TData) => boolean;
|
|
87
93
|
};
|
|
88
94
|
/** Selection the grid does not own - see the `rowSelectionModel` prop. */
|
|
89
95
|
selection?: {
|
package/dist/row-resize.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
/**
|
|
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:
|
|
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?.
|
|
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 =
|
|
41
|
+
const bars = [];
|
|
42
|
+
values.forEach((v, i) => {
|
|
42
43
|
const x = pad + i * slot + (slot - barW) / 2;
|
|
44
|
+
// A win/loss sparkline marks a win up and a loss down; a zero is a
|
|
45
|
+
// blank gap, no mark at all, the way Excel draws one.
|
|
43
46
|
if (v > 0)
|
|
44
|
-
|
|
45
|
-
if (v < 0)
|
|
46
|
-
|
|
47
|
-
return { x, y: mid - 1, w: barW, h: 2, negative: false };
|
|
47
|
+
bars.push({ x, y: mid - h, w: barW, h, negative: false });
|
|
48
|
+
else if (v < 0)
|
|
49
|
+
bars.push({ x, y: mid, w: barW, h, negative: true });
|
|
48
50
|
});
|
|
49
51
|
return { ...base, linePath: '', areaPath: '', bars, lastPoint: null };
|
|
50
52
|
}
|
|
@@ -151,6 +151,12 @@ export type SvGridTransactionResult = {
|
|
|
151
151
|
* the component.
|
|
152
152
|
*/
|
|
153
153
|
export type SvGridApi<TFeatures extends TableFeatures, TData extends RowData> = {
|
|
154
|
+
/**
|
|
155
|
+
* The grid's root element (`.sv-grid-root`), or null before mount and
|
|
156
|
+
* after unmount. For measuring, focusing, or reading the theme the grid
|
|
157
|
+
* is drawn with - the PDF export takes its colours from here.
|
|
158
|
+
*/
|
|
159
|
+
getElement(): HTMLElement | null;
|
|
154
160
|
/** Read a cell value from the underlying data at `rowIndex`. */
|
|
155
161
|
getCellValue(rowIndex: number, columnId: string): unknown;
|
|
156
162
|
/** Write a cell value through the column's field. */
|
|
@@ -413,6 +419,15 @@ export type SvGridApi<TFeatures extends TableFeatures, TData extends RowData> =
|
|
|
413
419
|
getDisplayedRows(): ReadonlyArray<TData>;
|
|
414
420
|
/** Snapshot of the current data array (pre-pipeline). */
|
|
415
421
|
getData(): ReadonlyArray<TData>;
|
|
422
|
+
/**
|
|
423
|
+
* The stable id the grid keys a row by: what the `getRowId` prop
|
|
424
|
+
* computed, or the row's position when no `getRowId` is set. Pass a
|
|
425
|
+
* display index (the space `getActiveCell`, `scrollToRow` and the cell
|
|
426
|
+
* methods use) or a row object; returns null for an unknown index or a
|
|
427
|
+
* row that is not in the data. Pair with `selectRows`, `toggleRowSelected`
|
|
428
|
+
* or an `applyTransaction` remove, all of which take these ids.
|
|
429
|
+
*/
|
|
430
|
+
getRowId(row: number | TData): string | null;
|
|
416
431
|
/**
|
|
417
432
|
* Snapshot of every column the grid currently knows about, in visual
|
|
418
433
|
* order, with the human-readable header label. Use this when exporting
|
|
@@ -560,6 +575,24 @@ export type SvGridApi<TFeatures extends TableFeatures, TData extends RowData> =
|
|
|
560
575
|
canRedo(): boolean;
|
|
561
576
|
/** Wipe both stacks (e.g. after a server save commits the buffer). */
|
|
562
577
|
clearHistory(): void;
|
|
578
|
+
/**
|
|
579
|
+
* Mark every step recorded from now on with `tag`, until the next call;
|
|
580
|
+
* `undefined` stops the marking. The grid never reads the tag. It is for a
|
|
581
|
+
* consumer that shows one of several data sets through the grid (a
|
|
582
|
+
* workbook's sheets, say) and has to know which one the next Ctrl+Z would
|
|
583
|
+
* change before it lets the grid apply it: it tags the steps with the data
|
|
584
|
+
* set they were made on and asks `peekUndo` first.
|
|
585
|
+
*/
|
|
586
|
+
setHistoryTag(tag: unknown): void;
|
|
587
|
+
/** The step the next `undo()` would apply, or null when there is none. A
|
|
588
|
+
* multi-cell action carries one tag, so it reports as one. */
|
|
589
|
+
peekUndo(): {
|
|
590
|
+
tag: unknown;
|
|
591
|
+
} | null;
|
|
592
|
+
/** The step the next `redo()` would apply, or null when there is none. */
|
|
593
|
+
peekRedo(): {
|
|
594
|
+
tag: unknown;
|
|
595
|
+
} | null;
|
|
563
596
|
/**
|
|
564
597
|
* The same `GridCommandContext` a registered shortcut handler receives.
|
|
565
598
|
*
|
package/dist/validate.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
|
-
|
|
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.
|
|
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",
|
package/src/GridMenus.svelte
CHANGED
|
@@ -214,6 +214,19 @@
|
|
|
214
214
|
});
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
+
/**
|
|
218
|
+
* Whether a column's active filter operator is one of the set operators,
|
|
219
|
+
* read at event time from component state: the condition input's focus
|
|
220
|
+
* and blur handlers cannot depend on a template `@const`, because blur
|
|
221
|
+
* also fires while the popover is being torn down, after that const's
|
|
222
|
+
* effect is gone.
|
|
223
|
+
*/
|
|
224
|
+
function isSetOperatorFor(colId: string): boolean {
|
|
225
|
+
const column = allColumns.find((c) => c.id === colId);
|
|
226
|
+
const op = filterMenuValues[colId]?.operator ?? defaultOperatorFor(column);
|
|
227
|
+
return op === "in" || op === "notIn";
|
|
228
|
+
}
|
|
229
|
+
|
|
217
230
|
/** Enter anywhere in the panel means "done" - filtering is already live. */
|
|
218
231
|
function onFilterPanelKeydown(event: KeyboardEvent) {
|
|
219
232
|
if (event.key !== "Enter" || event.shiftKey) return;
|
|
@@ -286,8 +299,23 @@
|
|
|
286
299
|
const saveComment = $derived(ctrl.saveComment);
|
|
287
300
|
const removeComment = $derived(ctrl.removeComment);
|
|
288
301
|
const closeCommentEditor = $derived(ctrl.closeCommentEditor);
|
|
302
|
+
|
|
303
|
+
function onContextMenuKeydown(event: KeyboardEvent) {
|
|
304
|
+
// Escape closes the context menu, the way every spreadsheet and desktop
|
|
305
|
+
// app does. The backdrop closes it on a click, but a keyboard user who
|
|
306
|
+
// opened it (Shift+F10, the Menu key) otherwise has no way out, and the
|
|
307
|
+
// menu sits over the sheet swallowing every other key. Handled at the
|
|
308
|
+
// window so it works whatever holds focus while the menu is open.
|
|
309
|
+
if (contextMenuFor && (event.key === "Escape" || event.key === "Esc")) {
|
|
310
|
+
event.preventDefault();
|
|
311
|
+
event.stopPropagation();
|
|
312
|
+
closeContextMenu();
|
|
313
|
+
}
|
|
314
|
+
}
|
|
289
315
|
</script>
|
|
290
316
|
|
|
317
|
+
<svelte:window onkeydown={onContextMenuKeydown} />
|
|
318
|
+
|
|
291
319
|
{#if tooltip}
|
|
292
320
|
<!-- Custom hover tooltip. Floats above the grid; positioned by
|
|
293
321
|
showTooltipFor(). Notes and column tooltips both surface here. -->
|
|
@@ -396,10 +424,12 @@
|
|
|
396
424
|
: menuActiveOperator === "regex"
|
|
397
425
|
? "Pattern..."
|
|
398
426
|
: "Filter value..."}
|
|
399
|
-
onfocus={
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
onblur={
|
|
427
|
+
onfocus={(event) => {
|
|
428
|
+
if (isSetOperatorFor(colId)) onSetOpFilterFocus(event.currentTarget as HTMLInputElement, colId);
|
|
429
|
+
}}
|
|
430
|
+
onblur={() => {
|
|
431
|
+
if (isSetOperatorFor(colId)) closeInSuggest();
|
|
432
|
+
}}
|
|
403
433
|
oninput={(event) => {
|
|
404
434
|
const input = event.currentTarget as HTMLInputElement;
|
|
405
435
|
updateFilterMenuValue(colId, input.value);
|