@svgrid/grid 2.2.31 → 2.2.33
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 +1 -1
- package/dist/DockNodeView.svelte +1 -1
- package/dist/GridFooter.svelte +0 -40
- package/dist/GridMenus.svelte +0 -77
- package/dist/SvAvatar.svelte +1 -1
- package/dist/SvCheckBox.svelte +2 -1
- package/dist/SvCircularProgress.svelte +1 -1
- package/dist/SvDateRangeInput.svelte +1 -1
- package/dist/SvGrid.controller.svelte.d.ts +20 -0
- package/dist/SvGrid.controller.svelte.js +358 -18
- package/dist/SvGrid.css +103 -3
- package/dist/SvGrid.svelte +164 -35
- package/dist/SvGrid.types.d.ts +81 -0
- package/dist/SvGridChart.svelte +2 -10
- package/dist/SvGridDropdown.svelte +38 -6
- package/dist/SvGridDropdown.svelte.d.ts +4 -0
- package/dist/SvGridSelect.svelte +12 -5
- package/dist/SvGridSelect.svelte.d.ts +6 -0
- package/dist/SvGroupCell.svelte +9 -2
- package/dist/SvKbd.svelte +1 -1
- package/dist/SvList.svelte +1 -1
- package/dist/SvMenubar.svelte +1 -1
- package/dist/SvMultiSelect.svelte +12 -4
- package/dist/SvMultiSelect.svelte.d.ts +6 -0
- package/dist/SvOtpInput.svelte +1 -1
- package/dist/SvPasswordInput.svelte +1 -1
- package/dist/SvProgress.svelte +1 -1
- package/dist/SvRating.svelte +1 -1
- package/dist/SvResult.svelte +1 -1
- package/dist/SvRichText.svelte +35 -16
- package/dist/SvRichText.svelte.d.ts +9 -0
- package/dist/SvSwitchButton.svelte +2 -1
- package/dist/SvToaster.svelte +1 -1
- package/dist/SvTour.svelte +4 -0
- package/dist/SvTree.svelte +9 -2
- package/dist/SvTree.svelte.d.ts +5 -0
- package/dist/SvTreeSelect.svelte +6 -4
- package/dist/build-api.js +16 -0
- package/dist/cdn/{GridMenus-Ds3OpzZT.js → GridMenus-B7XhhqdN.js} +34 -36
- package/dist/cdn/{GridMenus-Da_p7SKS.js → GridMenus-BxmIvCJh.js} +35 -37
- package/dist/cdn/{SvDateTimePicker-DB0aIEk8.js → SvDateTimePicker-ClqtVIj3.js} +2 -1
- package/dist/cdn/{SvDateTimePicker-B8GopjhC.js → SvDateTimePicker-o94wfSUa.js} +3 -2
- package/dist/cdn/{SvGridChart-BcLCYvw9.js → SvGridChart-Cr_90pPD.js} +1 -1
- package/dist/cdn/{SvGridChartPanel-DvjwxLvK.js → SvGridChartPanel-ICbxiUvK.js} +4 -4
- package/dist/cdn/{SvGridChartView-PwqI4s02.js → SvGridChartView-Bx42PMvf.js} +2 -2
- package/dist/cdn/{SvGridDropdown-hpnHvOGt.js → SvGridDropdown-CUOdm7YA.js} +93 -88
- package/dist/cdn/{SvGridDropdown-DjALc_4f.js → SvGridDropdown-D0VdjeR8.js} +129 -123
- package/dist/cdn/{disclose-version-DoD9AFD_.js → disclose-version-DljhZohK.js} +45 -45
- package/dist/cdn/{src-DYDCiC0D.js → src-erjBRYfX.js} +5952 -5615
- package/dist/cdn/{src-BLJfdyL0.js → src-ktbQVOk-.js} +4190 -3851
- package/dist/cdn/svgrid.js +9 -9
- package/dist/cdn/svgrid.svelte-external.js +8 -8
- package/dist/cell-render.d.ts +1 -0
- package/dist/cell-render.js +115 -1
- package/dist/cell-values.js +5 -0
- package/dist/chart.d.ts +2 -0
- package/dist/chart.js +1 -4
- package/dist/core.d.ts +80 -9
- package/dist/core.js +89 -0
- package/dist/createMenu.svelte.js +0 -1
- package/dist/createPopoverSelect.svelte.d.ts +2 -2
- package/dist/editing.js +1 -1
- package/dist/editor-contract.d.ts +12 -5
- package/dist/editor-registry.d.ts +18 -7
- package/dist/editor-registry.js +10 -1
- package/dist/export-format.js +3 -0
- package/dist/group-display.d.ts +39 -0
- package/dist/group-display.js +52 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/keyboard-handlers.js +16 -0
- package/dist/recurrence.js +3 -2
- package/dist/selection.js +0 -1
- package/dist/svgrid-wrapper.types.d.ts +8 -2
- package/dist/themes/index.d.ts +10 -0
- package/dist/themes/index.js +26 -0
- package/package.json +3 -2
- package/src/DockNodeView.svelte +1 -1
- package/src/GridFooter.svelte +0 -40
- package/src/GridMenus.svelte +0 -77
- package/src/SvAccordion.test.ts +5 -5
- package/src/SvAvatar.svelte +1 -1
- package/src/SvButtonGroup.test.ts +6 -6
- package/src/SvCalendar.test.ts +1 -1
- package/src/SvCheckBox.svelte +2 -1
- package/src/SvCircularProgress.svelte +1 -1
- package/src/SvDateRangeInput.svelte +1 -1
- package/src/SvFileUpload.test.ts +1 -1
- package/src/SvGrid.controller.svelte.ts +367 -22
- package/src/SvGrid.css +103 -3
- package/src/SvGrid.svelte +164 -35
- package/src/SvGrid.types.ts +81 -1
- package/src/SvGridChart.svelte +2 -10
- package/src/SvGridDropdown.svelte +38 -6
- package/src/SvGridSelect.svelte +12 -5
- package/src/SvGroupCell.svelte +9 -2
- package/src/SvKbd.svelte +1 -1
- package/src/SvList.svelte +1 -1
- package/src/SvMenubar.svelte +1 -1
- package/src/SvMultiSelect.svelte +12 -4
- package/src/SvOtpInput.svelte +1 -1
- package/src/SvPasswordInput.svelte +1 -1
- package/src/SvProgress.svelte +1 -1
- package/src/SvRating.svelte +1 -1
- package/src/SvResult.svelte +1 -1
- package/src/SvRichText.svelte +35 -16
- package/src/SvSwitchButton.svelte +2 -1
- package/src/SvToaster.svelte +1 -1
- package/src/SvTour.svelte +4 -0
- package/src/SvTree.svelte +9 -2
- package/src/SvTreeSelect.svelte +6 -4
- package/src/ai.test.ts +1 -1
- package/src/build-api.ts +15 -105
- package/src/builtin-editors.test.ts +3 -1
- package/src/cell-formatting.test.ts +1 -1
- package/src/cell-render.test.ts +27 -3
- package/src/cell-render.ts +122 -95
- package/src/cell-values.ts +4 -0
- package/src/chart-export.test.ts +2 -2
- package/src/chart.ts +1 -2
- package/src/clipboard.test.ts +0 -2
- package/src/clipboard.ts +0 -102
- package/src/columns.ts +0 -115
- package/src/core.ts +157 -8
- package/src/createMenu.svelte.ts +0 -1
- package/src/editing.test.ts +1 -1
- package/src/editing.ts +2 -105
- package/src/editor-contract.ts +12 -5
- package/src/editor-registry.grid.test.ts +48 -2
- package/src/editor-registry.test.ts +3 -1
- package/src/editor-registry.ts +23 -7
- package/src/export-format.ts +3 -0
- package/src/facet-buckets.test.ts +33 -33
- package/src/features.ts +0 -90
- package/src/group-display.test.ts +91 -1
- package/src/group-display.ts +83 -0
- package/src/index.ts +9 -0
- package/src/keyboard-handlers.coverage.test.ts +1 -1
- package/src/keyboard-handlers.ts +17 -120
- package/src/list-power.test.ts +2 -2
- package/src/menus.test.ts +4 -5
- package/src/menus.ts +0 -112
- package/src/named-views.coverage.test.ts +3 -3
- package/src/new-features.test.ts +3 -1
- package/src/recurrence.ts +3 -2
- package/src/row-drag.test.ts +1 -1
- package/src/row-resize.test.ts +41 -42
- package/src/scheduler-model.ts +1 -1
- package/src/scroll-sync.test.ts +4 -7
- package/src/scroll-sync.ts +0 -122
- package/src/selection.ts +0 -109
- package/src/server-data-source.coverage.test.ts +1 -1
- package/src/server-group-model.test.ts +8 -8
- package/src/spreadsheet.test.ts +0 -1
- package/src/summaries.ts +0 -112
- package/src/svgrid-wrapper.types.ts +11 -9
- package/src/svgrid.async-editor-options.test.ts +271 -0
- package/src/svgrid.auto-row-height.test.ts +204 -0
- package/src/svgrid.behavior.test.ts +0 -1
- package/src/svgrid.charting.test.ts +4 -4
- package/src/svgrid.column-reorder.test.ts +0 -8
- package/src/svgrid.comments-autocomplete.test.ts +8 -2
- package/src/svgrid.context-menu.test.ts +25 -9
- package/src/svgrid.filter-depth.test.ts +163 -0
- package/src/svgrid.filter-menu-listbox.svelte.test.ts +16 -2
- package/src/svgrid.filter-menu-scroll.test.ts +10 -2
- package/src/svgrid.grand-total.test.ts +188 -0
- package/src/svgrid.group-display-mode.test.ts +171 -0
- package/src/svgrid.group-footers.test.ts +121 -0
- package/src/svgrid.group-pagination.test.ts +153 -0
- package/src/svgrid.tree-data.test.ts +186 -0
- package/src/svgriddropdown.async-panel.svelte.test.ts +195 -0
- package/src/themes/index.ts +42 -0
- package/src/tree-edit.test.ts +4 -4
- package/src/tree-row-model.test.ts +168 -0
- package/src/ui-localization.test.ts +113 -0
- package/themes/ag-alpine.css +22 -0
- package/themes/antd.css +22 -0
- package/themes/atlassian.css +22 -0
- package/themes/bootstrap.css +22 -0
- package/themes/carbon.css +22 -0
- package/themes/catppuccin.css +22 -0
- package/themes/dracula.css +22 -0
- package/themes/ember.css +22 -0
- package/themes/excel.css +22 -0
- package/themes/fluent.css +22 -0
- package/themes/github.css +22 -0
- package/themes/linear.css +22 -0
- package/themes/material.css +22 -0
- package/themes/nord.css +22 -0
- package/themes/notion.css +22 -0
- package/themes/salesforce.css +22 -0
- package/themes/sap.css +22 -0
- package/themes/shadcn.css +22 -0
- package/themes/tailwind.css +22 -0
- package/themes/vercel.css +22 -0
package/dist/SvGrid.svelte
CHANGED
|
@@ -17,7 +17,6 @@
|
|
|
17
17
|
} from "./index";
|
|
18
18
|
import "./sv-grid-scrollbar";
|
|
19
19
|
import "./SvGrid.css";
|
|
20
|
-
import type { Snippet } from "svelte";
|
|
21
20
|
import {
|
|
22
21
|
RenderSnippetConfig,
|
|
23
22
|
RenderComponentConfig,
|
|
@@ -36,16 +35,10 @@
|
|
|
36
35
|
import { timeStringToDate, dateToTimeString } from "./SvGrid.helpers";
|
|
37
36
|
import type {
|
|
38
37
|
Props,
|
|
39
|
-
SelectionPoint,
|
|
40
|
-
SelectionRange,
|
|
41
|
-
CellEditState,
|
|
42
38
|
FilterOperator,
|
|
43
|
-
FilterOption,
|
|
44
|
-
MenuPosition,
|
|
45
39
|
} from "./SvGrid.types";
|
|
46
40
|
import {
|
|
47
41
|
cfTextStyle,
|
|
48
|
-
fmtStat,
|
|
49
42
|
getEditableInputValue,
|
|
50
43
|
getEditorInputType,
|
|
51
44
|
toValueArray,
|
|
@@ -57,7 +50,6 @@
|
|
|
57
50
|
import {
|
|
58
51
|
splitInTokens,
|
|
59
52
|
trailingInToken,
|
|
60
|
-
joinInTokens,
|
|
61
53
|
} from "./filtering/excel-filters";
|
|
62
54
|
import { createSvGridController } from "./SvGrid.controller.svelte";
|
|
63
55
|
// GridMenus hosts tooltips + all column/filter/context menu overlays - none of
|
|
@@ -180,10 +172,8 @@
|
|
|
180
172
|
|
|
181
173
|
// ---- View facade: re-bind the controller's reactive members as locals so the
|
|
182
174
|
// markup stays identical. Assignable state + bindings + DOM refs use c.* directly.
|
|
183
|
-
const paginationEnabled = $derived(ctrl.paginationEnabled);
|
|
184
175
|
const filterRowValues = $derived(ctrl.filterRowValues);
|
|
185
176
|
const filterMenuValues = $derived(ctrl.filterMenuValues);
|
|
186
|
-
const tooltip = $derived(ctrl.tooltip);
|
|
187
177
|
const showTooltipFor = $derived(ctrl.showTooltipFor);
|
|
188
178
|
const hideTooltip = $derived(ctrl.hideTooltip);
|
|
189
179
|
const findHits = $derived(ctrl.findHits);
|
|
@@ -256,28 +246,41 @@
|
|
|
256
246
|
// Server-side group / tree a11y: aria-level on every tree row, aria-expanded
|
|
257
247
|
// on the expandable ones. Undefined (attribute omitted) when serverGroup is off.
|
|
258
248
|
const serverGroup = $derived(opt.serverGroup);
|
|
249
|
+
// Client tree data reuses this same treegrid contract - the row model already
|
|
250
|
+
// records depth and expandability, so the accessors just read the row instead
|
|
251
|
+
// of calling back into consumer callbacks.
|
|
252
|
+
const treeData = $derived(opt.treeData);
|
|
259
253
|
function sgAriaLevel(row: Row<TData>): number | undefined {
|
|
254
|
+
if (treeData) return (row.depth ?? 0) + 1;
|
|
260
255
|
return serverGroup ? serverGroup.level(row.original as TData) + 1 : undefined;
|
|
261
256
|
}
|
|
262
257
|
function sgAriaExpanded(row: Row<TData>): boolean | undefined {
|
|
258
|
+
if (treeData) {
|
|
259
|
+
if (!row.getCanExpand?.()) return undefined;
|
|
260
|
+
return row.getIsExpanded?.() ?? false;
|
|
261
|
+
}
|
|
263
262
|
if (!serverGroup || !serverGroup.isGroup(row.original as TData)) return undefined;
|
|
264
263
|
return serverGroup.expanded?.(row.original as TData) ?? false;
|
|
265
264
|
}
|
|
265
|
+
/** Column that carries the tree expander: explicit, else the first visible one. */
|
|
266
|
+
const treeColumnId = $derived(
|
|
267
|
+
treeData ? (treeData.column ?? allColumns[0]?.id ?? "") : "",
|
|
268
|
+
);
|
|
269
|
+
// Row-grouping display modes: group state lives in synthetic columns instead
|
|
270
|
+
// of a full-width banner row.
|
|
271
|
+
const groupColumnMode = $derived(ctrl.groupColumnMode);
|
|
272
|
+
const autoGroupCell = $derived(ctrl.autoGroupCell);
|
|
273
|
+
const isAutoGroupColumn = (id: string): boolean =>
|
|
274
|
+
id === "__autoGroup" || id.startsWith("__group_");
|
|
266
275
|
const sortDirectionByColumn = $derived(ctrl.sortDirectionByColumn);
|
|
267
276
|
const groupingColumns = $derived(ctrl.groupingColumns);
|
|
268
|
-
const paginationState = $derived(ctrl.paginationState);
|
|
269
|
-
const allRowsBeforePagination = $derived(ctrl.allRowsBeforePagination);
|
|
270
277
|
const allRows = $derived(ctrl.allRows);
|
|
271
|
-
const statusBarEnabled = $derived(ctrl.statusBarEnabled);
|
|
272
|
-
const statusBarAggregates = $derived(ctrl.statusBarAggregates);
|
|
273
|
-
const statusBarStats = $derived(ctrl.statusBarStats);
|
|
274
278
|
const toolPanelEnabled = $derived(ctrl.toolPanelEnabled);
|
|
275
279
|
const toolPanelColumns = $derived(ctrl.toolPanelColumns);
|
|
276
280
|
const toolPanelHeaderLabel = $derived(ctrl.toolPanelHeaderLabel);
|
|
277
281
|
const toggleColumnVisibleInPanel = $derived(ctrl.toggleColumnVisibleInPanel);
|
|
278
282
|
const moveColumnInPanel = $derived(ctrl.moveColumnInPanel);
|
|
279
283
|
const toggleGroupInPanel = $derived(ctrl.toggleGroupInPanel);
|
|
280
|
-
const virtualizer = $derived(ctrl.virtualizer);
|
|
281
284
|
const rowVirtualizationEnabled = $derived(ctrl.rowVirtualizationEnabled);
|
|
282
285
|
// The fixed row height, matching what the virtualized path takes from the virtualizer.
|
|
283
286
|
// The non-virtualized (`virtualization={false}`) body must apply this too, else its rows
|
|
@@ -286,10 +289,25 @@
|
|
|
286
289
|
const rh = opt.rowHeight;
|
|
287
290
|
return typeof rh === "function" ? rh(i) : (rh ?? 30);
|
|
288
291
|
};
|
|
292
|
+
// Auto height applies to both bodies. The non-virtualized one has no
|
|
293
|
+
// `rowItem`, so it sizes from `rowSizePx` - but must still emit `min-height`
|
|
294
|
+
// and the wrap class, or `autoRowHeight` would silently do nothing whenever
|
|
295
|
+
// `virtualization={false}`.
|
|
296
|
+
const rowStyleFor = (i: number): string =>
|
|
297
|
+
autoRowHeightOn
|
|
298
|
+
? `min-height: ${measuredRowHeightPx(i) ?? rowSizePx(i)}px;`
|
|
299
|
+
: `height: ${rowSizePx(i)}px;`;
|
|
289
300
|
const columnVirtualizationEnabled = $derived(
|
|
290
301
|
ctrl.columnVirtualizationEnabled,
|
|
291
302
|
);
|
|
292
303
|
const virtualRows = $derived(ctrl.virtualRows);
|
|
304
|
+
const autoRowHeightOn = $derived(ctrl.autoRowHeightOn);
|
|
305
|
+
const measureRowHeight = $derived(ctrl.measureRowHeight);
|
|
306
|
+
const measuredRowHeightPx = $derived(ctrl.measuredRowHeightPx);
|
|
307
|
+
// Under auto height the row must be free to grow, so it gets `min-height`
|
|
308
|
+
// (the pre-measure estimate) instead of a fixed `height` that would clip it.
|
|
309
|
+
const rowHeightStyle = (size: number): string =>
|
|
310
|
+
autoRowHeightOn ? `min-height: ${size}px;` : `height: ${size}px;`;
|
|
293
311
|
// DOM-space spacer heights + capped total: identical to the logical
|
|
294
312
|
// virtualizer values for normal grids, scaled down past
|
|
295
313
|
// MAX_DOM_SCROLL_HEIGHT so huge grids stay scrollable to the last row on
|
|
@@ -405,6 +423,16 @@
|
|
|
405
423
|
const hasMeasured = $derived(ctrl.hasMeasured);
|
|
406
424
|
const onBodyScroll = $derived(ctrl.onBodyScroll);
|
|
407
425
|
const computeRowClass = $derived(ctrl.computeRowClass);
|
|
426
|
+
// Group subtotal rows are clones of their banner, so they render through the
|
|
427
|
+
// normal cell path and their aggregates land under the right columns; only
|
|
428
|
+
// the class distinguishes them. Typed here rather than cast inline - an `as`
|
|
429
|
+
// inside template markup type-checks but breaks the vitest/build parse.
|
|
430
|
+
const isGroupFooterRow = (row: Row<TData>): boolean =>
|
|
431
|
+
(row as { __groupFooter?: boolean }).__groupFooter === true;
|
|
432
|
+
// The grand total is a group footer that closes the whole set rather than one
|
|
433
|
+
// group, so it carries both classes - shared chrome, heavier top rule.
|
|
434
|
+
const isGrandTotalRow = (row: Row<TData>): boolean =>
|
|
435
|
+
(row as { __grandTotal?: boolean }).__grandTotal === true;
|
|
408
436
|
const computeCellClass = $derived(ctrl.computeCellClass);
|
|
409
437
|
const computeCellTooltip = $derived(ctrl.computeCellTooltip);
|
|
410
438
|
const computeCellValidity = $derived(ctrl.computeCellValidity);
|
|
@@ -412,6 +440,7 @@
|
|
|
412
440
|
const getCellDisplayValue = $derived(ctrl.getCellDisplayValue);
|
|
413
441
|
const getColumnAlign = $derived(ctrl.getColumnAlign);
|
|
414
442
|
const getColumnEditorOptions = $derived(ctrl.getColumnEditorOptions);
|
|
443
|
+
const areEditorOptionsLoading = $derived(ctrl.areEditorOptionsLoading);
|
|
415
444
|
const formatListCellValue = $derived(ctrl.formatListCellValue);
|
|
416
445
|
const formatCellValue = $derived(ctrl.formatCellValue);
|
|
417
446
|
const getPinnedCellValue = $derived(ctrl.getPinnedCellValue);
|
|
@@ -460,14 +489,26 @@
|
|
|
460
489
|
ctrl.editingCell = null;
|
|
461
490
|
ctrl.gridRootEl?.focus({ preventScroll: true });
|
|
462
491
|
},
|
|
492
|
+
// Tab out of an editor: commit, then step to the adjacent cell. Without
|
|
493
|
+
// this a registered editor's Tab walks focus out of the grid (#48) - the
|
|
494
|
+
// built-in editors already route through the same helper.
|
|
495
|
+
onCommitAndMove: (v: unknown, direction: 1 | -1) => {
|
|
496
|
+
if (v !== undefined) updateEditingCellValue(v);
|
|
497
|
+
ctrl.commitAndMoveByTab(direction === -1);
|
|
498
|
+
},
|
|
499
|
+
// Close a popover editor without committing. Same effect as cancel today;
|
|
500
|
+
// named separately so an editor can distinguish "dismiss my panel" from
|
|
501
|
+
// "abandon the edit" if that ever diverges.
|
|
502
|
+
onRequestClose: () => {
|
|
503
|
+
ctrl.editingCell = null;
|
|
504
|
+
ctrl.gridRootEl?.focus({ preventScroll: true });
|
|
505
|
+
},
|
|
506
|
+
inCell: true,
|
|
463
507
|
};
|
|
464
508
|
}
|
|
465
509
|
const onHeaderSortClick = $derived(ctrl.onHeaderSortClick);
|
|
466
510
|
const onGridKeyDown = $derived(ctrl.onGridKeyDown);
|
|
467
511
|
const onGridPaste = $derived(ctrl.onGridPaste);
|
|
468
|
-
const changePage = $derived(ctrl.changePage);
|
|
469
|
-
const goToPage = $derived(ctrl.goToPage);
|
|
470
|
-
const setPageSize = $derived(ctrl.setPageSize);
|
|
471
512
|
const updateFilterRow = $derived(ctrl.updateFilterRow);
|
|
472
513
|
const updateFilterMenuValue = $derived(ctrl.updateFilterMenuValue);
|
|
473
514
|
const updateFilterMenuValueTo = $derived(ctrl.updateFilterMenuValueTo);
|
|
@@ -869,6 +910,11 @@
|
|
|
869
910
|
<path d="M6 6l12 12" />
|
|
870
911
|
{:else if name === "chevron-down"}
|
|
871
912
|
<path d="M6 9l6 6 6-6" />
|
|
913
|
+
{:else if name === "chevron-right"}
|
|
914
|
+
<!-- Same path SvTree's twisty uses, so every expander in the library
|
|
915
|
+
draws the same chevron. Rotated 90deg by CSS when expanded rather
|
|
916
|
+
than swapped for a second glyph. -->
|
|
917
|
+
<path d="m9 18 6-6-6-6" />
|
|
872
918
|
{:else if name === "op-contains"}
|
|
873
919
|
<circle cx="11" cy="11" r="6" />
|
|
874
920
|
<path d="M20 20l-4.5-4.5" />
|
|
@@ -1027,7 +1073,12 @@
|
|
|
1027
1073
|
</svg>
|
|
1028
1074
|
{/if}
|
|
1029
1075
|
{:else}
|
|
1030
|
-
|
|
1076
|
+
<!-- Indent for rows nested under a group BANNER. Tree data and the
|
|
1077
|
+
column display modes draw their own indent next to the expander, so
|
|
1078
|
+
this must not stack on top of theirs - `row.depth` is set in all
|
|
1079
|
+
three cases and this used to fire for every one of them, leaving a
|
|
1080
|
+
20px gap between a tree row's chevron and its text. -->
|
|
1081
|
+
{#if row.depth > 0 && column.id === allColumns[0]?.id && !treeData && !groupColumnMode}
|
|
1031
1082
|
<span
|
|
1032
1083
|
class="sv-grid-group-child-indent"
|
|
1033
1084
|
style={`width: ${row.depth * 20}px;`}
|
|
@@ -1073,10 +1124,75 @@
|
|
|
1073
1124
|
color / weight and the icon-set glyph ride on the content wrapper.
|
|
1074
1125
|
Falls straight through to `cellBody` when no format applies, so the
|
|
1075
1126
|
common (unformatted) path pays nothing. -->
|
|
1127
|
+
<!-- Tree expander + indentation, prefixed to the tree column's cell. Rows
|
|
1128
|
+
without children still get the indent (and a spacer where the chevron
|
|
1129
|
+
would be) so values stay aligned down a level. -->
|
|
1130
|
+
{#snippet treeAffordance(row: Row<TData>)}
|
|
1131
|
+
{@const canExpand = row.getCanExpand?.() ?? false}
|
|
1132
|
+
<span
|
|
1133
|
+
class="sv-grid-tree-indent"
|
|
1134
|
+
style={`width:${(row.depth ?? 0) * (treeData?.indentPx ?? 12)}px;`}
|
|
1135
|
+
aria-hidden="true"
|
|
1136
|
+
></span>
|
|
1137
|
+
{#if canExpand}
|
|
1138
|
+
<button
|
|
1139
|
+
type="button"
|
|
1140
|
+
class="sv-grid-tree-toggle"
|
|
1141
|
+
aria-expanded={row.getIsExpanded?.() ? "true" : "false"}
|
|
1142
|
+
aria-label={row.getIsExpanded?.() ? "Collapse row" : "Expand row"}
|
|
1143
|
+
onclick={(event) => {
|
|
1144
|
+
event.stopPropagation();
|
|
1145
|
+
row.toggleExpanded?.();
|
|
1146
|
+
}}>{@render icon("chevron-right")}</button
|
|
1147
|
+
>
|
|
1148
|
+
{:else}
|
|
1149
|
+
<span class="sv-grid-tree-spacer" aria-hidden="true"></span>
|
|
1150
|
+
{/if}
|
|
1151
|
+
{/snippet}
|
|
1152
|
+
|
|
1153
|
+
<!-- The group label + expander for a `singleColumn` / `multipleColumns`
|
|
1154
|
+
auto-group cell. Indented by level in single-column mode; in multi-column
|
|
1155
|
+
mode each level already has its own column, so no indent. -->
|
|
1156
|
+
{#snippet autoGroupAffordance(row: Row<TData>, cell: { label: string; count: number; depth: number })}
|
|
1157
|
+
<span
|
|
1158
|
+
class="sv-grid-tree-indent"
|
|
1159
|
+
style={`width:${cell.depth * 16}px;`}
|
|
1160
|
+
aria-hidden="true"
|
|
1161
|
+
></span>
|
|
1162
|
+
<button
|
|
1163
|
+
type="button"
|
|
1164
|
+
class="sv-grid-tree-toggle"
|
|
1165
|
+
aria-expanded={row.getIsExpanded?.() ? "true" : "false"}
|
|
1166
|
+
aria-label={row.getIsExpanded?.() ? "Collapse group" : "Expand group"}
|
|
1167
|
+
onclick={(event) => {
|
|
1168
|
+
event.stopPropagation();
|
|
1169
|
+
row.toggleExpanded?.();
|
|
1170
|
+
}}>{@render icon("chevron-right")}</button
|
|
1171
|
+
>
|
|
1172
|
+
<span class="sv-grid-autogroup-label">{cell.label}</span>
|
|
1173
|
+
<span class="sv-grid-group-count">({cell.count})</span>
|
|
1174
|
+
{/snippet}
|
|
1175
|
+
|
|
1076
1176
|
{#snippet cellBodyWithFormat(
|
|
1077
1177
|
row: Row<TData>,
|
|
1078
1178
|
column: Column<TData>,
|
|
1079
1179
|
cellValue: unknown,
|
|
1180
|
+
)}
|
|
1181
|
+
{#if groupColumnMode && isAutoGroupColumn(column.id)}
|
|
1182
|
+
{@const agc = autoGroupCell(row, column.id)}
|
|
1183
|
+
{#if agc}{@render autoGroupAffordance(row, agc)}{/if}
|
|
1184
|
+
{:else}
|
|
1185
|
+
{#if treeData && column.id === treeColumnId}
|
|
1186
|
+
{@render treeAffordance(row)}
|
|
1187
|
+
{/if}
|
|
1188
|
+
{@render cellBodyFormatted(row, column, cellValue)}
|
|
1189
|
+
{/if}
|
|
1190
|
+
{/snippet}
|
|
1191
|
+
|
|
1192
|
+
{#snippet cellBodyFormatted(
|
|
1193
|
+
row: Row<TData>,
|
|
1194
|
+
column: Column<TData>,
|
|
1195
|
+
cellValue: unknown,
|
|
1080
1196
|
)}
|
|
1081
1197
|
{@const cf = cellConditionalFormat(row, column, cellValue)}
|
|
1082
1198
|
{#if cf && (cf.background || cf.dataBar || cf.icon || cf.color || cf.fontWeight != null)}
|
|
@@ -1156,7 +1272,7 @@
|
|
|
1156
1272
|
aria-label="Edit checkbox value"
|
|
1157
1273
|
onclick={(event) => {
|
|
1158
1274
|
event.stopPropagation();
|
|
1159
|
-
const nextValue = !
|
|
1275
|
+
const nextValue = !ctrl.editingCell?.value;
|
|
1160
1276
|
ctrl.editingCell = ctrl.editingCell
|
|
1161
1277
|
? { ...ctrl.editingCell, value: nextValue }
|
|
1162
1278
|
: ctrl.editingCell;
|
|
@@ -1165,7 +1281,7 @@
|
|
|
1165
1281
|
onkeydown={(event) =>
|
|
1166
1282
|
toggleCheckboxWithKeyboard(event, () => {
|
|
1167
1283
|
event.stopPropagation();
|
|
1168
|
-
const nextValue = !
|
|
1284
|
+
const nextValue = !ctrl.editingCell?.value;
|
|
1169
1285
|
ctrl.editingCell = ctrl.editingCell
|
|
1170
1286
|
? { ...ctrl.editingCell, value: nextValue }
|
|
1171
1287
|
: ctrl.editingCell;
|
|
@@ -1175,10 +1291,12 @@
|
|
|
1175
1291
|
></button>
|
|
1176
1292
|
{:else if ctrl.editingCell?.editorType === "list"}
|
|
1177
1293
|
{@const opts = getColumnEditorOptions(column, row)}
|
|
1294
|
+
{@const optsLoading = areEditorOptionsLoading(column, row)}
|
|
1178
1295
|
{@const multi = column.columnDef.editorMultiple === true}
|
|
1179
1296
|
{#if DropdownEditor}
|
|
1180
1297
|
<DropdownEditor
|
|
1181
1298
|
options={opts}
|
|
1299
|
+
loading={optsLoading}
|
|
1182
1300
|
value={ctrl.editingCell?.value}
|
|
1183
1301
|
multiple={multi}
|
|
1184
1302
|
placeholder="Select…"
|
|
@@ -1196,9 +1314,10 @@
|
|
|
1196
1314
|
{/if}
|
|
1197
1315
|
{:else if ctrl.editingCell?.editorType === "chips"}
|
|
1198
1316
|
{@const opts = getColumnEditorOptions(column, row)}
|
|
1317
|
+
{@const optsLoading = areEditorOptionsLoading(column, row)}
|
|
1199
1318
|
{@const multi = column.columnDef.editorMultiple === true}
|
|
1200
1319
|
{@const selectedArr = toValueArray(ctrl.editingCell?.value)}
|
|
1201
|
-
{@render chipsEditor(opts, multi, selectedArr)}
|
|
1320
|
+
{@render chipsEditor(opts, multi, selectedArr, optsLoading)}
|
|
1202
1321
|
{:else if ctrl.editingCell?.editorType === "rating"}
|
|
1203
1322
|
{@const ratingVal = Math.max(
|
|
1204
1323
|
0,
|
|
@@ -1241,9 +1360,11 @@
|
|
|
1241
1360
|
<!-- Custom dropdown: opens a themed popover identical in feel to
|
|
1242
1361
|
the existing 'list' editor (single-select, no typeahead). -->
|
|
1243
1362
|
{@const selectOpts = getColumnEditorOptions(column, row)}
|
|
1363
|
+
{@const optsLoading = areEditorOptionsLoading(column, row)}
|
|
1244
1364
|
{#if DropdownEditor}
|
|
1245
1365
|
<DropdownEditor
|
|
1246
1366
|
options={selectOpts}
|
|
1367
|
+
loading={optsLoading}
|
|
1247
1368
|
value={ctrl.editingCell?.value}
|
|
1248
1369
|
multiple={false}
|
|
1249
1370
|
placeholder="Select…"
|
|
@@ -1263,9 +1384,11 @@
|
|
|
1263
1384
|
<!-- Searchable combobox: same popover as 'select' with a
|
|
1264
1385
|
typeahead filter input baked in at the top. -->
|
|
1265
1386
|
{@const richOpts = getColumnEditorOptions(column, row)}
|
|
1387
|
+
{@const optsLoading = areEditorOptionsLoading(column, row)}
|
|
1266
1388
|
{#if DropdownEditor}
|
|
1267
1389
|
<DropdownEditor
|
|
1268
1390
|
options={richOpts}
|
|
1391
|
+
loading={optsLoading}
|
|
1269
1392
|
value={ctrl.editingCell?.value}
|
|
1270
1393
|
multiple={false}
|
|
1271
1394
|
searchable={true}
|
|
@@ -1527,6 +1650,7 @@
|
|
|
1527
1650
|
opts: CellEditorOption[],
|
|
1528
1651
|
multi: boolean,
|
|
1529
1652
|
selectedArr: Array<string | number>,
|
|
1653
|
+
optsLoading: boolean,
|
|
1530
1654
|
)}
|
|
1531
1655
|
{#if opts.length > 0}
|
|
1532
1656
|
<!-- Options-driven chips editor: defer to the custom dropdown,
|
|
@@ -1536,6 +1660,7 @@
|
|
|
1536
1660
|
{#if DropdownEditor}
|
|
1537
1661
|
<DropdownEditor
|
|
1538
1662
|
options={opts}
|
|
1663
|
+
loading={optsLoading}
|
|
1539
1664
|
value={ctrl.editingCell?.value}
|
|
1540
1665
|
multiple={multi}
|
|
1541
1666
|
placeholder="Pick…"
|
|
@@ -1657,7 +1782,7 @@
|
|
|
1657
1782
|
onclick={(event) => {
|
|
1658
1783
|
event.stopPropagation();
|
|
1659
1784
|
row.toggleExpanded?.();
|
|
1660
|
-
}}>{
|
|
1785
|
+
}}>{@render icon("chevron-right")}</button
|
|
1661
1786
|
>
|
|
1662
1787
|
<span class="sv-grid-group-label">{headerLabel}: {groupValue}</span>
|
|
1663
1788
|
<span class="sv-grid-group-count"
|
|
@@ -1853,7 +1978,7 @@
|
|
|
1853
1978
|
activeDescendantId,
|
|
1854
1979
|
rowCount: allRows.length,
|
|
1855
1980
|
colCount: allColumns.length,
|
|
1856
|
-
treegrid: !!opt.serverGroup,
|
|
1981
|
+
treegrid: !!opt.serverGroup || !!opt.treeData,
|
|
1857
1982
|
})}
|
|
1858
1983
|
onkeydown={onGridKeyDown}
|
|
1859
1984
|
onpaste={onGridPaste}
|
|
@@ -2438,14 +2563,14 @@
|
|
|
2438
2563
|
{#if row}
|
|
2439
2564
|
{#if opt.isDetailRow?.(row.original as TData, rowIndex)}
|
|
2440
2565
|
{@render detailRowMarkup(row, rowIndex)}
|
|
2441
|
-
{:else if isGroupRow(row)}
|
|
2566
|
+
{:else if isGroupRow(row) && !groupColumnMode}
|
|
2442
2567
|
<tr
|
|
2443
2568
|
class="sv-grid-row sv-grid-group-row"
|
|
2444
2569
|
class:sv-grid-row-selected={isRowSelected(row.id)}
|
|
2445
2570
|
aria-level={row.depth + 1}
|
|
2446
2571
|
aria-expanded={row.getIsExpanded?.() ? "true" : "false"}
|
|
2447
2572
|
{...getGridRowA11yProps(rowIndex + 1)}
|
|
2448
|
-
style={
|
|
2573
|
+
style={rowHeightStyle(rowItem.size)}
|
|
2449
2574
|
>
|
|
2450
2575
|
<td
|
|
2451
2576
|
class="sv-grid-cell sv-grid-group-cell"
|
|
@@ -2467,11 +2592,15 @@
|
|
|
2467
2592
|
class:sv-grid-row-alt={opt.zebraRows &&
|
|
2468
2593
|
rowIndex % 2 === 1}
|
|
2469
2594
|
class:sv-grid-row-draggable={rowDragManagedEffective}
|
|
2595
|
+
class:sv-grid-group-footer-row={isGroupFooterRow(row)}
|
|
2596
|
+
class:sv-grid-grand-total-row={isGrandTotalRow(row)}
|
|
2597
|
+
class:sv-grid-row-auto-height={autoRowHeightOn}
|
|
2470
2598
|
{...getGridRowA11yProps(rowIndex + 1)}
|
|
2471
2599
|
aria-level={sgAriaLevel(row)}
|
|
2472
2600
|
aria-expanded={sgAriaExpanded(row)}
|
|
2473
2601
|
{...rowDragAttrs(rowIndex)}
|
|
2474
|
-
style={
|
|
2602
|
+
style={rowHeightStyle(rowItem.size)}
|
|
2603
|
+
use:measureRowHeight={rowIndex}
|
|
2475
2604
|
>
|
|
2476
2605
|
{#if showRowNumbersEffective}
|
|
2477
2606
|
<td
|
|
@@ -2542,10 +2671,6 @@
|
|
|
2542
2671
|
row,
|
|
2543
2672
|
rendered.column,
|
|
2544
2673
|
)}
|
|
2545
|
-
{@const cellTooltip = computeCellTooltip(
|
|
2546
|
-
row,
|
|
2547
|
-
rendered.column,
|
|
2548
|
-
)}
|
|
2549
2674
|
{@const cellValidity = computeCellValidity(
|
|
2550
2675
|
row,
|
|
2551
2676
|
rendered.column,
|
|
@@ -2690,14 +2815,14 @@
|
|
|
2690
2815
|
{#each allRows as row, rowIndex (row.id)}
|
|
2691
2816
|
{#if opt.isDetailRow?.(row.original as TData, rowIndex)}
|
|
2692
2817
|
{@render detailRowMarkup(row, rowIndex)}
|
|
2693
|
-
{:else if isGroupRow(row)}
|
|
2818
|
+
{:else if isGroupRow(row) && !groupColumnMode}
|
|
2694
2819
|
<tr
|
|
2695
2820
|
class="sv-grid-row sv-grid-group-row"
|
|
2696
2821
|
class:sv-grid-row-selected={isRowSelected(row.id)}
|
|
2697
2822
|
aria-level={row.depth + 1}
|
|
2698
2823
|
aria-expanded={row.getIsExpanded?.() ? "true" : "false"}
|
|
2699
2824
|
{...getGridRowA11yProps(rowIndex + 1)}
|
|
2700
|
-
style={
|
|
2825
|
+
style={rowStyleFor(rowIndex)}
|
|
2701
2826
|
>
|
|
2702
2827
|
<td
|
|
2703
2828
|
class="sv-grid-cell sv-grid-group-cell"
|
|
@@ -2719,11 +2844,15 @@
|
|
|
2719
2844
|
class:sv-grid-row-alt={opt.zebraRows &&
|
|
2720
2845
|
rowIndex % 2 === 1}
|
|
2721
2846
|
class:sv-grid-row-draggable={rowDragManagedEffective}
|
|
2847
|
+
class:sv-grid-group-footer-row={isGroupFooterRow(row)}
|
|
2848
|
+
class:sv-grid-grand-total-row={isGrandTotalRow(row)}
|
|
2849
|
+
class:sv-grid-row-auto-height={autoRowHeightOn}
|
|
2722
2850
|
{...getGridRowA11yProps(rowIndex + 1)}
|
|
2723
2851
|
aria-level={sgAriaLevel(row)}
|
|
2724
2852
|
aria-expanded={sgAriaExpanded(row)}
|
|
2725
2853
|
{...rowDragAttrs(rowIndex)}
|
|
2726
|
-
style={
|
|
2854
|
+
style={rowStyleFor(rowIndex)}
|
|
2855
|
+
use:measureRowHeight={rowIndex}
|
|
2727
2856
|
>
|
|
2728
2857
|
{#if showRowNumbersEffective}
|
|
2729
2858
|
<td
|
package/dist/SvGrid.types.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Snippet } from "svelte";
|
|
2
2
|
import type { CellEditorType, ColumnDef, RowData, SvGridApi, TableFeatures } from "./index";
|
|
3
3
|
import type { ConditionalFormat } from "./conditional-formatting";
|
|
4
|
+
import type { GroupDisplayType } from "./group-display";
|
|
4
5
|
import type { GridMessages } from "./grid-messages";
|
|
5
6
|
import type { GridPivotConfig } from "./pivot-view.svelte";
|
|
6
7
|
import type { MenuItem } from "./SvMenuList.svelte";
|
|
@@ -992,7 +993,75 @@ export type Props<TFeatures extends TableFeatures = TableFeatures, TData extends
|
|
|
992
993
|
sortable?: boolean;
|
|
993
994
|
filterable?: boolean;
|
|
994
995
|
editable?: boolean;
|
|
996
|
+
/**
|
|
997
|
+
* Client-side tree data: nest rows into a hierarchy by parent id.
|
|
998
|
+
*
|
|
999
|
+
* ```svelte
|
|
1000
|
+
* <SvGrid {data} {columns} treeData={{ parentField: 'managerId', column: 'name' }} />
|
|
1001
|
+
* ```
|
|
1002
|
+
*
|
|
1003
|
+
* Unlike grouping, tree rows are real data rows - they keep their cells,
|
|
1004
|
+
* editing and selection, and only gain an expander plus indentation in
|
|
1005
|
+
* `column`. Rows whose parent is missing become roots rather than vanishing.
|
|
1006
|
+
*
|
|
1007
|
+
* For NESTED source data (`children: [...]`), flatten it first with
|
|
1008
|
+
* `flattenTreeData(data, { childrenField: 'children' })` and point
|
|
1009
|
+
* `parentField` at the `__parentId` it stamps on.
|
|
1010
|
+
*
|
|
1011
|
+
* Setting this replaces row grouping - a row cannot be both.
|
|
1012
|
+
*/
|
|
1013
|
+
treeData?: {
|
|
1014
|
+
/** Field holding each row's parent id. */
|
|
1015
|
+
parentField: string;
|
|
1016
|
+
/** Field holding the row's own id. Defaults to `'id'`. */
|
|
1017
|
+
idField?: string;
|
|
1018
|
+
/**
|
|
1019
|
+
* Column id that carries the expander + indentation. Defaults to the first
|
|
1020
|
+
* visible column.
|
|
1021
|
+
*/
|
|
1022
|
+
column?: string;
|
|
1023
|
+
/** Indent per depth level, in px. Default `12`. */
|
|
1024
|
+
indentPx?: number;
|
|
1025
|
+
};
|
|
995
1026
|
groupable?: boolean;
|
|
1027
|
+
/**
|
|
1028
|
+
* Render a subtotal row after each group's children, carrying that group's
|
|
1029
|
+
* aggregate values under the columns they belong to (the columns with an
|
|
1030
|
+
* `aggregate` set). Off by default.
|
|
1031
|
+
*
|
|
1032
|
+
* Footers are inserted after paging, so they never count against `pageSize` -
|
|
1033
|
+
* a page shows its `pageSize` data rows plus whatever footers close on it.
|
|
1034
|
+
*/
|
|
1035
|
+
groupFooters?: boolean;
|
|
1036
|
+
/**
|
|
1037
|
+
* Append a grand-total row at the very end, aggregating the whole filtered
|
|
1038
|
+
* set (not just the current page) with each column's `aggregate` function.
|
|
1039
|
+
*
|
|
1040
|
+
* Independent of `groupFooters`: use it on a flat grid for a bottom totals
|
|
1041
|
+
* line, or alongside group subtotals for both. Like group footers it is
|
|
1042
|
+
* appended after paging, so it never counts against `pageSize` - but it is
|
|
1043
|
+
* only appended on the LAST page, so a total never appears mid-dataset.
|
|
1044
|
+
* Columns without an `aggregate` are left blank.
|
|
1045
|
+
*/
|
|
1046
|
+
grandTotalRow?: boolean;
|
|
1047
|
+
/**
|
|
1048
|
+
* How grouped rows are displayed.
|
|
1049
|
+
*
|
|
1050
|
+
* - `groupRows` (default): a full-width banner row per group. Unchanged
|
|
1051
|
+
* behaviour - existing grids are untouched.
|
|
1052
|
+
* - `singleColumn`: one synthetic "Group" column holding every level,
|
|
1053
|
+
* indented by depth.
|
|
1054
|
+
* - `multipleColumns`: one synthetic column per grouped field.
|
|
1055
|
+
*
|
|
1056
|
+
* Both column modes hide the grouped source columns, since their values move
|
|
1057
|
+
* into the auto column(s), and render group rows as ordinary rows so their
|
|
1058
|
+
* aggregate cells line up under the real columns.
|
|
1059
|
+
*/
|
|
1060
|
+
groupDisplayMode?: GroupDisplayType;
|
|
1061
|
+
/** Header for the combined `singleColumn` auto-group column. Default `"Group"`. */
|
|
1062
|
+
autoGroupColumnHeader?: string;
|
|
1063
|
+
/** Width (px) of each auto-group column. Default `220`. */
|
|
1064
|
+
autoGroupColumnWidth?: number;
|
|
996
1065
|
pageable?: boolean;
|
|
997
1066
|
loading?: boolean;
|
|
998
1067
|
/**
|
|
@@ -1073,6 +1142,18 @@ export type Props<TFeatures extends TableFeatures = TableFeatures, TData extends
|
|
|
1073
1142
|
* per-row variable heights (e.g. an interactive row-resize feature).
|
|
1074
1143
|
* Defaults to 30. */
|
|
1075
1144
|
rowHeight?: number | ((rowIndex: number) => number);
|
|
1145
|
+
/**
|
|
1146
|
+
* Size each row to its own content instead of a fixed height: cell text wraps
|
|
1147
|
+
* and the row grows to fit the tallest cell. Rows are measured after they
|
|
1148
|
+
* render, so this works with virtualization - `rowHeight` (or 30) is the
|
|
1149
|
+
* estimate used before a row has been measured, which keeps the scrollbar
|
|
1150
|
+
* stable while you scroll into new rows.
|
|
1151
|
+
*
|
|
1152
|
+
* Costs a measurement pass per row, so prefer a fixed `rowHeight` when your
|
|
1153
|
+
* content is uniform. Ignored when `rowHeight` is a function (you are already
|
|
1154
|
+
* supplying per-row heights).
|
|
1155
|
+
*/
|
|
1156
|
+
autoRowHeight?: boolean;
|
|
1076
1157
|
/**
|
|
1077
1158
|
* Height (px) of a single column-header level row. With multi-level
|
|
1078
1159
|
* (grouped) headers the total header height is `levels * headerHeight`,
|
package/dist/SvGridChart.svelte
CHANGED
|
@@ -213,15 +213,7 @@
|
|
|
213
213
|
return { t0: zoom.i0 / n, t1: (zoom.i1 + 1) / n }
|
|
214
214
|
})
|
|
215
215
|
|
|
216
|
-
|
|
217
|
-
function brushXToIndex(localX: number): number {
|
|
218
|
-
if (!brushGeo) return 0
|
|
219
|
-
const w = brushGeo.plot.w
|
|
220
|
-
const x = Math.max(0, Math.min(w, localX - brushGeo.plot.x))
|
|
221
|
-
const n = spec.categories.length
|
|
222
|
-
return Math.max(0, Math.min(n - 1, Math.floor((x / w) * n)))
|
|
223
|
-
}
|
|
224
|
-
type BrushDrag = { mode: 'move' | 'left' | 'right'; startX: number; startT0: number; startT1: number }
|
|
216
|
+
type BrushDrag ={ mode: 'move' | 'left' | 'right'; startX: number; startT0: number; startT1: number }
|
|
225
217
|
let brushDrag = $state<BrushDrag | null>(null)
|
|
226
218
|
function brushSvgX(e: PointerEvent, svg: SVGSVGElement): number {
|
|
227
219
|
const r = svg.getBoundingClientRect()
|
|
@@ -919,7 +911,7 @@
|
|
|
919
911
|
{#if isRadar && geo.radarCenter}
|
|
920
912
|
{@const c = geo.radarCenter}
|
|
921
913
|
<!-- Concentric grid rings -->
|
|
922
|
-
{#each geo.radarRings as
|
|
914
|
+
{#each geo.radarRings as _v, ri (ri)}
|
|
923
915
|
{@const ringR = c.r * ((ri + 1) / geo.radarRings.length)}
|
|
924
916
|
<circle class="sv-grid-chart-radar-ring" cx={c.cx} cy={c.cy} r={ringR} />
|
|
925
917
|
{/each}
|
|
@@ -41,6 +41,10 @@
|
|
|
41
41
|
onCommit?: () => void
|
|
42
42
|
/** Called when the user dismisses (Escape, blur with no change). */
|
|
43
43
|
onCancel?: () => void
|
|
44
|
+
/** The option list is still being fetched (async `editorOptions`). Shows a
|
|
45
|
+
* loading line instead of "No options", which would read as "nothing to
|
|
46
|
+
* pick" when the truth is "not here yet". */
|
|
47
|
+
loading?: boolean
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
let {
|
|
@@ -54,6 +58,7 @@
|
|
|
54
58
|
onChange,
|
|
55
59
|
onCommit,
|
|
56
60
|
onCancel,
|
|
61
|
+
loading = false,
|
|
57
62
|
}: Props = $props()
|
|
58
63
|
|
|
59
64
|
/** Stable per-instance id used to build option ids for
|
|
@@ -158,9 +163,13 @@
|
|
|
158
163
|
function updatePanelPosition() {
|
|
159
164
|
if (!triggerEl) return
|
|
160
165
|
// Use the smaller of (option count, 10) for the upward-flip math so
|
|
161
|
-
// a 3-option list doesn't think it needs 320px of headroom.
|
|
162
|
-
|
|
163
|
-
|
|
166
|
+
// a 3-option list doesn't think it needs 320px of headroom. Measured off
|
|
167
|
+
// the VISIBLE options - the search-filtered list is what actually renders,
|
|
168
|
+
// and an async list is empty at open time.
|
|
169
|
+
const visibleCount = Math.min(visibleOptions.length, 10)
|
|
170
|
+
const searchHeight = searchable ? 38 : 0
|
|
171
|
+
const estimatedHeight =
|
|
172
|
+
visibleCount * optionRowHeight + (multiple ? 40 : 0) + searchHeight + 8
|
|
164
173
|
panelRect = anchoredRect(triggerEl.getBoundingClientRect(), {
|
|
165
174
|
estimatedHeight,
|
|
166
175
|
// Preserve the original behavior: anchor to the trigger, no clamping.
|
|
@@ -181,6 +190,20 @@
|
|
|
181
190
|
}
|
|
182
191
|
})
|
|
183
192
|
|
|
193
|
+
// Re-anchor when the option list itself changes while the panel is open.
|
|
194
|
+
// The geometry is derived from the option COUNT, so an async list - which
|
|
195
|
+
// lands after the panel opened, and a search filter, which shrinks it - would
|
|
196
|
+
// otherwise leave the panel sized for the list it had a moment ago. With an
|
|
197
|
+
// async source that means a panel sized for ZERO options: 8px tall, with the
|
|
198
|
+
// arriving options spilling outside its background and the grid showing
|
|
199
|
+
// through behind them.
|
|
200
|
+
$effect(() => {
|
|
201
|
+
if (!open) return
|
|
202
|
+
void visibleOptions.length
|
|
203
|
+
void optionRowHeight
|
|
204
|
+
updatePanelPosition()
|
|
205
|
+
})
|
|
206
|
+
|
|
184
207
|
function closePanel() {
|
|
185
208
|
open = false
|
|
186
209
|
highlighted = -1
|
|
@@ -387,9 +410,15 @@
|
|
|
387
410
|
{@const needsScroll = options.length > 10}
|
|
388
411
|
<!-- Cap to the comfortable 10-row height AND the room the viewport allows,
|
|
389
412
|
so a panel near the bottom edge scrolls instead of overflowing. -->
|
|
413
|
+
<!-- Capped by the REAL room the viewport allows (`availHeight`), never by
|
|
414
|
+
`maxHeight` - that one is clamped to our estimated height, and the
|
|
415
|
+
estimate assumes a 32px row. Themes with a larger font render taller
|
|
416
|
+
rows, so the estimate came up a few px short and the last option was
|
|
417
|
+
cut off by the panel edge. The estimate still decides which way to
|
|
418
|
+
flip; it must not decide how tall the panel is allowed to be. -->
|
|
390
419
|
{@const panelMax = Math.min(
|
|
391
420
|
needsScroll ? 10 * optionRowHeight + (multiple ? 40 : 0) + 8 : Number.POSITIVE_INFINITY,
|
|
392
|
-
panelRect.
|
|
421
|
+
panelRect.availHeight,
|
|
393
422
|
)}
|
|
394
423
|
<div
|
|
395
424
|
class="sv-grid-dropdown-panel"
|
|
@@ -400,7 +429,8 @@
|
|
|
400
429
|
use:portalToBody
|
|
401
430
|
use:popIn={{ up: panelRect.openUpward }}
|
|
402
431
|
style:position="fixed"
|
|
403
|
-
style:top={`${panelRect.top}px`}
|
|
432
|
+
style:top={panelRect.openUpward ? null : `${panelRect.top}px`}
|
|
433
|
+
style:bottom={panelRect.openUpward ? `${panelRect.bottom}px` : null}
|
|
404
434
|
style:left={`${panelRect.left}px`}
|
|
405
435
|
style:width={`${panelRect.width}px`}
|
|
406
436
|
style:max-height={panelMax !== null ? `${panelMax}px` : null}
|
|
@@ -422,7 +452,9 @@
|
|
|
422
452
|
}}
|
|
423
453
|
/>
|
|
424
454
|
{/if}
|
|
425
|
-
{#if visibleOptions.length === 0}
|
|
455
|
+
{#if loading && visibleOptions.length === 0}
|
|
456
|
+
<div class="sv-grid-dropdown-empty" role="status" aria-live="polite">Loading…</div>
|
|
457
|
+
{:else if visibleOptions.length === 0}
|
|
426
458
|
<div class="sv-grid-dropdown-empty">{searchable && searchQuery ? `No matches for "${searchQuery}"` : 'No options'}</div>
|
|
427
459
|
{:else}
|
|
428
460
|
{#each visibleOptions as opt, i (String(opt.value))}
|