@svgrid/grid 3.0.4 → 3.0.5
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 +4 -4
- package/dist/SvGrid.controller.svelte.d.ts +12 -2
- package/dist/SvGrid.controller.svelte.js +192 -8
- package/dist/SvGrid.css +73 -0
- package/dist/SvGrid.svelte +128 -3
- package/dist/SvGrid.types.d.ts +356 -3
- package/dist/build-api.js +11 -6
- package/dist/cdn/{GridMenus-BL7ZgQvU.js → GridMenus-CID62rlI.js} +11 -11
- package/dist/cdn/{GridMenus-7kbpnnBW.js → GridMenus-DLL-qZeu.js} +11 -11
- package/dist/cdn/server-block-cache-CGoWz-87.js +237 -0
- package/dist/cdn/{src-BKhZ6eXd.js → src-Bklofsad.js} +8818 -8798
- package/dist/cdn/{src-V1uu8iE9.js → src-jcFMkBnN.js} +4282 -4262
- package/dist/cdn/svgrid.js +27 -26
- package/dist/cdn/svgrid.svelte-external.js +27 -26
- package/dist/cdn/validate-C7Rn_Bns.js +76 -0
- package/dist/core.js +36 -13
- package/dist/editing.js +10 -0
- package/dist/gantt-view.svelte.d.ts +24 -0
- package/dist/gantt-view.svelte.js +13 -0
- package/dist/grid-icons.d.ts +2 -2
- package/dist/grid-icons.js +2 -2
- package/dist/grid-messages.d.ts +6 -0
- package/dist/grid-messages.js +5 -0
- package/dist/index.d.ts +5 -4
- package/dist/index.js +3 -3
- package/dist/row-model.d.ts +133 -0
- package/dist/row-model.js +16 -0
- package/dist/selection.d.ts +2 -1
- package/dist/selection.js +30 -2
- package/dist/server-block-cache.d.ts +214 -0
- package/dist/server-block-cache.js +531 -0
- package/dist/server-data-source.d.ts +240 -6
- package/dist/server-data-source.js +220 -20
- package/dist/server.d.ts +13 -0
- package/dist/server.js +13 -0
- package/dist/validate.d.ts +4 -0
- package/dist/validate.js +21 -3
- package/package.json +6 -1
- package/src/SvGrid.controller.svelte.ts +191 -8
- package/src/SvGrid.css +73 -0
- package/src/SvGrid.svelte +128 -3
- package/src/SvGrid.types.ts +376 -3
- package/src/build-api.ts +11 -4
- package/src/core.rowmodel-cache.test.ts +51 -0
- package/src/core.ts +33 -12
- package/src/editing.ts +8 -0
- package/src/gantt-stub.test.svelte +38 -0
- package/src/gantt-view.svelte.ts +35 -0
- package/src/grid-icons.ts +2 -2
- package/src/grid-messages.ts +12 -0
- package/src/icon-seam.test.ts +2 -4
- package/src/index.ts +36 -10
- package/src/row-model.ts +146 -0
- package/src/selection.test.ts +3 -0
- package/src/selection.ts +32 -2
- package/src/server-block-cache.test.ts +645 -0
- package/src/server-block-cache.ts +677 -0
- package/src/server-data-source.infinite.test.ts +343 -0
- package/src/server-data-source.ts +450 -27
- package/src/server.ts +47 -0
- package/src/svgrid.gantt-seam.test.ts +199 -0
- package/src/svgrid.row-model-prop.svelte.test.ts +352 -0
- package/src/svgrid.row-model-seam.svelte.test.ts +289 -0
- package/src/svgrid.upsell-license.test.ts +7 -4
- package/src/validate.test.ts +29 -0
- package/src/validate.ts +28 -3
- package/dist/SvGroupCell.svelte +0 -141
- package/dist/SvGroupCell.svelte.d.ts +0 -49
- package/dist/SvRowGroupPanel.svelte +0 -186
- package/dist/SvRowGroupPanel.svelte.d.ts +0 -25
- package/dist/cdn/validate-_CDJzgIo.js +0 -75
- package/dist/server-group-model.d.ts +0 -98
- package/dist/server-group-model.js +0 -263
- package/src/SvGroupCell.svelte +0 -141
- package/src/SvRowGroupPanel.svelte +0 -186
- package/src/server-group-model.test.ts +0 -294
- package/src/server-group-model.ts +0 -370
package/README.md
CHANGED
|
@@ -92,7 +92,7 @@ all wire up the moment you turn on the matching prop.
|
|
|
92
92
|
- **Selection** - cell-range click+drag and Shift+arrows, copy/paste as TSV, Excel-style fill handle, row selection.
|
|
93
93
|
- **Views** - row grouping with aggregation, tree data, master/detail, full-width detail rows, spreadsheet mode with formulas.
|
|
94
94
|
- **Layout** - row + column pinning, sticky header + first column, header drag-to-reorder, autosize, keyboard-accessible column sizing, zebra rows, responsive mode.
|
|
95
|
-
- **Data operations** - find in grid (Ctrl+F), undo / redo, transaction API, optimistic updates, server-side
|
|
95
|
+
- **Data operations** - find in grid (Ctrl+F), undo / redo, transaction API, optimistic updates, server-side data with sort / filter / paging / infinite-scroll pushdown (lazy grouping, tree, pivot and transactions on the server are in `@svgrid/enterprise`).
|
|
96
96
|
- **AI helpers, free** - natural-language filter, smart fill, summarize, classify, anomaly detection, and "chart this". Model-agnostic: register one provider, nothing is bundled.
|
|
97
97
|
- **UI components** - a Svelte 5 component suite ships in the package (inputs, selection, date/time, overlays, layout, feedback), standalone or as grid cell editors.
|
|
98
98
|
- **Accessibility** - WAI-ARIA grid roles, full keyboard navigation, RTL, high-contrast theme.
|
|
@@ -154,14 +154,14 @@ See [Use sv-grid docs as LLM context](https://svgrid.com/docs/help/llm-grounding
|
|
|
154
154
|
## Enterprise features
|
|
155
155
|
|
|
156
156
|
The MIT core is free for any use, including commercial, with **no feature gating**. Sorting, filtering,
|
|
157
|
-
editing, virtualization,
|
|
158
|
-
package. The optional [`@svgrid/enterprise`](https://www.npmjs.com/package/@svgrid/enterprise) pack layers
|
|
157
|
+
editing, virtualization, **server-side data** (paging, infinite block scroll, sort, filter, CRUD) and the
|
|
158
|
+
**AI helpers** all ship in the free package. The optional [`@svgrid/enterprise`](https://www.npmjs.com/package/@svgrid/enterprise) pack layers
|
|
159
159
|
on the export- and analytics-heavy features teams tend to need last:
|
|
160
160
|
|
|
161
161
|
- **Excel / PDF / CSV / TSV / HTML export** plus paginated print.
|
|
162
162
|
- **Excel / CSV / TSV / JSON import** with auto-mapping and per-row validation.
|
|
163
163
|
- **Pivot tables** with a drag-and-drop pivot Designer.
|
|
164
|
-
- **Kanban board
|
|
164
|
+
- **Kanban board, scheduler / calendar and Gantt renderers** for the `board`, `scheduler` and `gantt` props.
|
|
165
165
|
|
|
166
166
|
OSS projects receive an Enterprise key free. See [Pricing](https://svgrid.com/pricing/).
|
|
167
167
|
|
|
@@ -297,6 +297,8 @@ export declare function createSvGridController<TFeatures extends TableFeatures =
|
|
|
297
297
|
rows: TData[];
|
|
298
298
|
ids: string[];
|
|
299
299
|
};
|
|
300
|
+
readonly selectionBarCount: number;
|
|
301
|
+
readonly selectionBarVisible: boolean;
|
|
300
302
|
lastSelectionSerialized: string;
|
|
301
303
|
lastCellRangeSerialized: string;
|
|
302
304
|
readonly statusBarEnabled: boolean;
|
|
@@ -598,10 +600,18 @@ export declare function createSvGridController<TFeatures extends TableFeatures =
|
|
|
598
600
|
readonly getPinnedCellValue: typeof getPinnedCellValue;
|
|
599
601
|
readonly formatPinnedValue: (column: Column<TData>, value: unknown) => string;
|
|
600
602
|
readonly computePinnedCellClass: (rowData: TData, column: Column<TData>) => string;
|
|
601
|
-
readonly isRowSelected: (rowId: string) =>
|
|
603
|
+
readonly isRowSelected: (rowId: string) => any;
|
|
602
604
|
readonly toggleRowSelectionById: (rowId: string) => void;
|
|
603
|
-
readonly headerSelectionState: "
|
|
605
|
+
readonly headerSelectionState: "some" | "none" | "all";
|
|
606
|
+
readonly visibleRowRange: {
|
|
607
|
+
startIndex: number;
|
|
608
|
+
endIndex: number;
|
|
609
|
+
};
|
|
610
|
+
placeholderStateOf: (row: Row<TData>) => "loading" | "failed" | null;
|
|
611
|
+
/** Retry a failed placeholder row. No-op without `onRetryRow`. */
|
|
612
|
+
retryRow(row: Row<TData>): void;
|
|
604
613
|
readonly toggleSelectAllRows: () => void;
|
|
614
|
+
readonly setSelectAllRows: (select: boolean) => void;
|
|
605
615
|
userHasActivatedCell: boolean;
|
|
606
616
|
tabRunOrigin: number | null;
|
|
607
617
|
readonly setActiveCell: (rowIndex: number, colIndex: number) => void;
|
|
@@ -171,6 +171,60 @@ export function createSvGridController(rawProps, domIdBase) {
|
|
|
171
171
|
// preserves reference identity for non-overridden keys, which the columns-identity
|
|
172
172
|
// column cache and the `data`/`columns` sync effects rely on.
|
|
173
173
|
let optionOverrides = $state({});
|
|
174
|
+
/**
|
|
175
|
+
* A `rowModel` answers for the props it can, so a server-backed grid is
|
|
176
|
+
* one prop rather than a dozen.
|
|
177
|
+
*
|
|
178
|
+
* Bumped by the model itself through `subscribe` (see the effect below),
|
|
179
|
+
* which is what makes `getRows()` and `isLoading()` - plain method calls,
|
|
180
|
+
* invisible to the reactive system - re-read when the model changes.
|
|
181
|
+
*/
|
|
182
|
+
let rowModelVersion = $state(0);
|
|
183
|
+
const rowModelProps = $derived.by(function rowModelProps_d() {
|
|
184
|
+
const model = rawProps.rowModel;
|
|
185
|
+
if (!model)
|
|
186
|
+
return null;
|
|
187
|
+
rowModelVersion; // read, so a notification re-derives
|
|
188
|
+
const paging = model.pagination;
|
|
189
|
+
return {
|
|
190
|
+
data: model.getRows(),
|
|
191
|
+
loading: model.isLoading(),
|
|
192
|
+
loadingOverlay: true,
|
|
193
|
+
getRowId: model.getRowId,
|
|
194
|
+
// A model that sorts or filters server-side is telling the grid not
|
|
195
|
+
// to do it locally; one that does neither leaves the grid in charge.
|
|
196
|
+
externalSort: model.setSort ? true : undefined,
|
|
197
|
+
externalFilter: model.setFilter ? true : undefined,
|
|
198
|
+
onSortingChange: model.setSort ? (s) => model.setSort(s) : undefined,
|
|
199
|
+
onFiltersChange: model.setFilter ? (f) => model.setFilter(f) : undefined,
|
|
200
|
+
onVisibleRangeChange: model.setViewport
|
|
201
|
+
? (r) => model.setViewport(r.startIndex, r.endIndex)
|
|
202
|
+
: undefined,
|
|
203
|
+
rowPlaceholder: model.rowPlaceholder,
|
|
204
|
+
onRetryRow: model.retryRow,
|
|
205
|
+
serverGroup: model.group,
|
|
206
|
+
rowSelectionModel: model.selection,
|
|
207
|
+
serverFilterValues: model.filterValues
|
|
208
|
+
? (columnId) => model.filterValues(columnId)
|
|
209
|
+
: undefined,
|
|
210
|
+
pinnedTopRows: model.pinnedTopRows,
|
|
211
|
+
pinnedBottomRows: model.pinnedBottomRows,
|
|
212
|
+
pivotResultColumns: model.pivotResultColumns ?? undefined,
|
|
213
|
+
externalPagination: paging ? true : undefined,
|
|
214
|
+
rowCount: paging?.rowCount,
|
|
215
|
+
pageIndex: paging?.pageIndex,
|
|
216
|
+
pageSize: paging?.pageSize,
|
|
217
|
+
pageSizeOptions: paging?.pageSizes,
|
|
218
|
+
onPaginationChange: paging
|
|
219
|
+
? (p) => {
|
|
220
|
+
if (p.pageSize !== paging.pageSize)
|
|
221
|
+
paging.setPageSize(p.pageSize);
|
|
222
|
+
else
|
|
223
|
+
paging.setPage(p.pageIndex);
|
|
224
|
+
}
|
|
225
|
+
: undefined,
|
|
226
|
+
};
|
|
227
|
+
});
|
|
174
228
|
const props = new Proxy(rawProps, {
|
|
175
229
|
// Read the override as a PROPERTY (tracked by `$state`), not via `in` (the `has`
|
|
176
230
|
// trap is not tracked) - so a reactive read of `props.X` re-runs when an override
|
|
@@ -179,7 +233,23 @@ export function createSvGridController(rawProps, domIdBase) {
|
|
|
179
233
|
// undefined too. Whole-object reassignment on write makes the signal fire.
|
|
180
234
|
get: (t, k) => {
|
|
181
235
|
const v = typeof k === "string" ? optionOverrides[k] : undefined;
|
|
182
|
-
|
|
236
|
+
if (v !== undefined)
|
|
237
|
+
return v;
|
|
238
|
+
// Pivot mode swaps the column list: `pivotResultColumns` (the prop, or
|
|
239
|
+
// the row model's) stands in for `columns` while it is set, and
|
|
240
|
+
// `columns` itself stays the app's own list for when pivot ends.
|
|
241
|
+
if (k === "columns") {
|
|
242
|
+
const pivot = Reflect.get(t, "pivotResultColumns") ??
|
|
243
|
+
rowModelProps?.pivotResultColumns;
|
|
244
|
+
if (pivot)
|
|
245
|
+
return pivot;
|
|
246
|
+
}
|
|
247
|
+
const own = Reflect.get(t, k);
|
|
248
|
+
// A prop written on the grid always beats the row model, so adopting
|
|
249
|
+
// `rowModel` is never all-or-nothing.
|
|
250
|
+
if (own !== undefined || typeof k !== "string")
|
|
251
|
+
return own;
|
|
252
|
+
return rowModelProps?.[k];
|
|
183
253
|
},
|
|
184
254
|
});
|
|
185
255
|
// Resolved capability gates. Capabilities are OFF by default - a bare
|
|
@@ -518,7 +588,10 @@ export function createSvGridController(rawProps, domIdBase) {
|
|
|
518
588
|
// re-synced whenever the parent passes a new array; the imperative API
|
|
519
589
|
// mutates these so add/remove operations don't need a callback round-trip.
|
|
520
590
|
// svelte-ignore state_referenced_locally
|
|
521
|
-
|
|
591
|
+
// `data` is optional on the type (a `rowModel` may supply the rows
|
|
592
|
+
// instead), so every read falls back to empty rather than to undefined.
|
|
593
|
+
const EMPTY_DATA = [];
|
|
594
|
+
let internalData = $state.raw(props.data ?? EMPTY_DATA);
|
|
522
595
|
// Resolve `cellDataType` / `inferColumnTypes` into concrete editorType +
|
|
523
596
|
// format defaults once, up front, so every downstream reader sees a normal
|
|
524
597
|
// column. Explicit fields on the ColumnDef always win.
|
|
@@ -563,7 +636,7 @@ export function createSvGridController(rawProps, domIdBase) {
|
|
|
563
636
|
// accumulated cell-edit overrides - otherwise `getCellDisplayValue`
|
|
564
637
|
// would keep returning the old edited values from `editedCellValues`
|
|
565
638
|
// even though the underlying data has been replaced.
|
|
566
|
-
internalData = props.data;
|
|
639
|
+
internalData = props.data ?? EMPTY_DATA;
|
|
567
640
|
editedCellValues = {};
|
|
568
641
|
});
|
|
569
642
|
$effect(() => {
|
|
@@ -796,6 +869,20 @@ export function createSvGridController(rawProps, domIdBase) {
|
|
|
796
869
|
*
|
|
797
870
|
* Returns null when the cell should render nothing.
|
|
798
871
|
*/
|
|
872
|
+
/**
|
|
873
|
+
* Why a row has no data yet, or null when it does.
|
|
874
|
+
*
|
|
875
|
+
* Group rows are never placeholders, so the check is skipped for them:
|
|
876
|
+
* their `original` is a synthetic bucket, not a row the consumer handed
|
|
877
|
+
* us, and passing it to `rowPlaceholder` would ask a question about a row
|
|
878
|
+
* that does not exist.
|
|
879
|
+
*/
|
|
880
|
+
function placeholderStateOf(row) {
|
|
881
|
+
const ask = props.rowPlaceholder;
|
|
882
|
+
if (!ask || isGroupRow(row))
|
|
883
|
+
return null;
|
|
884
|
+
return ask(row.original, row.index);
|
|
885
|
+
}
|
|
799
886
|
function autoGroupCell(row, columnId) {
|
|
800
887
|
if (!isGroupRow(row))
|
|
801
888
|
return null;
|
|
@@ -1458,16 +1545,44 @@ export function createSvGridController(rawProps, domIdBase) {
|
|
|
1458
1545
|
const ids = [];
|
|
1459
1546
|
if (!selectionBarOn)
|
|
1460
1547
|
return { rows, ids };
|
|
1548
|
+
// Under an external selection model the loaded rows are the ones the
|
|
1549
|
+
// model says are selected; rows that never loaded are not in `allRows`
|
|
1550
|
+
// and are counted by `selectionBarCount` below instead. This walks
|
|
1551
|
+
// every loaded row, so it runs on every block a row model delivers:
|
|
1552
|
+
// a model that can say "nothing is selected" spares the walk, and the
|
|
1553
|
+
// props it needs are read once, not through the proxy per row.
|
|
1554
|
+
const model = props.rowSelectionModel;
|
|
1555
|
+
if (model && model.selectedCount?.() === 0)
|
|
1556
|
+
return { rows, ids };
|
|
1557
|
+
const ask = props.rowPlaceholder;
|
|
1558
|
+
const isPlaceholder = ask
|
|
1559
|
+
? (row) => !isGroupRow(row) && !!ask(row.original, row.index)
|
|
1560
|
+
: () => false;
|
|
1461
1561
|
for (const row of allRows) {
|
|
1462
|
-
if (isGroupRow(row))
|
|
1562
|
+
if (isGroupRow(row) || isPlaceholder(row))
|
|
1463
1563
|
continue;
|
|
1464
|
-
|
|
1564
|
+
const selected = model
|
|
1565
|
+
? model.isSelected(row.id, row.original)
|
|
1566
|
+
: !!rowSelectionState[row.id];
|
|
1567
|
+
if (selected) {
|
|
1465
1568
|
rows.push(row.original);
|
|
1466
1569
|
ids.push(row.id);
|
|
1467
1570
|
}
|
|
1468
1571
|
}
|
|
1469
1572
|
return { rows, ids };
|
|
1470
1573
|
});
|
|
1574
|
+
/**
|
|
1575
|
+
* What the selection bar's count chip says. A model that stores the rule
|
|
1576
|
+
* ("everything except these") knows about rows the grid never loaded, so
|
|
1577
|
+
* its number wins over counting ticks on screen; `null` from it means it
|
|
1578
|
+
* cannot say, and the bar falls back to the loaded count.
|
|
1579
|
+
*/
|
|
1580
|
+
const selectionBarCount = $derived.by(function selectionBarCount_d() {
|
|
1581
|
+
const fromModel = props.rowSelectionModel?.selectedCount?.();
|
|
1582
|
+
return fromModel ?? selectionBarTarget.ids.length;
|
|
1583
|
+
});
|
|
1584
|
+
/** Whether the bar has anything to show: the model's count when it has one, else the loaded ticks. */
|
|
1585
|
+
const selectionBarVisible = $derived(selectionBarOn && selectionBarCount > 0);
|
|
1471
1586
|
// Forward selection changes to the consumer. Skips the very first invocation
|
|
1472
1587
|
// (the initial empty state) so consumers don't get a spurious callback on mount.
|
|
1473
1588
|
let lastSelectionSerialized = "";
|
|
@@ -2419,6 +2534,59 @@ export function createSvGridController(rawProps, domIdBase) {
|
|
|
2419
2534
|
const virtualRowStart = $derived.by(() => virtualRows[0]?.start ?? 0);
|
|
2420
2535
|
const virtualRowEnd = $derived.by(() => virtualRows[virtualRows.length - 1]?.end ?? 0);
|
|
2421
2536
|
const virtualRowBottomSpacer = $derived.by(() => Math.max(virtualRowTotalSize - virtualRowEnd, 0));
|
|
2537
|
+
// A row model tells us when to re-read it. Subscribing here (rather than
|
|
2538
|
+
// polling, or asking the consumer to wrap the model in a rune) is what lets
|
|
2539
|
+
// `getRows()` be a plain method call on a plain object.
|
|
2540
|
+
$effect(() => {
|
|
2541
|
+
const model = rawProps.rowModel;
|
|
2542
|
+
if (!model)
|
|
2543
|
+
return;
|
|
2544
|
+
return model.subscribe(() => {
|
|
2545
|
+
rowModelVersion += 1;
|
|
2546
|
+
});
|
|
2547
|
+
});
|
|
2548
|
+
// The index range on screen, for data sources that fetch by block.
|
|
2549
|
+
// `virtualRowStart` / `virtualRowEnd` above are PIXEL offsets; a fetcher
|
|
2550
|
+
// needs row numbers, and needs them whether or not virtualization is on.
|
|
2551
|
+
const visibleRowRange = $derived.by(function visibleRowRange_d() {
|
|
2552
|
+
if (!rowVirtualizationEnabled) {
|
|
2553
|
+
return { startIndex: 0, endIndex: Math.max(0, allRows.length - 1) };
|
|
2554
|
+
}
|
|
2555
|
+
const first = virtualRows[0];
|
|
2556
|
+
const last = virtualRows[virtualRows.length - 1];
|
|
2557
|
+
if (!first || !last)
|
|
2558
|
+
return { startIndex: 0, endIndex: 0 };
|
|
2559
|
+
return { startIndex: first.index, endIndex: last.index };
|
|
2560
|
+
});
|
|
2561
|
+
// Emitted from an effect rather than the derived so a consumer that
|
|
2562
|
+
// fetches (and therefore changes `data`) cannot re-enter the derivation.
|
|
2563
|
+
let lastReportedRange = "";
|
|
2564
|
+
$effect(() => {
|
|
2565
|
+
const report = props.onVisibleRangeChange;
|
|
2566
|
+
if (!report)
|
|
2567
|
+
return;
|
|
2568
|
+
const range = visibleRowRange;
|
|
2569
|
+
const key = range.startIndex + ":" + range.endIndex;
|
|
2570
|
+
if (key === lastReportedRange)
|
|
2571
|
+
return;
|
|
2572
|
+
lastReportedRange = key;
|
|
2573
|
+
report(range);
|
|
2574
|
+
});
|
|
2575
|
+
// A paging row model that asks for `autoPageSize` gets as many rows as
|
|
2576
|
+
// the body shows without scrolling, re-measured as the viewport changes.
|
|
2577
|
+
$effect(() => {
|
|
2578
|
+
rowModelVersion;
|
|
2579
|
+
const paging = rawProps.rowModel?.pagination;
|
|
2580
|
+
if (!paging?.autoPageSize)
|
|
2581
|
+
return;
|
|
2582
|
+
const height = viewportHeight;
|
|
2583
|
+
if (height <= 0)
|
|
2584
|
+
return;
|
|
2585
|
+
const rowPx = rowSizePxOf(0) || 30;
|
|
2586
|
+
const fit = Math.max(1, Math.floor(height / rowPx));
|
|
2587
|
+
if (fit !== paging.pageSize)
|
|
2588
|
+
paging.setPageSize(fit);
|
|
2589
|
+
});
|
|
2422
2590
|
// --- Huge-list scroll scaling -----------------------------------------
|
|
2423
2591
|
// Browsers cap how tall a single element may be, and mobile caps sit well
|
|
2424
2592
|
// below desktop. Past a few hundred thousand rows the true content height
|
|
@@ -3147,7 +3315,12 @@ export function createSvGridController(rawProps, domIdBase) {
|
|
|
3147
3315
|
// rows (10k iterations on the large-dataset demo, 100k on its top tier) to
|
|
3148
3316
|
// recount a selection that had not changed.
|
|
3149
3317
|
const headerSelectionState = $derived.by(function headerSelectionState_d() {
|
|
3150
|
-
|
|
3318
|
+
// An external model knows about rows the grid has never seen, so it is
|
|
3319
|
+
// the only thing that can answer honestly once the data is a window
|
|
3320
|
+
// onto a server.
|
|
3321
|
+
if (props.rowSelectionModel)
|
|
3322
|
+
return props.rowSelectionModel.headerState();
|
|
3323
|
+
const selectable = allRows.filter((row) => !isGroupRow(row) && !placeholderStateOf(row));
|
|
3151
3324
|
if (!selectable.length)
|
|
3152
3325
|
return "none";
|
|
3153
3326
|
let selected = 0;
|
|
@@ -3300,7 +3473,7 @@ export function createSvGridController(rawProps, domIdBase) {
|
|
|
3300
3473
|
const meta = isBucketableColumn(column);
|
|
3301
3474
|
if (!meta)
|
|
3302
3475
|
continue;
|
|
3303
|
-
const buckets = buildBuckets(column, meta.isDate, props.data, getColumnAccessorValue);
|
|
3476
|
+
const buckets = buildBuckets(column, meta.isDate, props.data ?? EMPTY_DATA, getColumnAccessorValue);
|
|
3304
3477
|
if (buckets)
|
|
3305
3478
|
map.set(column.id, buckets);
|
|
3306
3479
|
}
|
|
@@ -3788,6 +3961,8 @@ export function createSvGridController(rawProps, domIdBase) {
|
|
|
3788
3961
|
get selectionBarMaxVisible() { return selectionBarMaxVisible; },
|
|
3789
3962
|
get selectionBarHideClear() { return selectionBarHideClear; },
|
|
3790
3963
|
get selectionBarTarget() { return selectionBarTarget; },
|
|
3964
|
+
get selectionBarCount() { return selectionBarCount; },
|
|
3965
|
+
get selectionBarVisible() { return selectionBarVisible; },
|
|
3791
3966
|
get lastSelectionSerialized() { return lastSelectionSerialized; },
|
|
3792
3967
|
set lastSelectionSerialized(v) { lastSelectionSerialized = v; },
|
|
3793
3968
|
get lastCellRangeSerialized() { return lastCellRangeSerialized; },
|
|
@@ -4125,7 +4300,14 @@ export function createSvGridController(rawProps, domIdBase) {
|
|
|
4125
4300
|
get isRowSelected() { return isRowSelected; },
|
|
4126
4301
|
get toggleRowSelectionById() { return toggleRowSelectionById; },
|
|
4127
4302
|
get headerSelectionState() { return headerSelectionState; },
|
|
4303
|
+
get visibleRowRange() { return visibleRowRange; },
|
|
4304
|
+
placeholderStateOf,
|
|
4305
|
+
/** Retry a failed placeholder row. No-op without `onRetryRow`. */
|
|
4306
|
+
retryRow(row) {
|
|
4307
|
+
props.onRetryRow?.(row.original, row.index);
|
|
4308
|
+
},
|
|
4128
4309
|
get toggleSelectAllRows() { return toggleSelectAllRows; },
|
|
4310
|
+
get setSelectAllRows() { return setSelectAllRows; },
|
|
4129
4311
|
get userHasActivatedCell() { return userHasActivatedCell; },
|
|
4130
4312
|
set userHasActivatedCell(v) { userHasActivatedCell = v; },
|
|
4131
4313
|
get tabRunOrigin() { return tabRunOrigin; },
|
|
@@ -4274,7 +4456,7 @@ export function createSvGridController(rawProps, domIdBase) {
|
|
|
4274
4456
|
const { updateFilterRow, updateFilterOperator, updateFilterMenuValue, updateFilterMenuValueTo, addFilterToken, removeFilterToken, toggleFilterToken, toggleCheckboxWithKeyboard, isColumnFiltered, closeMenus, openInSuggest, closeInSuggest, openChooseColumns, openColumnMenu, openFilterMenu, openOperatorMenu, sortColumnFromMenu, clearColumnSort, groupByColumnFromMenu, clearGroupingFromMenu, isFacetChecked, toggleFacetValue, setFacetSelection, setFilterTokens, isAllFacetsChecked, toggleAllFacets, clearColumnFilter, changePage, goToPage, setPageSize, openContextMenu, closeContextMenu, contextMenuItems, saveComment, removeComment, closeCommentEditor } = createMenus(ctx);
|
|
4275
4457
|
const { cellConditionalFormat, computeRowClass, computeCellClass, computeCellTooltip, computeCellValidity, computeCellNote, getColumnEditorOptions, areEditorOptionsLoading, formatListCellValue, formatCellValue, formatPinnedValue, computePinnedCellClass } = createCellRender(ctx);
|
|
4276
4458
|
const { isCellEditable, isCellEditableAt, getRowColumnValue, getCellDisplayValue, startEditingWithChar, startEditing, stopEditing, startFullRowEdit, setFullRowDraft, commitFullRowEdit, cancelFullRowEdit, saveEditingCell, applyHistoryStep, updateEditingCellValue, onEditorKeyDown, commitAndMoveByTab, focusOnMount, onCellDoubleClick, pasteFromClipboard, onGridPaste, armPasteFallback } = createEditing(ctx);
|
|
4277
|
-
const { isRowSelected, toggleRowSelectionById, toggleSelectAllRows, setActiveCell, scrollActiveCellIntoView, setSelection, extendSelection, isCellInSelectedRange, getCellRangeEdges, activeRangeRect, cellSelectionState, getSelectionRects, isInFillPreview, dragPreviewRect, fillMarqueeEdges, findColumnById, onCellPointerDown, onCellPointerEnter, endDragSelection, onWindowPointerMove, onCellClick, emitCellDoubleClick } = createSelection(ctx);
|
|
4459
|
+
const { isRowSelected, toggleRowSelectionById, toggleSelectAllRows, setSelectAllRows, setActiveCell, scrollActiveCellIntoView, setSelection, extendSelection, isCellInSelectedRange, getCellRangeEdges, activeRangeRect, cellSelectionState, getSelectionRects, isInFillPreview, dragPreviewRect, fillMarqueeEdges, findColumnById, onCellPointerDown, onCellPointerEnter, endDragSelection, onWindowPointerMove, onCellClick, emitCellDoubleClick } = createSelection(ctx);
|
|
4278
4460
|
const { cellPinStyle, isColumnPinned, getCurrentColumnOrder, emitColumnOrder, setColumnOrderInternal, applyColumnDrop, onColumnHeaderDragStart, onColumnHeaderDragOver, onColumnHeaderDragLeave, onColumnHeaderDrop, onColumnHeaderDragEnd, pinColumnLeft, pinColumnRight, unpinColumn, toggleColumnVisibleInPanel, moveColumnInPanel, toggleGroupInPanel, getColumnBaseWidth, getColumnWidth, measureText, autosizeColumn, autosizeAllColumns, resetColumns } = createColumns(ctx);
|
|
4279
4461
|
const { onRowDragStart, onRowDragOver, onRowDragLeave, onRowDrop, onRowsContainerDragOver, onRowsContainerDrop, onRowDragEnd, onRowPointerDown, destroyRowDrag } = createRowDrag(ctx);
|
|
4280
4462
|
const { register: registerAlignedGrid, broadcastScroll: broadcastAlignedScroll, broadcastWidths: broadcastAlignedWidths } = createAlignedGrids(ctx);
|
|
@@ -4313,6 +4495,8 @@ export function createSvGridController(rawProps, domIdBase) {
|
|
|
4313
4495
|
pageSize: props.pageSize,
|
|
4314
4496
|
groupBy: props.groupBy,
|
|
4315
4497
|
treeData: props.treeData,
|
|
4498
|
+
gantt: props.gantt,
|
|
4499
|
+
serverGroup: props.serverGroup,
|
|
4316
4500
|
initialColumnPinning: props.initialColumnPinning,
|
|
4317
4501
|
columnVirtualization: props.columnVirtualization,
|
|
4318
4502
|
externalPagination: props.externalPagination,
|
package/dist/SvGrid.css
CHANGED
|
@@ -463,6 +463,79 @@
|
|
|
463
463
|
user-select: text;
|
|
464
464
|
}
|
|
465
465
|
|
|
466
|
+
/* Rows whose data has not arrived yet (server-side / infinite loading).
|
|
467
|
+
|
|
468
|
+
The shimmer is per CELL, not per row, so the columns keep their widths
|
|
469
|
+
and the table does not reflow as blocks land. Widths vary a little by
|
|
470
|
+
column position so a screenful of them reads as text rather than as a
|
|
471
|
+
barcode - the same trick every skeleton screen uses. */
|
|
472
|
+
.sv-grid-placeholder-cell {
|
|
473
|
+
user-select: none;
|
|
474
|
+
cursor: default;
|
|
475
|
+
}
|
|
476
|
+
/* The per-cell shimmer of a placeholder row. Its own name: `.sv-grid-skeleton`
|
|
477
|
+
above is the full-body loading overlay, absolutely positioned under the
|
|
478
|
+
header, and sharing it pulled every placeholder cell's bar up there. */
|
|
479
|
+
.sv-grid-placeholder-skeleton {
|
|
480
|
+
display: inline-block;
|
|
481
|
+
width: 72%;
|
|
482
|
+
height: 9px;
|
|
483
|
+
border-radius: 5px;
|
|
484
|
+
vertical-align: middle;
|
|
485
|
+
background: linear-gradient(
|
|
486
|
+
90deg,
|
|
487
|
+
color-mix(in srgb, var(--sg-fg, #0f172a) 8%, transparent) 25%,
|
|
488
|
+
color-mix(in srgb, var(--sg-fg, #0f172a) 3%, transparent) 50%,
|
|
489
|
+
color-mix(in srgb, var(--sg-fg, #0f172a) 8%, transparent) 75%
|
|
490
|
+
);
|
|
491
|
+
background-size: 200% 100%;
|
|
492
|
+
animation: sv-grid-skeleton-shimmer 1.2s linear infinite;
|
|
493
|
+
}
|
|
494
|
+
.sv-grid-placeholder-cell:nth-child(2n) .sv-grid-placeholder-skeleton { width: 54%; }
|
|
495
|
+
.sv-grid-placeholder-cell:nth-child(3n) .sv-grid-placeholder-skeleton { width: 84%; }
|
|
496
|
+
@keyframes sv-grid-skeleton-shimmer {
|
|
497
|
+
from { background-position: 200% 0; }
|
|
498
|
+
to { background-position: -200% 0; }
|
|
499
|
+
}
|
|
500
|
+
@media (prefers-reduced-motion: reduce) {
|
|
501
|
+
.sv-grid-placeholder-skeleton { animation: none; }
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/* A block that failed to load: one message across the row, and a way out.
|
|
505
|
+
The cell stays a table cell so its colspan holds; the flex box is inside
|
|
506
|
+
it (a flex `td` stops spanning and the tint shrank to the text). Rows
|
|
507
|
+
that continue a failed run are the same band without the message. */
|
|
508
|
+
.sv-grid-placeholder-failed .sv-grid-placeholder-cell {
|
|
509
|
+
background: color-mix(in srgb, var(--sg-danger, #dc2626) 6%, transparent);
|
|
510
|
+
}
|
|
511
|
+
.sv-grid-placeholder-failed-body {
|
|
512
|
+
display: flex;
|
|
513
|
+
align-items: center;
|
|
514
|
+
gap: 10px;
|
|
515
|
+
min-height: 100%;
|
|
516
|
+
padding-left: var(--sg-cell-px, 7px);
|
|
517
|
+
}
|
|
518
|
+
.sv-grid-placeholder-failed-cont .sv-grid-placeholder-cell {
|
|
519
|
+
border-top-color: transparent;
|
|
520
|
+
}
|
|
521
|
+
.sv-grid-placeholder-error {
|
|
522
|
+
color: var(--sg-danger, #dc2626);
|
|
523
|
+
font-size: 13px;
|
|
524
|
+
}
|
|
525
|
+
.sv-grid-placeholder-retry {
|
|
526
|
+
background: none;
|
|
527
|
+
border: 1px solid var(--sg-border, #e2e8f0);
|
|
528
|
+
border-radius: var(--sg-radius, 6px);
|
|
529
|
+
padding: 2px 10px;
|
|
530
|
+
font: inherit;
|
|
531
|
+
font-size: 12px;
|
|
532
|
+
color: var(--sg-fg, #0f172a);
|
|
533
|
+
cursor: pointer;
|
|
534
|
+
}
|
|
535
|
+
.sv-grid-placeholder-retry:hover {
|
|
536
|
+
background: color-mix(in srgb, var(--sg-fg, #0f172a) 6%, transparent);
|
|
537
|
+
}
|
|
538
|
+
|
|
466
539
|
/* Pinned columns read as "frozen" via three layered cues:
|
|
467
540
|
1. A distinct fill (slightly stronger than the header) so the
|
|
468
541
|
pinned strip is visible even before you scroll. Themes with a
|
package/dist/SvGrid.svelte
CHANGED
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
import { getSchedulerView } from "./scheduler-view.svelte";
|
|
55
55
|
import { getSelectionBarView } from "./selection-bar-view.svelte";
|
|
56
56
|
import { getBoardView } from "./board-view.svelte";
|
|
57
|
+
import { getGanttView } from "./gantt-view.svelte";
|
|
57
58
|
import { getChartView } from "./chart-view.svelte";
|
|
58
59
|
let props: Props<TFeatures, TData> = $props();
|
|
59
60
|
// Per-instance base for every DOM id the grid mints. `$props.id()` is stable
|
|
@@ -78,6 +79,9 @@
|
|
|
78
79
|
const boardConfig = $derived(opt.board);
|
|
79
80
|
// Scheduler / calendar mode: same "view of the grid" seam as the board.
|
|
80
81
|
const schedulerConfig = $derived(opt.scheduler);
|
|
82
|
+
// Gantt mode: a task table beside a time chart. Same seam again; the renderer
|
|
83
|
+
// ships in @svgrid/enterprise (enableGanttView).
|
|
84
|
+
const ganttConfig = $derived(opt.gantt);
|
|
81
85
|
// Chart mode: another "view of the grid". Unlike board/scheduler, the renderer
|
|
82
86
|
// is free - a built-in default (SvGridChartView, lazy-loaded below) wraps the
|
|
83
87
|
// standalone SvChart, overridable via the `chart-view` seam (getChartView).
|
|
@@ -408,6 +412,11 @@
|
|
|
408
412
|
ctrl.columnVirtualizationEnabled,
|
|
409
413
|
);
|
|
410
414
|
const virtualRows = $derived(ctrl.virtualRows);
|
|
415
|
+
// The first body row on screen: where a failed run that started above
|
|
416
|
+
// the window repeats its message.
|
|
417
|
+
const firstRenderedRowIndex = $derived(
|
|
418
|
+
rowVirtualizationEnabled ? (virtualRows[0]?.index ?? 0) : 0,
|
|
419
|
+
);
|
|
411
420
|
const frozenRowCount = $derived(ctrl.frozenRowCount);
|
|
412
421
|
const frozenRowList = $derived(ctrl.frozenRowList);
|
|
413
422
|
// Merged cells. A merge is drawn once per band (the frozen rows, the
|
|
@@ -1050,6 +1059,48 @@
|
|
|
1050
1059
|
{/if}
|
|
1051
1060
|
</div>
|
|
1052
1061
|
</div>
|
|
1062
|
+
{:else if ganttConfig}
|
|
1063
|
+
<div
|
|
1064
|
+
class="sv-grid-root sv-grid-gantt-root"
|
|
1065
|
+
class:sv-grid-root-fill={opt.containerHeight === "100%"}
|
|
1066
|
+
style={`height: ${
|
|
1067
|
+
typeof opt.containerHeight === "string"
|
|
1068
|
+
? opt.containerHeight
|
|
1069
|
+
: `${opt.containerHeight ?? 520}px`
|
|
1070
|
+
}; display: flex; flex-direction: column;`}
|
|
1071
|
+
>
|
|
1072
|
+
{#if ganttConfig.searchable !== false}
|
|
1073
|
+
<label class="sv-grid-board-search">
|
|
1074
|
+
{@render icon("search")}
|
|
1075
|
+
<input
|
|
1076
|
+
type="search"
|
|
1077
|
+
placeholder={ganttConfig.searchPlaceholder ??
|
|
1078
|
+
messages.ganttSearchPlaceholder}
|
|
1079
|
+
bind:value={ctrl.globalFilter}
|
|
1080
|
+
aria-label="Search tasks"
|
|
1081
|
+
/>
|
|
1082
|
+
</label>
|
|
1083
|
+
{/if}
|
|
1084
|
+
<div style="flex: 1 1 auto; min-height: 0;">
|
|
1085
|
+
{#if getGanttView()}
|
|
1086
|
+
{@const GanttView = getGanttView()}
|
|
1087
|
+
<GanttView
|
|
1088
|
+
data={boardData}
|
|
1089
|
+
columns={opt.columns}
|
|
1090
|
+
gantt={ganttConfig}
|
|
1091
|
+
getRowId={opt.getRowId}
|
|
1092
|
+
/>
|
|
1093
|
+
{:else}
|
|
1094
|
+
<!-- The Gantt renderer ships in @svgrid/enterprise. Call
|
|
1095
|
+
`enableGanttView()` (or `installEnterprise(api)`) to register it. -->
|
|
1096
|
+
<div class="sv-grid-scheduler-upsell sv-grid-gantt-upsell" role="note">
|
|
1097
|
+
<strong>{messages.ganttUpsellTitle}</strong>
|
|
1098
|
+
<p>{messages.ganttUpsellBody}</p>
|
|
1099
|
+
{@render enterpriseLicenseNote()}
|
|
1100
|
+
</div>
|
|
1101
|
+
{/if}
|
|
1102
|
+
</div>
|
|
1103
|
+
</div>
|
|
1053
1104
|
{:else if chartViewConfig}
|
|
1054
1105
|
<div
|
|
1055
1106
|
class="sv-grid-root sv-grid-chart-root"
|
|
@@ -1461,6 +1512,73 @@
|
|
|
1461
1512
|
</tr>
|
|
1462
1513
|
{/snippet}
|
|
1463
1514
|
|
|
1515
|
+
<!-- A row whose data has not arrived.
|
|
1516
|
+
|
|
1517
|
+
"loading" keeps the real columns and draws a shimmer bar in each,
|
|
1518
|
+
so the table keeps its shape while blocks stream in - a full-width
|
|
1519
|
+
bar would make the whole grid jump every time one landed. "failed"
|
|
1520
|
+
is the opposite case: there is nothing per-column to say, just one
|
|
1521
|
+
message and a way to try again, so it spans the row like a detail
|
|
1522
|
+
cell does. Neither is selectable or editable; there is nothing
|
|
1523
|
+
there to act on.
|
|
1524
|
+
|
|
1525
|
+
A failed block is a run of failed rows, one per row it claimed,
|
|
1526
|
+
and one message serves the run: the first failed row on screen
|
|
1527
|
+
carries it (the run's first row, or the top of the window when
|
|
1528
|
+
the run started above it) and the rows under it are a plain
|
|
1529
|
+
tinted band. A hundred "Could not load" lines for one block
|
|
1530
|
+
was a wall of red. -->
|
|
1531
|
+
{#snippet placeholderRowMarkup(
|
|
1532
|
+
phRow: Row<TData>,
|
|
1533
|
+
phRowIndex: number,
|
|
1534
|
+
phState: "loading" | "failed",
|
|
1535
|
+
phStyle: string,
|
|
1536
|
+
phContinues: boolean,
|
|
1537
|
+
)}
|
|
1538
|
+
<tr
|
|
1539
|
+
class="sv-grid-row sv-grid-placeholder-row"
|
|
1540
|
+
class:sv-grid-placeholder-failed={phState === "failed"}
|
|
1541
|
+
class:sv-grid-placeholder-failed-cont={phContinues}
|
|
1542
|
+
aria-busy={phState === "loading" ? "true" : undefined}
|
|
1543
|
+
{...getGridRowA11yProps(phRowIndex + 1)}
|
|
1544
|
+
style={phStyle}
|
|
1545
|
+
>
|
|
1546
|
+
{#if phState === "failed"}
|
|
1547
|
+
<td
|
|
1548
|
+
class="sv-grid-cell sv-grid-placeholder-cell"
|
|
1549
|
+
colspan={allColumns.length +
|
|
1550
|
+
(showRowNumbersEffective ? 1 : 0) +
|
|
1551
|
+
(showRowSelectionEffective ? 1 : 0)}
|
|
1552
|
+
>
|
|
1553
|
+
{#if !phContinues}
|
|
1554
|
+
<div class="sv-grid-placeholder-failed-body">
|
|
1555
|
+
<span class="sv-grid-placeholder-error">{messages.rowLoadFailed}</span>
|
|
1556
|
+
{#if opt.onRetryRow}
|
|
1557
|
+
<button
|
|
1558
|
+
type="button"
|
|
1559
|
+
class="sv-grid-placeholder-retry"
|
|
1560
|
+
onclick={() => ctrl.retryRow(phRow)}>{messages.rowLoadRetry}</button
|
|
1561
|
+
>
|
|
1562
|
+
{/if}
|
|
1563
|
+
</div>
|
|
1564
|
+
{/if}
|
|
1565
|
+
</td>
|
|
1566
|
+
{:else}
|
|
1567
|
+
{#if showRowNumbersEffective}
|
|
1568
|
+
<td class="sv-grid-cell sv-grid-rownum-cell"></td>
|
|
1569
|
+
{/if}
|
|
1570
|
+
{#if showRowSelectionEffective}
|
|
1571
|
+
<td class="sv-grid-cell sv-grid-select-cell"></td>
|
|
1572
|
+
{/if}
|
|
1573
|
+
{#each allColumns as col (col.id)}
|
|
1574
|
+
<td class="sv-grid-cell sv-grid-placeholder-cell">
|
|
1575
|
+
<span class="sv-grid-placeholder-skeleton" aria-hidden="true"></span>
|
|
1576
|
+
</td>
|
|
1577
|
+
{/each}
|
|
1578
|
+
{/if}
|
|
1579
|
+
</tr>
|
|
1580
|
+
{/snippet}
|
|
1581
|
+
|
|
1464
1582
|
<!-- A single pinned row (top or bottom). Read-only by design: no
|
|
1465
1583
|
inline editing, no row-selection checkbox, no fill handle.
|
|
1466
1584
|
Position-sticky CSS keeps it anchored to the top of the body or
|
|
@@ -1474,7 +1592,14 @@
|
|
|
1474
1592
|
the virtualizer's measured size, or the declared one.
|
|
1475
1593
|
-->
|
|
1476
1594
|
{#snippet bodyRow(row: Row<TData>, rowIndex: number, rowStyle: string)}
|
|
1477
|
-
{
|
|
1595
|
+
{@const phState = ctrl.placeholderStateOf(row)}
|
|
1596
|
+
{#if phState}
|
|
1597
|
+
{@const phContinues =
|
|
1598
|
+
phState === "failed" &&
|
|
1599
|
+
rowIndex > firstRenderedRowIndex &&
|
|
1600
|
+
ctrl.placeholderStateOf(allRows[rowIndex - 1]!) === "failed"}
|
|
1601
|
+
{@render placeholderRowMarkup(row, rowIndex, phState, rowStyle, phContinues)}
|
|
1602
|
+
{:else if opt.isDetailRow?.(row.original as TData, rowIndex)}
|
|
1478
1603
|
{@render detailRowMarkup(row, rowIndex)}
|
|
1479
1604
|
{:else if isGroupRow(row) && !groupColumnMode}
|
|
1480
1605
|
<tr
|
|
@@ -1827,7 +1952,7 @@
|
|
|
1827
1952
|
style={chartDockReserveStyle}
|
|
1828
1953
|
data-move-grab={ctrl.moveGrabHover || ctrl.moveDrag ? "true" : undefined}
|
|
1829
1954
|
data-selbar={
|
|
1830
|
-
ctrl.
|
|
1955
|
+
ctrl.selectionBarVisible
|
|
1831
1956
|
? (getSelectionBarView() ? ctrl.selectionBarPosition : `upsell-${ctrl.selectionBarPosition}`)
|
|
1832
1957
|
: undefined
|
|
1833
1958
|
}
|
|
@@ -3004,7 +3129,7 @@
|
|
|
3004
3129
|
`absolute` against it, so moving it outside would anchor it to the
|
|
3005
3130
|
page. The renderer is Enterprise and arrives through the
|
|
3006
3131
|
selection-bar-view seam; without it, a short note in its place. -->
|
|
3007
|
-
{#if ctrl.
|
|
3132
|
+
{#if ctrl.selectionBarVisible}
|
|
3008
3133
|
{#if getSelectionBarView()}
|
|
3009
3134
|
{@const SelectionBarView = getSelectionBarView()}
|
|
3010
3135
|
<SelectionBarView {ctrl} />
|