bo-grid 0.25.0 → 1.0.0
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 +98 -17
- package/dist/{bo-grid.FilterMenu-BHI6rILc.js → bo-grid.FilterMenu-BBxlxrnZ.js} +1 -1
- package/dist/{bo-grid.ToolPanel-C3u-4YKc.js → bo-grid.ToolPanel-CCBi982x.js} +1 -1
- package/dist/bo-grid.element-BZGnfKB_.js +6898 -0
- package/dist/bo-grid.element.d.ts +17 -0
- package/dist/bo-grid.element.js +3 -2
- package/dist/grid/Cell.svelte +61 -9
- package/dist/grid/Grid.svelte +213 -19
- package/dist/grid/Grid.svelte.d.ts +21 -1
- package/dist/grid/Pager.svelte +45 -0
- package/dist/grid/Pager.svelte.d.ts +3 -0
- package/dist/grid/column.d.ts +36 -3
- package/dist/grid/column.js +17 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +0 -1
- package/package.json +3 -2
- package/dist/bo-grid.element-DPnHUXMa.js +0 -6623
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Type declarations for the `bo-grid/element` entry (the <bo-grid> custom element).
|
|
2
|
+
import type { GridProps } from './index';
|
|
3
|
+
|
|
4
|
+
/** The <bo-grid> `config` object — every <Grid> prop. */
|
|
5
|
+
export type BoGridConfig = GridProps;
|
|
6
|
+
|
|
7
|
+
/** A <bo-grid> DOM element with a typed `config` property. */
|
|
8
|
+
export interface BoGridElement extends HTMLElement {
|
|
9
|
+
config?: BoGridConfig;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Create a <bo-grid> element with its `config` already set (safe
|
|
13
|
+
* create-before-attach pattern for React/Vue/vanilla). */
|
|
14
|
+
export function createBoGrid(config?: BoGridConfig): BoGridElement;
|
|
15
|
+
|
|
16
|
+
declare const BoGrid: unknown;
|
|
17
|
+
export default BoGrid;
|
package/dist/bo-grid.element.js
CHANGED
package/dist/grid/Cell.svelte
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import type { ColumnDef, GridRow } from './column';
|
|
4
4
|
import {
|
|
5
5
|
formatCell,
|
|
6
|
+
tooltipText,
|
|
6
7
|
colStyle,
|
|
7
8
|
candlesOf,
|
|
8
9
|
isNumeric,
|
|
@@ -146,12 +147,22 @@
|
|
|
146
147
|
const extraClass = $derived(
|
|
147
148
|
typeof col.cellClass === 'function' ? (col.cellClass(value, row) ?? '') : (col.cellClass ?? ''),
|
|
148
149
|
);
|
|
149
|
-
//
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
)
|
|
150
|
+
// Styled floating tooltip text (opt-in via column `tooltip`); the grid root
|
|
151
|
+
// renders the actual tooltip from this cell's `data-bo-tip` attribute.
|
|
152
|
+
const tip = $derived(tooltipText(col, value, row));
|
|
153
|
+
|
|
154
|
+
// JS cell renderer (framework-agnostic alt to the `cell` snippet). Returns an
|
|
155
|
+
// HTML string ({@html}) or a DOM Node (mounted via the action below).
|
|
156
|
+
const rendered = $derived(col.render ? col.render({ value, row, column: col }) : undefined);
|
|
157
|
+
// Mount/replace a Node return value; updates when the derived node changes.
|
|
158
|
+
function renderNode(host: HTMLElement, node: Node | string | null | undefined) {
|
|
159
|
+
const set = (n: Node | string | null | undefined) => {
|
|
160
|
+
if (n instanceof Node) host.replaceChildren(n);
|
|
161
|
+
else host.textContent = n == null ? '' : String(n);
|
|
162
|
+
};
|
|
163
|
+
set(node);
|
|
164
|
+
return { update: set };
|
|
165
|
+
}
|
|
155
166
|
|
|
156
167
|
// ---- Conditional formatting (v0.10): data bar + icon set ----
|
|
157
168
|
// Geometry/threshold logic lives in column.ts (pure, unit-tested); here we map
|
|
@@ -193,6 +204,7 @@
|
|
|
193
204
|
<span
|
|
194
205
|
class="c {kind} {extraClass}"
|
|
195
206
|
class:dim={col.type === 'volume'}
|
|
207
|
+
class:wrap={col.wrap}
|
|
196
208
|
class:pos={col.type === 'percent' && Number(value) >= 0}
|
|
197
209
|
class:neg={col.type === 'percent' && Number(value) < 0}
|
|
198
210
|
class:sel={selected}
|
|
@@ -202,7 +214,7 @@
|
|
|
202
214
|
role="gridcell"
|
|
203
215
|
tabindex="-1"
|
|
204
216
|
id={cellId}
|
|
205
|
-
|
|
217
|
+
data-bo-tip={tip}
|
|
206
218
|
aria-colindex={colIndex}
|
|
207
219
|
aria-selected={selected}
|
|
208
220
|
onpointerdown={(e) => onCellDown?.(r, c, e)}
|
|
@@ -269,6 +281,12 @@
|
|
|
269
281
|
onclick={(e) => e.stopPropagation()}
|
|
270
282
|
ondblclick={(e) => e.stopPropagation()}
|
|
271
283
|
/>
|
|
284
|
+
{:else if col.render}
|
|
285
|
+
{#if typeof rendered === 'string'}
|
|
286
|
+
<span class="bo-render">{@html rendered}</span>
|
|
287
|
+
{:else}
|
|
288
|
+
<span class="bo-render" use:renderNode={rendered}></span>
|
|
289
|
+
{/if}
|
|
272
290
|
{:else if col.type === 'custom'}
|
|
273
291
|
{#if cellSnippet}{@render cellSnippet({ row, column: col, value })}{:else}{value ?? ''}{/if}
|
|
274
292
|
{:else if col.type === 'sparkline'}
|
|
@@ -324,10 +342,10 @@
|
|
|
324
342
|
{/key}
|
|
325
343
|
{:else if col.flash}
|
|
326
344
|
{#key row.flashSeq}
|
|
327
|
-
<span class="flash {row.flashDir}">{formatCell(col, value, row)}</span>
|
|
345
|
+
<span class="flash bo-cell-text {row.flashDir}">{formatCell(col, value, row)}</span>
|
|
328
346
|
{/key}
|
|
329
347
|
{:else}
|
|
330
|
-
{formatCell(col, value, row)}
|
|
348
|
+
<span class="bo-cell-text">{formatCell(col, value, row)}</span>
|
|
331
349
|
{/if}
|
|
332
350
|
{#if fillCorner}
|
|
333
351
|
<span
|
|
@@ -356,6 +374,35 @@
|
|
|
356
374
|
white-space: nowrap;
|
|
357
375
|
text-overflow: ellipsis;
|
|
358
376
|
}
|
|
377
|
+
/* Truncating text node inside the flex cell: a bare text child of a flex
|
|
378
|
+
container won't honour text-overflow, so plain values render through this
|
|
379
|
+
span to get a real ellipsis when they overflow. */
|
|
380
|
+
.bo-cell-text {
|
|
381
|
+
min-width: 0;
|
|
382
|
+
overflow: hidden;
|
|
383
|
+
white-space: nowrap;
|
|
384
|
+
text-overflow: ellipsis;
|
|
385
|
+
}
|
|
386
|
+
/* Host for a JS `render` return (string HTML or a DOM node). Lays out inline
|
|
387
|
+
and truncates like a normal cell; consumer markup controls the rest. */
|
|
388
|
+
.bo-render {
|
|
389
|
+
display: inline-flex;
|
|
390
|
+
align-items: center;
|
|
391
|
+
gap: 5px;
|
|
392
|
+
min-width: 0;
|
|
393
|
+
overflow: hidden;
|
|
394
|
+
text-overflow: ellipsis;
|
|
395
|
+
}
|
|
396
|
+
/* Opt-in multi-line wrap (col.wrap) — pair with a taller rowHeight. */
|
|
397
|
+
.c.wrap {
|
|
398
|
+
white-space: normal;
|
|
399
|
+
}
|
|
400
|
+
.c.wrap .bo-cell-text,
|
|
401
|
+
.c.wrap strong {
|
|
402
|
+
white-space: normal;
|
|
403
|
+
overflow: visible;
|
|
404
|
+
text-overflow: clip;
|
|
405
|
+
}
|
|
359
406
|
.num {
|
|
360
407
|
justify-content: flex-end;
|
|
361
408
|
font-family: var(--bo-mono);
|
|
@@ -500,10 +547,15 @@
|
|
|
500
547
|
line-height: 1;
|
|
501
548
|
}
|
|
502
549
|
.text strong {
|
|
550
|
+
min-width: 0;
|
|
551
|
+
overflow: hidden;
|
|
552
|
+
white-space: nowrap;
|
|
553
|
+
text-overflow: ellipsis;
|
|
503
554
|
font-family: var(--bo-mono);
|
|
504
555
|
font-weight: 600;
|
|
505
556
|
}
|
|
506
557
|
.text em {
|
|
558
|
+
flex: none;
|
|
507
559
|
font-style: normal;
|
|
508
560
|
font-size: 10px;
|
|
509
561
|
color: var(--bo-text-dim);
|
package/dist/grid/Grid.svelte
CHANGED
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
import RowMenu from './RowMenu.svelte';
|
|
37
37
|
|
|
38
38
|
let {
|
|
39
|
-
rows,
|
|
40
|
-
columns,
|
|
39
|
+
rows = [],
|
|
40
|
+
columns = [],
|
|
41
41
|
height,
|
|
42
42
|
filter = '',
|
|
43
43
|
groupBy = [],
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
rowHeight,
|
|
49
49
|
theme,
|
|
50
50
|
resizable = true,
|
|
51
|
+
cellSelection = true,
|
|
51
52
|
rowSelection = false,
|
|
52
53
|
onRowSelectionChange,
|
|
53
54
|
hiddenColumns = [],
|
|
@@ -57,6 +58,7 @@
|
|
|
57
58
|
virtualizeColumns = false,
|
|
58
59
|
rowClass,
|
|
59
60
|
getRowId = (r: GridRow) => r.id,
|
|
61
|
+
selectedRowId = null,
|
|
60
62
|
onRowClick,
|
|
61
63
|
sort,
|
|
62
64
|
onSortChange,
|
|
@@ -81,6 +83,8 @@
|
|
|
81
83
|
loadGroup,
|
|
82
84
|
onRowReorder,
|
|
83
85
|
pageSize = 0,
|
|
86
|
+
pageSizeOptions,
|
|
87
|
+
onPageSizeChange,
|
|
84
88
|
page,
|
|
85
89
|
onPageChange,
|
|
86
90
|
onColumnReorder,
|
|
@@ -90,7 +94,12 @@
|
|
|
90
94
|
}: {
|
|
91
95
|
rows: GridRow[];
|
|
92
96
|
columns: ColumnDef[];
|
|
93
|
-
height
|
|
97
|
+
/** Viewport height. A **number** is the scroll viewport's pixel height (the
|
|
98
|
+
grid's total height is that plus header/toolbar/footer chrome). A **CSS
|
|
99
|
+
string** (e.g. `'100%'`, `'80vh'`, `'480px'`) sizes the whole grid element
|
|
100
|
+
and the viewport auto-fits the space left after the chrome — give the grid
|
|
101
|
+
a sized parent (or use `'100%'` inside a flex/grid cell). */
|
|
102
|
+
height: number | string;
|
|
94
103
|
/** Row height in px (uniform), or a function for variable heights
|
|
95
104
|
(in-memory mode only). Default 36. */
|
|
96
105
|
rowHeight?: number | ((row: GridRow, index: number) => number);
|
|
@@ -99,6 +108,11 @@
|
|
|
99
108
|
/** Allow drag-to-resize column widths. Default true; opt out per column
|
|
100
109
|
with `resizable: false`. */
|
|
101
110
|
resizable?: boolean;
|
|
111
|
+
/** Highlight cells/ranges on click-drag (the blue selection fill + focus
|
|
112
|
+
ring + fill handle). Set false for a read-only/display grid where clicks
|
|
113
|
+
should pass straight through to `onRowClick`/`onCellClick` without a
|
|
114
|
+
selection highlight. Default true. */
|
|
115
|
+
cellSelection?: boolean;
|
|
102
116
|
/** Show a leading checkbox column for whole-row selection (keyed by row id,
|
|
103
117
|
stable across sort/filter). Default false. */
|
|
104
118
|
rowSelection?: boolean;
|
|
@@ -128,6 +142,10 @@
|
|
|
128
142
|
/** Identity key for row selection. Defaults to `row.id`; override for
|
|
129
143
|
string/UUID/composite keys. */
|
|
130
144
|
getRowId?: (row: GridRow) => string | number;
|
|
145
|
+
/** Controlled "active row" highlight (keyed by `getRowId`) — for
|
|
146
|
+
master-detail / list-detail where one row is selected. Independent of the
|
|
147
|
+
checkbox `rowSelection` and the cell range selection. `null` = none. */
|
|
148
|
+
selectedRowId?: string | number | null;
|
|
131
149
|
/** Called when a data row is activated by click or Enter (open a detail
|
|
132
150
|
view, navigate, …). Edit-input and checkbox clicks are excluded. */
|
|
133
151
|
onRowClick?: (row: GridRow, event: MouseEvent | KeyboardEvent) => void;
|
|
@@ -210,6 +228,12 @@
|
|
|
210
228
|
/** Rows per page. When > 0 (in-memory mode), shows a pager instead of one
|
|
211
229
|
long scroll; rows still virtualize within a page. Default 0 (off). */
|
|
212
230
|
pageSize?: number;
|
|
231
|
+
/** Page-size choices for a dropdown in the pager (e.g. `[25, 50, 100]`).
|
|
232
|
+
Picking one re-pages from page 0. Uncontrolled unless you also drive
|
|
233
|
+
`pageSize` and update it from `onPageSizeChange`. */
|
|
234
|
+
pageSizeOptions?: number[];
|
|
235
|
+
/** Called with the new page size when the pager's size dropdown changes. */
|
|
236
|
+
onPageSizeChange?: (size: number) => void;
|
|
213
237
|
/** Controlled current page (0-based). Omit for uncontrolled paging. */
|
|
214
238
|
page?: number;
|
|
215
239
|
/** Called with the new page index when the pager is used. */
|
|
@@ -239,6 +263,11 @@
|
|
|
239
263
|
const gid = `bo-grid-${uid++}`;
|
|
240
264
|
|
|
241
265
|
let scrollTop = $state(0);
|
|
266
|
+
// Styled floating tooltip (opt-in via column `tooltip`): a single fixed-
|
|
267
|
+
// position bubble driven by the hovered cell's `data-bo-tip`. Fixed escapes
|
|
268
|
+
// the cell/viewport overflow:hidden without a portal.
|
|
269
|
+
const hasTooltips = $derived(columns.some((c) => !!c.tooltip || !!c.headerTooltip));
|
|
270
|
+
let tip = $state<{ x: number; y: number; text: string; below: boolean } | null>(null);
|
|
242
271
|
// Sort order, primary first. Empty = unsorted. Multiple keys via Shift-click.
|
|
243
272
|
// Controlled by the `sort` prop when provided, else internal state.
|
|
244
273
|
let internalSorts = $state<SortState[]>([]);
|
|
@@ -512,6 +541,12 @@
|
|
|
512
541
|
const COL_OVERSCAN = 320; // px
|
|
513
542
|
let scrollLeft = $state(0);
|
|
514
543
|
let viewW = $state(0);
|
|
544
|
+
// Viewport height in px for row virtualization. A numeric `height` is that
|
|
545
|
+
// value directly; a CSS-string `height` (auto-fit) is the measured viewport
|
|
546
|
+
// height (clientHeight), with a sane fallback before the first measure.
|
|
547
|
+
const heightIsPx = $derived(typeof height === 'number');
|
|
548
|
+
let viewH = $state(0);
|
|
549
|
+
const viewPx = $derived(heightIsPx ? (height as number) : viewH || 400);
|
|
515
550
|
type ColItem = { kind: 'cell'; ci: number; key: string } | { kind: 'spacer'; w: number; key: string };
|
|
516
551
|
const colItems = $derived.by<ColItem[]>(() => {
|
|
517
552
|
const n = cols.length;
|
|
@@ -902,15 +937,24 @@
|
|
|
902
937
|
});
|
|
903
938
|
|
|
904
939
|
// Pagination (in-memory only): slice the view into pages; rows still
|
|
905
|
-
// virtualize within a page. Off when
|
|
906
|
-
|
|
940
|
+
// virtualize within a page. Off when the page size is <= 0.
|
|
941
|
+
// Effective page size: a runtime pick from `pageSizeOptions` (uncontrolled),
|
|
942
|
+
// else the `pageSize` prop.
|
|
943
|
+
let internalPageSize = $state<number | null>(null);
|
|
944
|
+
const effPageSize = $derived(internalPageSize ?? pageSize);
|
|
945
|
+
function setPageSize(size: number): void {
|
|
946
|
+
internalPageSize = size;
|
|
947
|
+
onPageSizeChange?.(size);
|
|
948
|
+
setPage(0); // a new page size invalidates the current page index
|
|
949
|
+
}
|
|
950
|
+
const paged = $derived(effPageSize > 0 && !source);
|
|
907
951
|
let internalPage = $state(0);
|
|
908
952
|
const currentPage = $derived(page ?? internalPage); // controlled by `page` prop, else internal
|
|
909
953
|
function setPage(p: number): void {
|
|
910
954
|
if (page === undefined) internalPage = p; // uncontrolled: own the state
|
|
911
955
|
onPageChange?.(p); // always notify
|
|
912
956
|
}
|
|
913
|
-
const pageCount = $derived(paged ? Math.max(1, Math.ceil(view.length /
|
|
957
|
+
const pageCount = $derived(paged ? Math.max(1, Math.ceil(view.length / effPageSize)) : 1);
|
|
914
958
|
// Keep the page in range when the view shrinks (filter/sort changes).
|
|
915
959
|
$effect(() => {
|
|
916
960
|
const max = pageCount - 1;
|
|
@@ -919,7 +963,7 @@
|
|
|
919
963
|
});
|
|
920
964
|
});
|
|
921
965
|
const pageRows = $derived(
|
|
922
|
-
paged ? view.slice(currentPage *
|
|
966
|
+
paged ? view.slice(currentPage * effPageSize, currentPage * effPageSize + effPageSize) : view,
|
|
923
967
|
);
|
|
924
968
|
|
|
925
969
|
const treeData = $derived(!!(getChildren || loadChildren) && !source);
|
|
@@ -929,7 +973,7 @@
|
|
|
929
973
|
// Drag-to-reorder rows (flat, unsorted, in-memory only). The handle lives in
|
|
930
974
|
// the first cell; the dragged/drop indices are tracked in component state.
|
|
931
975
|
const reorderable = $derived(
|
|
932
|
-
!!onRowReorder && !source && !treeData && groupBy.length === 0 &&
|
|
976
|
+
!!onRowReorder && !source && !treeData && groupBy.length === 0 && effPageSize <= 0,
|
|
933
977
|
);
|
|
934
978
|
let dragRowVr = $state(-1);
|
|
935
979
|
let dropRowVr = $state(-1);
|
|
@@ -1059,12 +1103,12 @@
|
|
|
1059
1103
|
const rowWidthStyle = $derived(
|
|
1060
1104
|
hScroll ? `width:${layout.totalWidth + leadPx}px;right:auto;` : '',
|
|
1061
1105
|
);
|
|
1062
|
-
const visibleCount = $derived(Math.ceil(
|
|
1106
|
+
const visibleCount = $derived(Math.ceil(viewPx / baseH) + OVERSCAN * 2);
|
|
1063
1107
|
const start = $derived(Math.max(0, hm.indexAt(scrollTop) - OVERSCAN));
|
|
1064
1108
|
const renderEnd = $derived(
|
|
1065
1109
|
source
|
|
1066
1110
|
? (controller && controller.total > 0 ? Math.min(start + visibleCount, controller.total) : start + visibleCount)
|
|
1067
|
-
: Math.min(flat.length, hm.indexAt(scrollTop +
|
|
1111
|
+
: Math.min(flat.length, hm.indexAt(scrollTop + viewPx) + OVERSCAN + 1),
|
|
1068
1112
|
);
|
|
1069
1113
|
|
|
1070
1114
|
type RenderItem =
|
|
@@ -1139,6 +1183,27 @@
|
|
|
1139
1183
|
if (hScroll && headEl) headEl.scrollLeft = el.scrollLeft; // keep header in sync
|
|
1140
1184
|
if (hScroll && filterRowEl) filterRowEl.scrollLeft = el.scrollLeft; // and the filter row
|
|
1141
1185
|
if (hScroll && groupHeadEl) groupHeadEl.scrollLeft = el.scrollLeft; // and group headers
|
|
1186
|
+
if (tip) tip = null; // a stale fixed-position bubble would float away on scroll
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
// Tooltip hover delegation (only wired when a column opts in). Matches both
|
|
1190
|
+
// data cells (`.c[data-bo-tip]`) and column headers (`.h[data-bo-tip]`).
|
|
1191
|
+
function onTipOver(e: PointerEvent) {
|
|
1192
|
+
const cell = (e.target as HTMLElement | null)?.closest?.('[data-bo-tip]') as HTMLElement | null;
|
|
1193
|
+
const text = cell?.dataset.boTip;
|
|
1194
|
+
if (!cell || !text) {
|
|
1195
|
+
if (tip) tip = null;
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
const rect = cell.getBoundingClientRect();
|
|
1199
|
+
const below = rect.top < 72; // not enough room above near the header — flip down
|
|
1200
|
+
const x = Math.min(Math.max(rect.left + rect.width / 2, 96), window.innerWidth - 96);
|
|
1201
|
+
tip = { text, x, y: below ? rect.bottom + 7 : rect.top - 7, below };
|
|
1202
|
+
}
|
|
1203
|
+
function onTipOut(e: PointerEvent) {
|
|
1204
|
+
const to = e.relatedTarget as HTMLElement | null;
|
|
1205
|
+
if (to?.closest?.('[data-bo-tip]')) return; // moved within the same tipped cell/header
|
|
1206
|
+
if (tip) tip = null;
|
|
1142
1207
|
}
|
|
1143
1208
|
|
|
1144
1209
|
function toggleGroup(path: string) {
|
|
@@ -1150,6 +1215,8 @@
|
|
|
1150
1215
|
|
|
1151
1216
|
function onCellDown(r: number, c: number, e: PointerEvent) {
|
|
1152
1217
|
if (e.button !== 0) return;
|
|
1218
|
+
// Display grid: skip range selection but let onclick (row/cell click) fire.
|
|
1219
|
+
if (!cellSelection) return;
|
|
1153
1220
|
e.preventDefault();
|
|
1154
1221
|
gridEl?.focus();
|
|
1155
1222
|
if (e.shiftKey) sel.extendTo(r, c);
|
|
@@ -1354,7 +1421,7 @@
|
|
|
1354
1421
|
const top = hm.offsetOf(f.r);
|
|
1355
1422
|
const h = hm.heightOf(f.r);
|
|
1356
1423
|
if (top < viewportEl.scrollTop) viewportEl.scrollTop = top;
|
|
1357
|
-
else if (top + h > viewportEl.scrollTop +
|
|
1424
|
+
else if (top + h > viewportEl.scrollTop + viewPx) viewportEl.scrollTop = top + h - viewPx;
|
|
1358
1425
|
}
|
|
1359
1426
|
|
|
1360
1427
|
async function copySelection() {
|
|
@@ -1540,7 +1607,7 @@
|
|
|
1540
1607
|
const f = sel.focus;
|
|
1541
1608
|
if (f && (e.key === 'Home' || e.key === 'End' || e.key === 'PageUp' || e.key === 'PageDown')) {
|
|
1542
1609
|
e.preventDefault();
|
|
1543
|
-
const page = Math.max(1, Math.floor(
|
|
1610
|
+
const page = Math.max(1, Math.floor(viewPx / baseH) - 1);
|
|
1544
1611
|
if (e.key === 'Home') focusTo(mod ? 0 : f.r, 0, e.shiftKey);
|
|
1545
1612
|
else if (e.key === 'End') focusTo(mod ? maxR : f.r, maxC, e.shiftKey);
|
|
1546
1613
|
else if (e.key === 'PageDown') focusTo(f.r + page, f.c, e.shiftKey);
|
|
@@ -1594,7 +1661,7 @@
|
|
|
1594
1661
|
aria-colcount={cols.length + leadCols}
|
|
1595
1662
|
aria-multiselectable="true"
|
|
1596
1663
|
aria-activedescendant={activeId}
|
|
1597
|
-
style={themeStyle}
|
|
1664
|
+
style="{themeStyle}{heightIsPx ? '' : `;height:${height};min-height:0`}"
|
|
1598
1665
|
bind:this={gridEl}
|
|
1599
1666
|
onkeydown={onKeydown}
|
|
1600
1667
|
>
|
|
@@ -1623,7 +1690,17 @@
|
|
|
1623
1690
|
{/each}
|
|
1624
1691
|
</div>
|
|
1625
1692
|
{/if}
|
|
1626
|
-
|
|
1693
|
+
<!-- Tooltip hover delegation; header semantics live on the columnheader buttons. -->
|
|
1694
|
+
<!-- svelte-ignore a11y_interactive_supports_focus -->
|
|
1695
|
+
<div
|
|
1696
|
+
class="head"
|
|
1697
|
+
role="row"
|
|
1698
|
+
aria-rowindex={1}
|
|
1699
|
+
bind:this={headEl}
|
|
1700
|
+
style={hScroll ? 'overflow:hidden;' : ''}
|
|
1701
|
+
onpointerover={hasTooltips ? onTipOver : undefined}
|
|
1702
|
+
onpointerout={hasTooltips ? onTipOut : undefined}
|
|
1703
|
+
>
|
|
1627
1704
|
{#if expandable}
|
|
1628
1705
|
<span class="expandcell selhead" role="columnheader" aria-colindex={1} style={expandCellStyle(true)}></span>
|
|
1629
1706
|
{/if}
|
|
@@ -1652,6 +1729,7 @@
|
|
|
1652
1729
|
type="button"
|
|
1653
1730
|
role="columnheader"
|
|
1654
1731
|
aria-colindex={ci + 1 + leadCols}
|
|
1732
|
+
data-bo-tip={col.headerTooltip}
|
|
1655
1733
|
draggable="true"
|
|
1656
1734
|
aria-sort={isSortable(col) && sortInfo(col.key)
|
|
1657
1735
|
? sortInfo(col.key)?.dir === 'asc'
|
|
@@ -1682,6 +1760,9 @@
|
|
|
1682
1760
|
}}
|
|
1683
1761
|
>
|
|
1684
1762
|
<span class="label">{col.header}</span>
|
|
1763
|
+
{#if col.headerTooltip && col.headerInfo}
|
|
1764
|
+
<span class="hinfo" aria-hidden="true">i</span>
|
|
1765
|
+
{/if}
|
|
1685
1766
|
{#if isSortable(col) && sortInfo(col.key)}
|
|
1686
1767
|
{@const si = sortInfo(col.key)}
|
|
1687
1768
|
<span class="ind">
|
|
@@ -1762,12 +1843,18 @@
|
|
|
1762
1843
|
</div>
|
|
1763
1844
|
{/if}
|
|
1764
1845
|
|
|
1846
|
+
<!-- Tooltip hover delegation lives on the scroll container; the grid's
|
|
1847
|
+
interactive semantics are on the role="grid" root. -->
|
|
1848
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
1765
1849
|
<div
|
|
1766
1850
|
class="viewport"
|
|
1767
|
-
style="height
|
|
1851
|
+
style="{heightIsPx ? `height:${height}px` : 'flex:1 1 auto;min-height:0'};{hScroll ? 'overflow-x:auto;' : ''}"
|
|
1768
1852
|
bind:this={viewportEl}
|
|
1769
1853
|
bind:clientWidth={viewW}
|
|
1854
|
+
bind:clientHeight={viewH}
|
|
1770
1855
|
onscroll={onScroll}
|
|
1856
|
+
onpointerover={hasTooltips ? onTipOver : undefined}
|
|
1857
|
+
onpointerout={hasTooltips ? onTipOut : undefined}
|
|
1771
1858
|
>
|
|
1772
1859
|
{#if pinnedRows.length > 0}
|
|
1773
1860
|
<div class="pinned-top">
|
|
@@ -1839,7 +1926,7 @@
|
|
|
1839
1926
|
{:else}
|
|
1840
1927
|
<!-- Row activation is keyboard-accessible at the grid level: Enter on the focused cell fires onRowClick (focus is via aria-activedescendant). -->
|
|
1841
1928
|
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
|
|
1842
|
-
<div class="row {rowClass?.(item.row) ?? ''}" class:alt={item.vr % 2 === 1} class:rowsel={rowSelection && isRowSelected(getRowId(item.row))} class:clickable={!!onRowClick} class:droptarget={reorderable && dropRowVr === item.vr && dragRowVr !== item.vr} role="row" tabindex="-1" aria-rowindex={item.vr + 2} aria-selected={rowSelection ? isRowSelected(getRowId(item.row)) : undefined} aria-level={treeData ? (item.depth ?? 0) + 1 : undefined} aria-expanded={treeData && item.hasChildren ? isExpanded(getRowId(item.row)) : undefined} style="top:{hm.offsetOf(item.vr)}px;height:{expandable ? baseH : hm.heightOf(item.vr)}px;{rowWidthStyle}" onclick={(e) => onRowClick?.(item.row, e)} oncontextmenu={(e) => openRowMenu(item.row, e)} ondragover={reorderable ? (e) => { if (dragRowVr < 0) return; e.preventDefault(); dropRowVr = item.vr; } : undefined} ondrop={reorderable ? (e) => { e.preventDefault(); onRowDrop(); } : undefined}>
|
|
1929
|
+
<div class="row {rowClass?.(item.row) ?? ''}" class:alt={item.vr % 2 === 1} class:rowsel={rowSelection && isRowSelected(getRowId(item.row))} class:rowactive={selectedRowId != null && getRowId(item.row) === selectedRowId} class:clickable={!!onRowClick} class:droptarget={reorderable && dropRowVr === item.vr && dragRowVr !== item.vr} role="row" tabindex="-1" aria-rowindex={item.vr + 2} aria-selected={rowSelection ? isRowSelected(getRowId(item.row)) : undefined} aria-level={treeData ? (item.depth ?? 0) + 1 : undefined} aria-expanded={treeData && item.hasChildren ? isExpanded(getRowId(item.row)) : undefined} style="top:{hm.offsetOf(item.vr)}px;height:{expandable ? baseH : hm.heightOf(item.vr)}px;{rowWidthStyle}" onclick={(e) => onRowClick?.(item.row, e)} oncontextmenu={(e) => openRowMenu(item.row, e)} ondragover={reorderable ? (e) => { if (dragRowVr < 0) return; e.preventDefault(); dropRowVr = item.vr; } : undefined} ondrop={reorderable ? (e) => { e.preventDefault(); onRowDrop(); } : undefined}>
|
|
1843
1930
|
{#if expandable}
|
|
1844
1931
|
<span class="expandcell" style={expandCellStyle(false)}>
|
|
1845
1932
|
<button
|
|
@@ -1882,8 +1969,8 @@
|
|
|
1882
1969
|
colIndex={ci + 1 + leadCols}
|
|
1883
1970
|
cellId={`${gid}-r${item.vr}-c${ci}`}
|
|
1884
1971
|
cellSnippet={cell}
|
|
1885
|
-
selected={sel.contains(item.vr, ci)}
|
|
1886
|
-
focused={sel.isFocus(item.vr, ci)}
|
|
1972
|
+
selected={cellSelection && sel.contains(item.vr, ci)}
|
|
1973
|
+
focused={cellSelection && sel.isFocus(item.vr, ci)}
|
|
1887
1974
|
pinned={pinned && layout.info[ci].pinned}
|
|
1888
1975
|
pinSide={layout.info[ci].side ?? 'left'}
|
|
1889
1976
|
pinOffset={layout.info[ci].side === 'right' ? layout.info[ci].right : layout.info[ci].left + leadPx}
|
|
@@ -1945,7 +2032,15 @@
|
|
|
1945
2032
|
<AggregationBar result={agg} kinds={aggregations} />
|
|
1946
2033
|
|
|
1947
2034
|
{#if paged}
|
|
1948
|
-
<Pager
|
|
2035
|
+
<Pager
|
|
2036
|
+
page={currentPage}
|
|
2037
|
+
{pageCount}
|
|
2038
|
+
total={view.length}
|
|
2039
|
+
onGoto={goToPage}
|
|
2040
|
+
pageSize={effPageSize}
|
|
2041
|
+
{pageSizeOptions}
|
|
2042
|
+
onPageSize={setPageSize}
|
|
2043
|
+
/>
|
|
1949
2044
|
{/if}
|
|
1950
2045
|
|
|
1951
2046
|
{#if menu}
|
|
@@ -1979,6 +2074,18 @@
|
|
|
1979
2074
|
onClose={() => (panelXY = null)}
|
|
1980
2075
|
/>
|
|
1981
2076
|
{/if}
|
|
2077
|
+
|
|
2078
|
+
{#if tip}
|
|
2079
|
+
<div
|
|
2080
|
+
class="bo-tip"
|
|
2081
|
+
class:below={tip.below}
|
|
2082
|
+
role="tooltip"
|
|
2083
|
+
aria-hidden="true"
|
|
2084
|
+
style="left:{tip.x}px;top:{tip.y}px"
|
|
2085
|
+
>
|
|
2086
|
+
{tip.text}
|
|
2087
|
+
</div>
|
|
2088
|
+
{/if}
|
|
1982
2089
|
</div>
|
|
1983
2090
|
|
|
1984
2091
|
<style>
|
|
@@ -2022,6 +2129,65 @@
|
|
|
2022
2129
|
.grid:focus-visible {
|
|
2023
2130
|
border-color: var(--bo-sel-border);
|
|
2024
2131
|
}
|
|
2132
|
+
/* Styled floating tooltip (opt-in via column `tooltip`). Fixed-positioned so
|
|
2133
|
+
it escapes the cell/viewport overflow; centred on the cell, flips below
|
|
2134
|
+
near the header. */
|
|
2135
|
+
.bo-tip {
|
|
2136
|
+
position: fixed;
|
|
2137
|
+
z-index: 60;
|
|
2138
|
+
max-width: 280px;
|
|
2139
|
+
padding: 5px 9px;
|
|
2140
|
+
font-family: var(--bo-sans);
|
|
2141
|
+
font-size: 12px;
|
|
2142
|
+
line-height: 1.45;
|
|
2143
|
+
color: var(--bo-text);
|
|
2144
|
+
background: var(--bo-header-bg);
|
|
2145
|
+
border: 0.5px solid var(--bo-border);
|
|
2146
|
+
border-radius: 6px;
|
|
2147
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
|
|
2148
|
+
white-space: normal;
|
|
2149
|
+
overflow-wrap: anywhere;
|
|
2150
|
+
pointer-events: none;
|
|
2151
|
+
transform: translate(-50%, -100%);
|
|
2152
|
+
animation: bo-tip-in 90ms ease-out;
|
|
2153
|
+
}
|
|
2154
|
+
.bo-tip.below {
|
|
2155
|
+
transform: translate(-50%, 0);
|
|
2156
|
+
}
|
|
2157
|
+
/* Caret. */
|
|
2158
|
+
.bo-tip::after {
|
|
2159
|
+
content: "";
|
|
2160
|
+
position: absolute;
|
|
2161
|
+
left: 50%;
|
|
2162
|
+
width: 7px;
|
|
2163
|
+
height: 7px;
|
|
2164
|
+
background: var(--bo-header-bg);
|
|
2165
|
+
border: 0.5px solid var(--bo-border);
|
|
2166
|
+
transform: translateX(-50%) rotate(45deg);
|
|
2167
|
+
}
|
|
2168
|
+
.bo-tip:not(.below)::after {
|
|
2169
|
+
bottom: -4px;
|
|
2170
|
+
border-top: 0;
|
|
2171
|
+
border-left: 0;
|
|
2172
|
+
}
|
|
2173
|
+
.bo-tip.below::after {
|
|
2174
|
+
top: -4px;
|
|
2175
|
+
border-bottom: 0;
|
|
2176
|
+
border-right: 0;
|
|
2177
|
+
}
|
|
2178
|
+
@keyframes bo-tip-in {
|
|
2179
|
+
from {
|
|
2180
|
+
opacity: 0;
|
|
2181
|
+
}
|
|
2182
|
+
to {
|
|
2183
|
+
opacity: 1;
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
@media (prefers-reduced-motion: reduce) {
|
|
2187
|
+
.bo-tip {
|
|
2188
|
+
animation: none;
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2025
2191
|
/* Built-in quick-filter toolbar (opt-in via `quickFilter`). */
|
|
2026
2192
|
.bo-toolbar {
|
|
2027
2193
|
display: flex;
|
|
@@ -2174,6 +2340,28 @@
|
|
|
2174
2340
|
font-size: 9px;
|
|
2175
2341
|
color: var(--bo-text);
|
|
2176
2342
|
}
|
|
2343
|
+
/* Header info cue: a small circled "i" signalling a headerTooltip. */
|
|
2344
|
+
.h .hinfo {
|
|
2345
|
+
display: inline-flex;
|
|
2346
|
+
align-items: center;
|
|
2347
|
+
justify-content: center;
|
|
2348
|
+
flex: none;
|
|
2349
|
+
width: 12px;
|
|
2350
|
+
height: 12px;
|
|
2351
|
+
margin-left: 1px;
|
|
2352
|
+
font-size: 8px;
|
|
2353
|
+
font-style: italic;
|
|
2354
|
+
font-family: var(--bo-mono);
|
|
2355
|
+
line-height: 1;
|
|
2356
|
+
color: var(--bo-text-dim);
|
|
2357
|
+
border: 0.5px solid var(--bo-text-dim);
|
|
2358
|
+
border-radius: 50%;
|
|
2359
|
+
pointer-events: none;
|
|
2360
|
+
}
|
|
2361
|
+
.h:hover .hinfo {
|
|
2362
|
+
color: var(--bo-text);
|
|
2363
|
+
border-color: var(--bo-text);
|
|
2364
|
+
}
|
|
2177
2365
|
.h .ind .ord {
|
|
2178
2366
|
font-size: 8px;
|
|
2179
2367
|
line-height: 1;
|
|
@@ -2374,6 +2562,12 @@
|
|
|
2374
2562
|
.row.rowsel {
|
|
2375
2563
|
background: var(--bo-sel-fill);
|
|
2376
2564
|
}
|
|
2565
|
+
/* Controlled active-row highlight (selectedRowId) — a tint + an accent bar so
|
|
2566
|
+
it reads distinctly from the checkbox selection and hover. */
|
|
2567
|
+
.row.rowactive {
|
|
2568
|
+
background: var(--bo-sel-fill);
|
|
2569
|
+
box-shadow: inset 2px 0 0 var(--bo-sel-border);
|
|
2570
|
+
}
|
|
2377
2571
|
.row.clickable {
|
|
2378
2572
|
cursor: pointer;
|
|
2379
2573
|
}
|
|
@@ -8,7 +8,12 @@ import type { RowSource } from './source';
|
|
|
8
8
|
type $$ComponentProps = {
|
|
9
9
|
rows: GridRow[];
|
|
10
10
|
columns: ColumnDef[];
|
|
11
|
-
height
|
|
11
|
+
/** Viewport height. A **number** is the scroll viewport's pixel height (the
|
|
12
|
+
grid's total height is that plus header/toolbar/footer chrome). A **CSS
|
|
13
|
+
string** (e.g. `'100%'`, `'80vh'`, `'480px'`) sizes the whole grid element
|
|
14
|
+
and the viewport auto-fits the space left after the chrome — give the grid
|
|
15
|
+
a sized parent (or use `'100%'` inside a flex/grid cell). */
|
|
16
|
+
height: number | string;
|
|
12
17
|
/** Row height in px (uniform), or a function for variable heights
|
|
13
18
|
(in-memory mode only). Default 36. */
|
|
14
19
|
rowHeight?: number | ((row: GridRow, index: number) => number);
|
|
@@ -17,6 +22,11 @@ type $$ComponentProps = {
|
|
|
17
22
|
/** Allow drag-to-resize column widths. Default true; opt out per column
|
|
18
23
|
with `resizable: false`. */
|
|
19
24
|
resizable?: boolean;
|
|
25
|
+
/** Highlight cells/ranges on click-drag (the blue selection fill + focus
|
|
26
|
+
ring + fill handle). Set false for a read-only/display grid where clicks
|
|
27
|
+
should pass straight through to `onRowClick`/`onCellClick` without a
|
|
28
|
+
selection highlight. Default true. */
|
|
29
|
+
cellSelection?: boolean;
|
|
20
30
|
/** Show a leading checkbox column for whole-row selection (keyed by row id,
|
|
21
31
|
stable across sort/filter). Default false. */
|
|
22
32
|
rowSelection?: boolean;
|
|
@@ -46,6 +56,10 @@ type $$ComponentProps = {
|
|
|
46
56
|
/** Identity key for row selection. Defaults to `row.id`; override for
|
|
47
57
|
string/UUID/composite keys. */
|
|
48
58
|
getRowId?: (row: GridRow) => string | number;
|
|
59
|
+
/** Controlled "active row" highlight (keyed by `getRowId`) — for
|
|
60
|
+
master-detail / list-detail where one row is selected. Independent of the
|
|
61
|
+
checkbox `rowSelection` and the cell range selection. `null` = none. */
|
|
62
|
+
selectedRowId?: string | number | null;
|
|
49
63
|
/** Called when a data row is activated by click or Enter (open a detail
|
|
50
64
|
view, navigate, …). Edit-input and checkbox clicks are excluded. */
|
|
51
65
|
onRowClick?: (row: GridRow, event: MouseEvent | KeyboardEvent) => void;
|
|
@@ -134,6 +148,12 @@ type $$ComponentProps = {
|
|
|
134
148
|
/** Rows per page. When > 0 (in-memory mode), shows a pager instead of one
|
|
135
149
|
long scroll; rows still virtualize within a page. Default 0 (off). */
|
|
136
150
|
pageSize?: number;
|
|
151
|
+
/** Page-size choices for a dropdown in the pager (e.g. `[25, 50, 100]`).
|
|
152
|
+
Picking one re-pages from page 0. Uncontrolled unless you also drive
|
|
153
|
+
`pageSize` and update it from `onPageSizeChange`. */
|
|
154
|
+
pageSizeOptions?: number[];
|
|
155
|
+
/** Called with the new page size when the pager's size dropdown changes. */
|
|
156
|
+
onPageSizeChange?: (size: number) => void;
|
|
137
157
|
/** Controlled current page (0-based). Omit for uncontrolled paging. */
|
|
138
158
|
page?: number;
|
|
139
159
|
/** Called with the new page index when the pager is used. */
|