@marianmeres/stuic 3.161.0 → 3.163.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/dist/components/DataTable/DataTable.svelte +242 -114
- package/dist/components/DataTable/DataTable.svelte.d.ts +63 -3
- package/dist/components/DataTable/README.md +219 -54
- package/dist/components/DataTable/i18n-sk.d.ts +17 -0
- package/dist/components/DataTable/i18n-sk.js +29 -0
- package/dist/components/DataTable/i18n.d.ts +43 -0
- package/dist/components/DataTable/i18n.js +51 -0
- package/dist/components/DataTable/index.css +73 -17
- package/dist/components/DataTable/index.d.ts +2 -0
- package/dist/components/DataTable/index.js +2 -0
- package/package.json +1 -1
|
@@ -4,35 +4,8 @@
|
|
|
4
4
|
import type { PagingCalcResult } from "@marianmeres/paging-store";
|
|
5
5
|
import type { THC } from "../Thc/index.js";
|
|
6
6
|
import type { TranslateFn } from "../../types.js";
|
|
7
|
-
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
// i18n ready
|
|
11
|
-
function t_default(
|
|
12
|
-
k: string,
|
|
13
|
-
values: false | null | undefined | Record<string, string | number> = null,
|
|
14
|
-
fallback: string | boolean = "",
|
|
15
|
-
_i18nSpanWrap: boolean = true
|
|
16
|
-
) {
|
|
17
|
-
const m: Record<string, string> = {
|
|
18
|
-
previous_page: "Prev",
|
|
19
|
-
next_page: "Next",
|
|
20
|
-
page_x_of_y: "Page {page} of {pageCount}",
|
|
21
|
-
no_data: "No data",
|
|
22
|
-
select_all_rows: "Select all rows on this page",
|
|
23
|
-
select_row: "Select row",
|
|
24
|
-
select_all_on_page_x: "All {count} on this page selected.",
|
|
25
|
-
select_all_results: "Select all {totalCount} results",
|
|
26
|
-
all_results_selected: "All {totalCount} results selected.",
|
|
27
|
-
clear_selection: "Clear selection",
|
|
28
|
-
};
|
|
29
|
-
let out = m[k] ?? fallback ?? k;
|
|
30
|
-
return isPlainObject(values)
|
|
31
|
-
? replaceMap(out, values as any, {
|
|
32
|
-
preSearchKeyTransform: (k) => `{${k}}`,
|
|
33
|
-
})
|
|
34
|
-
: out;
|
|
35
|
-
}
|
|
7
|
+
// i18n ready -- see ./i18n.ts (english, built-in) and ./i18n-sk.ts (opt-in slovak)
|
|
8
|
+
import { DATA_TABLE_MESSAGES_EN, t_default } from "./i18n.js";
|
|
36
9
|
|
|
37
10
|
export interface DataTableColumn<T = Record<string, any>> {
|
|
38
11
|
/** Property key to extract from row data (supports dot-notation: "data.name") */
|
|
@@ -95,9 +68,71 @@
|
|
|
95
68
|
/** Set of row IDs explicitly deselected while in all-pages mode (bindable) */
|
|
96
69
|
excluded?: Set<string | number>;
|
|
97
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Keep the selection bar -- the strip above the table holding the idle status,
|
|
73
|
+
* `batchActions`, and the select-all-across-pages offer -- in the layout at all
|
|
74
|
+
* times, so checking the first row does not shove the table down. While nothing
|
|
75
|
+
* is selected it reads `t("no_rows_selected")`.
|
|
76
|
+
*
|
|
77
|
+
* Only meaningful when `selectable` is on AND there is something to reserve room
|
|
78
|
+
* for (a `batchActions` snippet or `allowSelectAllPages`); with neither, no bar
|
|
79
|
+
* is rendered at all.
|
|
80
|
+
*
|
|
81
|
+
* Set `false` for the pop-in behaviour: the bar appears only once it has content.
|
|
82
|
+
*/
|
|
83
|
+
reserveBatchBar?: boolean;
|
|
84
|
+
|
|
98
85
|
/** Callback when a row is clicked */
|
|
99
86
|
onRowClick?: (row: T, index: number) => void;
|
|
100
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Where this row's primary action navigates. When it returns a href for a row, the
|
|
90
|
+
* content of that row's "lead" cell is wrapped in an anchor -- which is what makes a
|
|
91
|
+
* clickable row keyboard-, middle- and modifier-click-reachable.
|
|
92
|
+
*
|
|
93
|
+
* Return `undefined` for a row with no destination (a placeholder, a row still
|
|
94
|
+
* minting its id) -- that row's lead cell renders exactly as it does today.
|
|
95
|
+
*
|
|
96
|
+
* Independent of `onRowClick`: supply both and the anchor handles the keyboard and
|
|
97
|
+
* modified clicks while `onRowClick` keeps handling a plain click anywhere else on
|
|
98
|
+
* the row -- the row click handler ignores events originating inside an anchor, so
|
|
99
|
+
* the two never double-fire. (Consequence: a plain click on the lead cell navigates
|
|
100
|
+
* via the href and does NOT call `onRowClick` -- point both at the same destination.)
|
|
101
|
+
*/
|
|
102
|
+
rowHref?: (row: T, index: number) => string | undefined;
|
|
103
|
+
/**
|
|
104
|
+
* Which column is the "lead" cell for `rowHref`. Defaults to the first column in
|
|
105
|
+
* `columns`. On mobile, if that column is `hideOnMobile`, the first visible card
|
|
106
|
+
* field is used instead (so the card keeps a keyboard-reachable link).
|
|
107
|
+
*/
|
|
108
|
+
rowHrefColumn?: string;
|
|
109
|
+
/** Additional CSS classes for the anchor generated by `rowHref`. */
|
|
110
|
+
classRowLink?: string;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Make the desktop `<tr>` itself focusable and Enter-activatable.
|
|
114
|
+
*
|
|
115
|
+
* For a row whose action is NOT a navigation -- prefer `rowHref` when it is, since a
|
|
116
|
+
* real link is better in every respect (tab order, `Enter`, middle/cmd-click, "copy
|
|
117
|
+
* link address", and an announcement of where it goes).
|
|
118
|
+
*
|
|
119
|
+
* Deliberately does NOT set `role="button"`: a `<tr>` carries an implicit `row` role
|
|
120
|
+
* that the table's own structure depends on, and overriding it detaches the row from
|
|
121
|
+
* the table for assistive technology. The row stays a row; it merely becomes focusable.
|
|
122
|
+
*
|
|
123
|
+
* Also deliberately `Enter`-only, unlike the mobile card (which IS a `role="button"`,
|
|
124
|
+
* where `Space` is expected). Taking `Space` -- page scroll -- away from a table is a
|
|
125
|
+
* worse trade than the one the card makes.
|
|
126
|
+
*
|
|
127
|
+
* No-op unless `onRowClick` or `selectOnRowClick` is set.
|
|
128
|
+
*/
|
|
129
|
+
rowActivatable?: boolean;
|
|
130
|
+
/**
|
|
131
|
+
* Accessible name for an activatable row, e.g. `(row) => "Open " + row.name`.
|
|
132
|
+
* Only applied when the row is actually activatable.
|
|
133
|
+
*/
|
|
134
|
+
rowLabel?: (row: T, index: number) => string | undefined;
|
|
135
|
+
|
|
101
136
|
/** Show loading state (spinner overlay + reduced opacity) */
|
|
102
137
|
loading?: boolean;
|
|
103
138
|
|
|
@@ -125,7 +160,8 @@
|
|
|
125
160
|
]
|
|
126
161
|
>;
|
|
127
162
|
/**
|
|
128
|
-
*
|
|
163
|
+
* Content of the selection bar while something is selected. Replaces the default
|
|
164
|
+
* "{count} selected" status, so render your own count alongside the buttons.
|
|
129
165
|
*
|
|
130
166
|
* Note: in all-pages mode (`selectedAll === true`) `selectedRows` only contains
|
|
131
167
|
* rows from the current page that aren't excluded. Off-page rows are not
|
|
@@ -147,8 +183,8 @@
|
|
|
147
183
|
]
|
|
148
184
|
>;
|
|
149
185
|
/**
|
|
150
|
-
* Custom "select all results across pages"
|
|
151
|
-
*
|
|
186
|
+
* Custom "select all results across pages" offer, rendered on the trailing edge
|
|
187
|
+
* of the selection bar. When omitted, a default one is rendered.
|
|
152
188
|
*/
|
|
153
189
|
selectAllBanner?: Snippet<
|
|
154
190
|
[
|
|
@@ -202,7 +238,13 @@
|
|
|
202
238
|
allowSelectAllPages = false,
|
|
203
239
|
selectedAll = $bindable(false),
|
|
204
240
|
excluded = $bindable(new Set()),
|
|
241
|
+
reserveBatchBar = true,
|
|
205
242
|
onRowClick,
|
|
243
|
+
rowHref,
|
|
244
|
+
rowHrefColumn,
|
|
245
|
+
classRowLink,
|
|
246
|
+
rowActivatable = false,
|
|
247
|
+
rowLabel,
|
|
206
248
|
loading = false,
|
|
207
249
|
cell,
|
|
208
250
|
row,
|
|
@@ -342,22 +384,70 @@
|
|
|
342
384
|
return allOnPageSelected;
|
|
343
385
|
});
|
|
344
386
|
|
|
345
|
-
//
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
387
|
+
// The default selection status, shown when there is no `batchActions` snippet to take
|
|
388
|
+
// its place. Both keys post-date the component, so a consumer `t` written against an
|
|
389
|
+
// older version does not know them -- and one that answers an unknown key with ""
|
|
390
|
+
// would leave the reserved bar visibly blank. Hence the explicit english fallbacks
|
|
391
|
+
// (pre-interpolated, since a `t` may hand a fallback straight back untouched), which
|
|
392
|
+
// every other, older key can do without.
|
|
393
|
+
let statusText = $derived(
|
|
394
|
+
effectiveCount > 0
|
|
395
|
+
? t(
|
|
396
|
+
"x_rows_selected",
|
|
397
|
+
{ count: effectiveCount },
|
|
398
|
+
DATA_TABLE_MESSAGES_EN.x_rows_selected.replace("{count}", `${effectiveCount}`)
|
|
399
|
+
)
|
|
400
|
+
: t("no_rows_selected", null, DATA_TABLE_MESSAGES_EN.no_rows_selected)
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
// The bar only exists if something can appear in it; `reserveBatchBar` then decides
|
|
404
|
+
// whether it holds its place while that something is absent.
|
|
405
|
+
let hasBatchBar = $derived(selectable && (!!batchActions || allowSelectAllPages));
|
|
406
|
+
let showBatchBar = $derived(
|
|
407
|
+
hasBatchBar &&
|
|
408
|
+
(reserveBatchBar || showSelectAllBanner || (effectiveCount > 0 && !!batchActions))
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
// --- Row click / activation ---
|
|
412
|
+
|
|
413
|
+
// A row is a big target that legitimately contains its own controls. Anything that
|
|
414
|
+
// originates inside one of those is that control's business, not the row's - this
|
|
415
|
+
// guards the click handler AND both keydown handlers (Enter on a focused lead link
|
|
416
|
+
// must navigate, not fire onRowClick; Space on a focused checkbox must toggle it).
|
|
417
|
+
function isInteractiveTarget(e: Event): boolean {
|
|
418
|
+
const target = e.target as HTMLElement | null;
|
|
419
|
+
if (!target?.closest) return false;
|
|
420
|
+
return !!(
|
|
349
421
|
target.closest('input[type="checkbox"]') ||
|
|
350
422
|
target.closest("button") ||
|
|
351
423
|
target.closest("a")
|
|
352
|
-
)
|
|
353
|
-
|
|
354
|
-
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function activateRow(row: T, index: number) {
|
|
355
428
|
if (selectable && selectOnRowClick) {
|
|
356
429
|
toggleSelectRow(getRowId(row, index));
|
|
357
430
|
}
|
|
358
431
|
onRowClick?.(row, index);
|
|
359
432
|
}
|
|
360
433
|
|
|
434
|
+
function handleRowClick(row: T, index: number, e: MouseEvent) {
|
|
435
|
+
if (isInteractiveTarget(e)) return;
|
|
436
|
+
activateRow(row, index);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Shared by the desktop `<tr>` (Enter only) and the mobile card (Enter + Space, since
|
|
441
|
+
* the card actually is a `role="button"`).
|
|
442
|
+
*/
|
|
443
|
+
function handleRowKeydown(row: T, index: number, e: KeyboardEvent, withSpace: boolean) {
|
|
444
|
+
if (!rowClickable) return;
|
|
445
|
+
if (e.key !== "Enter" && !(withSpace && e.key === " ")) return;
|
|
446
|
+
if (isInteractiveTarget(e)) return;
|
|
447
|
+
e.preventDefault();
|
|
448
|
+
activateRow(row, index);
|
|
449
|
+
}
|
|
450
|
+
|
|
361
451
|
// --- Cell value helpers ---
|
|
362
452
|
function getCellValue(row: T, column: DataTableColumn<T>): any {
|
|
363
453
|
return column.key.split(".").reduce((obj: any, k) => obj?.[k], row);
|
|
@@ -373,49 +463,101 @@
|
|
|
373
463
|
let rootClass = $derived(unstyled ? classProp : twMerge("stuic-data-table", classProp));
|
|
374
464
|
|
|
375
465
|
let mobileColumns = $derived(columns.filter((col) => !col.hideOnMobile));
|
|
376
|
-
</script>
|
|
377
466
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
467
|
+
// --- Row link / activation derivations ---
|
|
468
|
+
let rowClickable = $derived(!!(onRowClick || selectOnRowClick));
|
|
469
|
+
let rowIsActivatable = $derived(rowActivatable && rowClickable);
|
|
470
|
+
|
|
471
|
+
// The lead cell is the one whose content gets wrapped in the `rowHref` anchor.
|
|
472
|
+
let leadColumnKey = $derived(rowHrefColumn ?? columns[0]?.key);
|
|
473
|
+
// A `hideOnMobile` lead column would leave the card with no link at all, so there we
|
|
474
|
+
// fall back to the first visible field rather than silently dropping the anchor.
|
|
475
|
+
let mobileLeadColumnKey = $derived(
|
|
476
|
+
mobileColumns.some((col) => col.key === leadColumnKey)
|
|
477
|
+
? leadColumnKey
|
|
478
|
+
: mobileColumns[0]?.key
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
let rowLinkClass = $derived(
|
|
482
|
+
unstyled ? classRowLink : twMerge("stuic-data-table-row-link", classRowLink)
|
|
483
|
+
);
|
|
484
|
+
</script>
|
|
392
485
|
|
|
393
|
-
<!--
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
486
|
+
<!--
|
|
487
|
+
The rendered content of a single cell, shared by both layouts so that a `rowHref`
|
|
488
|
+
anchor wraps exactly what the cell would have rendered anyway (a consumer `cell`
|
|
489
|
+
snippet included).
|
|
490
|
+
-->
|
|
491
|
+
{#snippet cellBody(
|
|
492
|
+
col: DataTableColumn<T>,
|
|
493
|
+
rowData: T,
|
|
494
|
+
value: any,
|
|
495
|
+
rowIndex: number,
|
|
496
|
+
variant: "desktop" | "mobile"
|
|
497
|
+
)}
|
|
498
|
+
{#if cell}
|
|
499
|
+
{@render cell({ column: col, row: rowData, value, rowIndex, variant })}
|
|
404
500
|
{:else}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
501
|
+
{getCellDisplay(rowData, col)}
|
|
502
|
+
{/if}
|
|
503
|
+
{/snippet}
|
|
504
|
+
|
|
505
|
+
<!--
|
|
506
|
+
Selection bar -- one strip carrying the selection status, the consumer's
|
|
507
|
+
`batchActions`, and the select-all-across-pages offer. Deliberately a single element
|
|
508
|
+
rather than a stack of independent conditionals: each of those appearing on its own
|
|
509
|
+
reflows everything below it, which is exactly the jump `reserveBatchBar` exists to
|
|
510
|
+
prevent. Sits outside the root container so the loading overlay doesn't cover it.
|
|
511
|
+
-->
|
|
512
|
+
{#if showBatchBar}
|
|
513
|
+
<div
|
|
514
|
+
class={!unstyled ? "stuic-data-table-batch" : undefined}
|
|
515
|
+
data-idle={!unstyled && effectiveCount === 0 ? "true" : undefined}
|
|
516
|
+
data-select-all={!unstyled && showSelectAllBanner ? "true" : undefined}
|
|
517
|
+
>
|
|
518
|
+
<div class={!unstyled ? "stuic-data-table-batch-main" : undefined}>
|
|
519
|
+
{#if effectiveCount > 0 && batchActions}
|
|
520
|
+
{@render batchActions({
|
|
521
|
+
selected,
|
|
522
|
+
selectedRows,
|
|
523
|
+
selectedAll,
|
|
524
|
+
excluded,
|
|
525
|
+
effectiveCount,
|
|
526
|
+
totalCount,
|
|
527
|
+
clearSelection: clearAllSelection,
|
|
528
|
+
})}
|
|
411
529
|
{:else}
|
|
412
|
-
<span
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
</Button>
|
|
530
|
+
<span class={!unstyled ? "stuic-data-table-batch-status" : undefined}>
|
|
531
|
+
{statusText}
|
|
532
|
+
</span>
|
|
416
533
|
{/if}
|
|
417
534
|
</div>
|
|
418
|
-
|
|
535
|
+
|
|
536
|
+
{#if showSelectAllBanner && paging}
|
|
537
|
+
<div class={!unstyled ? "stuic-data-table-batch-select-all" : undefined}>
|
|
538
|
+
{#if selectAllBanner}
|
|
539
|
+
{@render selectAllBanner({
|
|
540
|
+
selectedAll,
|
|
541
|
+
effectiveCount,
|
|
542
|
+
totalCount: paging.total,
|
|
543
|
+
pageCount: data.length,
|
|
544
|
+
selectAll: enterSelectAll,
|
|
545
|
+
clearSelection: clearAllSelection,
|
|
546
|
+
})}
|
|
547
|
+
{:else if selectedAll}
|
|
548
|
+
<span>{t("all_results_selected", { totalCount: paging.total })}</span>
|
|
549
|
+
<Button variant="ghost" size="sm" onclick={clearAllSelection}>
|
|
550
|
+
{t("clear_selection")}
|
|
551
|
+
</Button>
|
|
552
|
+
{:else}
|
|
553
|
+
<span>{t("select_all_on_page_x", { count: data.length })}</span>
|
|
554
|
+
<Button variant="ghost" size="sm" onclick={enterSelectAll}>
|
|
555
|
+
{t("select_all_results", { totalCount: paging.total })}
|
|
556
|
+
</Button>
|
|
557
|
+
{/if}
|
|
558
|
+
</div>
|
|
559
|
+
{/if}
|
|
560
|
+
</div>
|
|
419
561
|
{/if}
|
|
420
562
|
|
|
421
563
|
<!-- Root container -->
|
|
@@ -461,16 +603,20 @@
|
|
|
461
603
|
{@const rowId = getRowId(rowData, rowIndex)}
|
|
462
604
|
{@const isSelected = selectable && isRowSelected(rowId)}
|
|
463
605
|
{@const selectDisabled = !!selectDisabledBy?.(rowData, rowIndex)}
|
|
606
|
+
{@const rowLink = rowHref?.(rowData, rowIndex)}
|
|
464
607
|
{#if row}
|
|
465
608
|
{@render row({ row: rowData, columns, rowIndex, isSelected })}
|
|
466
609
|
{:else}
|
|
467
610
|
<tr
|
|
468
611
|
data-hoverable={!unstyled ? "true" : undefined}
|
|
469
|
-
data-clickable={!unstyled &&
|
|
470
|
-
? "true"
|
|
471
|
-
: undefined}
|
|
612
|
+
data-clickable={!unstyled && rowClickable ? "true" : undefined}
|
|
472
613
|
data-selected={!unstyled && isSelected ? "true" : undefined}
|
|
614
|
+
tabindex={rowIsActivatable ? 0 : undefined}
|
|
615
|
+
aria-label={rowIsActivatable ? rowLabel?.(rowData, rowIndex) : undefined}
|
|
473
616
|
onclick={(e) => handleRowClick(rowData, rowIndex, e)}
|
|
617
|
+
onkeydown={(e) => {
|
|
618
|
+
if (rowActivatable) handleRowKeydown(rowData, rowIndex, e, false);
|
|
619
|
+
}}
|
|
474
620
|
>
|
|
475
621
|
{#if selectable}
|
|
476
622
|
<td data-checkbox class="stuic-checkbox">
|
|
@@ -485,20 +631,17 @@
|
|
|
485
631
|
{/if}
|
|
486
632
|
{#each columns as col (col.key)}
|
|
487
633
|
{@const value = getCellValue(rowData, col)}
|
|
634
|
+
{@const href = col.key === leadColumnKey ? rowLink : undefined}
|
|
488
635
|
<td
|
|
489
636
|
class={col.class}
|
|
490
637
|
data-align={!unstyled && col.align ? col.align : undefined}
|
|
491
638
|
>
|
|
492
|
-
{#if
|
|
493
|
-
{
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
value,
|
|
497
|
-
rowIndex,
|
|
498
|
-
variant: "desktop",
|
|
499
|
-
})}
|
|
639
|
+
{#if href}
|
|
640
|
+
<a class={rowLinkClass} {href}>
|
|
641
|
+
{@render cellBody(col, rowData, value, rowIndex, "desktop")}
|
|
642
|
+
</a>
|
|
500
643
|
{:else}
|
|
501
|
-
{
|
|
644
|
+
{@render cellBody(col, rowData, value, rowIndex, "desktop")}
|
|
502
645
|
{/if}
|
|
503
646
|
</td>
|
|
504
647
|
{/each}
|
|
@@ -531,6 +674,7 @@
|
|
|
531
674
|
{@const rowId = getRowId(rowData, rowIndex)}
|
|
532
675
|
{@const isSelected = selectable && isRowSelected(rowId)}
|
|
533
676
|
{@const selectDisabled = !!selectDisabledBy?.(rowData, rowIndex)}
|
|
677
|
+
{@const rowLink = rowHref?.(rowData, rowIndex)}
|
|
534
678
|
{#if mobileRow}
|
|
535
679
|
{@render mobileRow({
|
|
536
680
|
row: rowData,
|
|
@@ -542,25 +686,12 @@
|
|
|
542
686
|
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
543
687
|
<div
|
|
544
688
|
class={!unstyled ? "stuic-data-table-card" : undefined}
|
|
545
|
-
data-clickable={!unstyled &&
|
|
546
|
-
? "true"
|
|
547
|
-
: undefined}
|
|
689
|
+
data-clickable={!unstyled && rowClickable ? "true" : undefined}
|
|
548
690
|
data-selected={!unstyled && isSelected ? "true" : undefined}
|
|
549
|
-
role={
|
|
550
|
-
tabindex={
|
|
691
|
+
role={rowClickable ? "button" : undefined}
|
|
692
|
+
tabindex={rowClickable ? 0 : undefined}
|
|
551
693
|
onclick={(e) => handleRowClick(rowData, rowIndex, e)}
|
|
552
|
-
onkeydown={(e) =>
|
|
553
|
-
if (
|
|
554
|
-
(onRowClick || selectOnRowClick) &&
|
|
555
|
-
(e.key === "Enter" || e.key === " ")
|
|
556
|
-
) {
|
|
557
|
-
e.preventDefault();
|
|
558
|
-
if (selectable && selectOnRowClick) {
|
|
559
|
-
toggleSelectRow(rowId);
|
|
560
|
-
}
|
|
561
|
-
onRowClick?.(rowData, rowIndex);
|
|
562
|
-
}
|
|
563
|
-
}}
|
|
694
|
+
onkeydown={(e) => handleRowKeydown(rowData, rowIndex, e, true)}
|
|
564
695
|
>
|
|
565
696
|
{#if selectable}
|
|
566
697
|
<div
|
|
@@ -579,6 +710,7 @@
|
|
|
579
710
|
{/if}
|
|
580
711
|
{#each mobileColumns as col (col.key)}
|
|
581
712
|
{@const value = getCellValue(rowData, col)}
|
|
713
|
+
{@const href = col.key === mobileLeadColumnKey ? rowLink : undefined}
|
|
582
714
|
<div class={!unstyled ? "stuic-data-table-card-row" : undefined}>
|
|
583
715
|
<span class={!unstyled ? "stuic-data-table-card-label" : undefined}>
|
|
584
716
|
{#if isTHCNotEmpty(col.label)}
|
|
@@ -588,16 +720,12 @@
|
|
|
588
720
|
{/if}
|
|
589
721
|
</span>
|
|
590
722
|
<span class={!unstyled ? "stuic-data-table-card-value" : undefined}>
|
|
591
|
-
{#if
|
|
592
|
-
{
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
value,
|
|
596
|
-
rowIndex,
|
|
597
|
-
variant: "mobile",
|
|
598
|
-
})}
|
|
723
|
+
{#if href}
|
|
724
|
+
<a class={rowLinkClass} {href}>
|
|
725
|
+
{@render cellBody(col, rowData, value, rowIndex, "mobile")}
|
|
726
|
+
</a>
|
|
599
727
|
{:else}
|
|
600
|
-
{
|
|
728
|
+
{@render cellBody(col, rowData, value, rowIndex, "mobile")}
|
|
601
729
|
{/if}
|
|
602
730
|
</span>
|
|
603
731
|
</div>
|
|
@@ -56,8 +56,67 @@ export interface Props<T = Record<string, any>> extends Omit<HTMLAttributes<HTML
|
|
|
56
56
|
selectedAll?: boolean;
|
|
57
57
|
/** Set of row IDs explicitly deselected while in all-pages mode (bindable) */
|
|
58
58
|
excluded?: Set<string | number>;
|
|
59
|
+
/**
|
|
60
|
+
* Keep the selection bar -- the strip above the table holding the idle status,
|
|
61
|
+
* `batchActions`, and the select-all-across-pages offer -- in the layout at all
|
|
62
|
+
* times, so checking the first row does not shove the table down. While nothing
|
|
63
|
+
* is selected it reads `t("no_rows_selected")`.
|
|
64
|
+
*
|
|
65
|
+
* Only meaningful when `selectable` is on AND there is something to reserve room
|
|
66
|
+
* for (a `batchActions` snippet or `allowSelectAllPages`); with neither, no bar
|
|
67
|
+
* is rendered at all.
|
|
68
|
+
*
|
|
69
|
+
* Set `false` for the pop-in behaviour: the bar appears only once it has content.
|
|
70
|
+
*/
|
|
71
|
+
reserveBatchBar?: boolean;
|
|
59
72
|
/** Callback when a row is clicked */
|
|
60
73
|
onRowClick?: (row: T, index: number) => void;
|
|
74
|
+
/**
|
|
75
|
+
* Where this row's primary action navigates. When it returns a href for a row, the
|
|
76
|
+
* content of that row's "lead" cell is wrapped in an anchor -- which is what makes a
|
|
77
|
+
* clickable row keyboard-, middle- and modifier-click-reachable.
|
|
78
|
+
*
|
|
79
|
+
* Return `undefined` for a row with no destination (a placeholder, a row still
|
|
80
|
+
* minting its id) -- that row's lead cell renders exactly as it does today.
|
|
81
|
+
*
|
|
82
|
+
* Independent of `onRowClick`: supply both and the anchor handles the keyboard and
|
|
83
|
+
* modified clicks while `onRowClick` keeps handling a plain click anywhere else on
|
|
84
|
+
* the row -- the row click handler ignores events originating inside an anchor, so
|
|
85
|
+
* the two never double-fire. (Consequence: a plain click on the lead cell navigates
|
|
86
|
+
* via the href and does NOT call `onRowClick` -- point both at the same destination.)
|
|
87
|
+
*/
|
|
88
|
+
rowHref?: (row: T, index: number) => string | undefined;
|
|
89
|
+
/**
|
|
90
|
+
* Which column is the "lead" cell for `rowHref`. Defaults to the first column in
|
|
91
|
+
* `columns`. On mobile, if that column is `hideOnMobile`, the first visible card
|
|
92
|
+
* field is used instead (so the card keeps a keyboard-reachable link).
|
|
93
|
+
*/
|
|
94
|
+
rowHrefColumn?: string;
|
|
95
|
+
/** Additional CSS classes for the anchor generated by `rowHref`. */
|
|
96
|
+
classRowLink?: string;
|
|
97
|
+
/**
|
|
98
|
+
* Make the desktop `<tr>` itself focusable and Enter-activatable.
|
|
99
|
+
*
|
|
100
|
+
* For a row whose action is NOT a navigation -- prefer `rowHref` when it is, since a
|
|
101
|
+
* real link is better in every respect (tab order, `Enter`, middle/cmd-click, "copy
|
|
102
|
+
* link address", and an announcement of where it goes).
|
|
103
|
+
*
|
|
104
|
+
* Deliberately does NOT set `role="button"`: a `<tr>` carries an implicit `row` role
|
|
105
|
+
* that the table's own structure depends on, and overriding it detaches the row from
|
|
106
|
+
* the table for assistive technology. The row stays a row; it merely becomes focusable.
|
|
107
|
+
*
|
|
108
|
+
* Also deliberately `Enter`-only, unlike the mobile card (which IS a `role="button"`,
|
|
109
|
+
* where `Space` is expected). Taking `Space` -- page scroll -- away from a table is a
|
|
110
|
+
* worse trade than the one the card makes.
|
|
111
|
+
*
|
|
112
|
+
* No-op unless `onRowClick` or `selectOnRowClick` is set.
|
|
113
|
+
*/
|
|
114
|
+
rowActivatable?: boolean;
|
|
115
|
+
/**
|
|
116
|
+
* Accessible name for an activatable row, e.g. `(row) => "Open " + row.name`.
|
|
117
|
+
* Only applied when the row is actually activatable.
|
|
118
|
+
*/
|
|
119
|
+
rowLabel?: (row: T, index: number) => string | undefined;
|
|
61
120
|
/** Show loading state (spinner overlay + reduced opacity) */
|
|
62
121
|
loading?: boolean;
|
|
63
122
|
/** Custom cell renderer snippet (rendered in both desktop table and mobile card layouts; use `variant` to tell them apart) */
|
|
@@ -80,7 +139,8 @@ export interface Props<T = Record<string, any>> extends Omit<HTMLAttributes<HTML
|
|
|
80
139
|
}
|
|
81
140
|
]>;
|
|
82
141
|
/**
|
|
83
|
-
*
|
|
142
|
+
* Content of the selection bar while something is selected. Replaces the default
|
|
143
|
+
* "{count} selected" status, so render your own count alongside the buttons.
|
|
84
144
|
*
|
|
85
145
|
* Note: in all-pages mode (`selectedAll === true`) `selectedRows` only contains
|
|
86
146
|
* rows from the current page that aren't excluded. Off-page rows are not
|
|
@@ -100,8 +160,8 @@ export interface Props<T = Record<string, any>> extends Omit<HTMLAttributes<HTML
|
|
|
100
160
|
}
|
|
101
161
|
]>;
|
|
102
162
|
/**
|
|
103
|
-
* Custom "select all results across pages"
|
|
104
|
-
*
|
|
163
|
+
* Custom "select all results across pages" offer, rendered on the trailing edge
|
|
164
|
+
* of the selection bar. When omitted, a default one is rendered.
|
|
105
165
|
*/
|
|
106
166
|
selectAllBanner?: Snippet<[
|
|
107
167
|
{
|
|
@@ -64,6 +64,30 @@ DataTable integrates with [`@marianmeres/paging-store`](https://github.com/maria
|
|
|
64
64
|
|
|
65
65
|
Shift+click a checkbox to toggle a range of rows from the last clicked checkbox to the current one. Disabled rows in the range are skipped. The anchor resets when `data` changes (e.g. on page navigation).
|
|
66
66
|
|
|
67
|
+
#### The selection bar holds its place
|
|
68
|
+
|
|
69
|
+
Everything selection-related lives in one strip above the table: the status while nothing
|
|
70
|
+
is selected, your `batchActions` while something is, and the select-all-across-pages offer
|
|
71
|
+
on its trailing edge. That strip is rendered from the start and keeps a constant height
|
|
72
|
+
(`reserveBatchBar`, on by default), so checking the first row swaps its contents instead
|
|
73
|
+
of pushing the table down.
|
|
74
|
+
|
|
75
|
+
- The height comes from `--stuic-data-table-batch-min-height`, sized to a `size="sm"`
|
|
76
|
+
Button. Batch actions built from bigger controls need that token raised, or the bar
|
|
77
|
+
grows when they appear — which is the jump all over again.
|
|
78
|
+
- Idle, the bar is transparent (`--stuic-data-table-batch-bg-idle`) so a permanently
|
|
79
|
+
reserved strip doesn't shout; it takes on `--stuic-data-table-batch-bg` once something
|
|
80
|
+
is selected and `--stuic-data-table-select-all-bg` while the select-all offer is up.
|
|
81
|
+
- The bar only exists when there is something to reserve room for — `selectable` plus
|
|
82
|
+
either a `batchActions` snippet or `allowSelectAllPages`. `selectable` on its own
|
|
83
|
+
renders no bar.
|
|
84
|
+
- `reserveBatchBar={false}` restores the old behaviour: the bar appears only once it has
|
|
85
|
+
content.
|
|
86
|
+
|
|
87
|
+
Without a `batchActions` snippet the bar still reports the count itself
|
|
88
|
+
(`t("x_rows_selected")` / `t("no_rows_selected")`). With one, the snippet replaces that
|
|
89
|
+
status entirely — render your own count next to the buttons.
|
|
90
|
+
|
|
67
91
|
### Select All Across Pages
|
|
68
92
|
|
|
69
93
|
When your data is paged, DataTable can offer a "select all results" affordance that expands selection beyond the current page. Enable it with `allowSelectAllPages`. When the user opts in, selection flips to **all-pages mode**: every row is implicitly selected, and `excluded` holds IDs the user has explicitly deselected.
|
|
@@ -115,7 +139,7 @@ function deleteSelection({ selectedAll, excluded }) {
|
|
|
115
139
|
|
|
116
140
|
New records inserted while all-pages mode is active are implicitly selected (they aren't in `excluded`). This matches the conventional intent — "delete everything matching X" should include matches that arrive before the operation runs. If you need snapshot-at-click semantics, capture a timestamp or ID list in your consumer.
|
|
117
141
|
|
|
118
|
-
**Customising the
|
|
142
|
+
**Customising the offer:** the default offer renders on the trailing edge of the selection bar and uses the built-in `t()` keys `select_all_on_page_x`, `select_all_results`, `all_results_selected`, and `clear_selection`. Override its markup entirely with the `selectAllBanner` snippet (which renders in the same slot — it is no longer a standalone block below the batch bar):
|
|
119
143
|
|
|
120
144
|
```svelte
|
|
121
145
|
<DataTable
|
|
@@ -143,6 +167,89 @@ New records inserted while all-pages mode is active are implicitly selected (the
|
|
|
143
167
|
|
|
144
168
|
**Filter changes:** when filters change in the consumer, reset the bound selection stores (`selected`, `selectedAll`, `excluded`) explicitly — DataTable doesn't track which filter produced the current state.
|
|
145
169
|
|
|
170
|
+
### Clickable Rows That Work Without a Mouse
|
|
171
|
+
|
|
172
|
+
`onRowClick` alone makes a row **mouse-only** on the desktop table layout: a `<tr>` is not
|
|
173
|
+
focusable, so nothing about the row is reachable by keyboard. (The mobile card layout has
|
|
174
|
+
always been fine — it is a `<div role="button">`.) Two opt-in props close that gap, and
|
|
175
|
+
they are not alternatives:
|
|
176
|
+
|
|
177
|
+
| Your row action is… | Use |
|
|
178
|
+
| ------------------------------- | ---------------- |
|
|
179
|
+
| a navigation (it has a URL) | `rowHref` |
|
|
180
|
+
| anything else (drawer, expand…) | `rowActivatable` |
|
|
181
|
+
|
|
182
|
+
#### `rowHref` — the lead cell becomes a real link
|
|
183
|
+
|
|
184
|
+
```svelte
|
|
185
|
+
<DataTable
|
|
186
|
+
{columns}
|
|
187
|
+
{data}
|
|
188
|
+
getRowId={(row) => row.id}
|
|
189
|
+
rowHref={(row) => `/users/${row.id}`}
|
|
190
|
+
/>
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
The content of the row's **lead cell** (the first column by default, or `rowHrefColumn`)
|
|
194
|
+
is wrapped in an `<a href>` — including whatever your `cell` snippet renders for it, and
|
|
195
|
+
respecting the column's `renderValue`. A link gets, for free and correctly: keyboard focus
|
|
196
|
+
in the right tab order, `Enter`, ⌘/middle-click and "open in new tab", "copy link address",
|
|
197
|
+
and a screen-reader announcement of _where it goes_ rather than an anonymous "button".
|
|
198
|
+
|
|
199
|
+
Return `undefined` for a row with no destination (a placeholder, a row still minting its
|
|
200
|
+
id) — that row's lead cell renders exactly as it would without `rowHref`.
|
|
201
|
+
|
|
202
|
+
`rowHref` composes with `onRowClick`: the row click handler ignores clicks originating
|
|
203
|
+
inside an anchor, so the two never double-fire. The consequence is worth stating — a plain
|
|
204
|
+
click **on the lead cell** navigates via the href and does _not_ call `onRowClick`, so point
|
|
205
|
+
both at the same destination.
|
|
206
|
+
|
|
207
|
+
On mobile the same field is linked. If the lead column is `hideOnMobile`, the card links its
|
|
208
|
+
first visible field instead, rather than silently dropping the anchor.
|
|
209
|
+
|
|
210
|
+
By default the link looks like the text it replaced (`color: inherit`, no underline until
|
|
211
|
+
hover) — a table where every lead cell is blue-and-underlined is a worse table. Three CSS
|
|
212
|
+
variables and `classRowLink` are there if you disagree.
|
|
213
|
+
|
|
214
|
+
#### `rowActivatable` — the `<tr>` itself
|
|
215
|
+
|
|
216
|
+
For a row whose action has no address:
|
|
217
|
+
|
|
218
|
+
```svelte
|
|
219
|
+
<DataTable
|
|
220
|
+
{columns}
|
|
221
|
+
{data}
|
|
222
|
+
onRowClick={(row) => openDrawer(row)}
|
|
223
|
+
rowActivatable
|
|
224
|
+
rowLabel={(row) => `Open ${row.name}`}
|
|
225
|
+
/>
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
The `<tr>` gets `tabindex="0"`, an optional `aria-label` from `rowLabel`, and a
|
|
229
|
+
`:focus-visible` ring. It is a no-op unless `onRowClick` or `selectOnRowClick` is set.
|
|
230
|
+
|
|
231
|
+
Two deliberate choices, so nobody "fixes" them later:
|
|
232
|
+
|
|
233
|
+
- **No `role="button"` on the `<tr>`.** A `<tr>` carries an implicit `row` role that the
|
|
234
|
+
table's own structure depends on. Overriding it detaches the row from the table for
|
|
235
|
+
assistive technology — row/column counts stop making sense and cell-by-cell navigation
|
|
236
|
+
breaks. The row stays a row; it merely becomes focusable. (The mobile card is a `<div>`,
|
|
237
|
+
so `role="button"` is correct _there_.)
|
|
238
|
+
- **`Enter` only, no `Space`.** `Space` on a focused row scrolls the page, and a row is not
|
|
239
|
+
a button — taking `Space` away from a table is a worse trade than the one the card makes.
|
|
240
|
+
|
|
241
|
+
With `selectable` + `selectOnRowClick`, `Enter` toggles selection _and_ fires `onRowClick`,
|
|
242
|
+
matching the mobile card.
|
|
243
|
+
|
|
244
|
+
#### Both are opt-in
|
|
245
|
+
|
|
246
|
+
With neither prop set, the rendered markup is exactly what it was before they existed. A
|
|
247
|
+
consumer who replaces the whole `<tr>` with the `row` snippet opts out of both and owns the
|
|
248
|
+
keyboard story themselves.
|
|
249
|
+
|
|
250
|
+
Interactive descendants always win: a click, `Enter` or `Space` that originates inside a
|
|
251
|
+
`<button>`, an `<a>` or a checkbox in the row is that control's business, not the row's.
|
|
252
|
+
|
|
146
253
|
### Custom Cell Rendering
|
|
147
254
|
|
|
148
255
|
The `cell` snippet is used for both desktop and mobile layouts. Use the `variant` param if rendering differs per layout.
|
|
@@ -204,33 +311,39 @@ Replace the entire `<tr>` on desktop. When this snippet is provided, DataTable d
|
|
|
204
311
|
|
|
205
312
|
## Props
|
|
206
313
|
|
|
207
|
-
| Prop | Type
|
|
208
|
-
| --------------------- |
|
|
209
|
-
| `columns` | `DataTableColumn<T>[]`
|
|
210
|
-
| `data` | `T[]`
|
|
211
|
-
| `getRowId` | `(row, index) => string \| number`
|
|
212
|
-
| `paging` | `PagingCalcResult`
|
|
213
|
-
| `onPageChange` | `(offset: number) => void`
|
|
214
|
-
| `selectable` | `boolean`
|
|
215
|
-
| `selected` | `Set<string \| number>`
|
|
216
|
-
| `selectOnRowClick` | `boolean`
|
|
217
|
-
| `selectDisabledBy` | `(row, index) => boolean`
|
|
218
|
-
| `allowSelectAllPages` | `boolean`
|
|
219
|
-
| `selectedAll` | `boolean`
|
|
220
|
-
| `excluded` | `Set<string \| number>`
|
|
221
|
-
| `
|
|
222
|
-
| `
|
|
223
|
-
| `
|
|
224
|
-
| `
|
|
225
|
-
| `
|
|
226
|
-
| `
|
|
227
|
-
| `
|
|
228
|
-
| `
|
|
229
|
-
| `
|
|
230
|
-
| `
|
|
231
|
-
| `
|
|
232
|
-
| `
|
|
233
|
-
| `
|
|
314
|
+
| Prop | Type | Default | Description |
|
|
315
|
+
| --------------------- | ------------------------------------- | ------------- | -------------------------------------------------------------------------- |
|
|
316
|
+
| `columns` | `DataTableColumn<T>[]` | required | Column definitions |
|
|
317
|
+
| `data` | `T[]` | required | Array of row data |
|
|
318
|
+
| `getRowId` | `(row, index) => string \| number` | `(_, i) => i` | Row ID extractor |
|
|
319
|
+
| `paging` | `PagingCalcResult` | - | Paging state (from `@marianmeres/paging-store`) |
|
|
320
|
+
| `onPageChange` | `(offset: number) => void` | - | Called with the new offset when the user navigates pages |
|
|
321
|
+
| `selectable` | `boolean` | `false` | Enable selection checkboxes |
|
|
322
|
+
| `selected` | `Set<string \| number>` | `new Set()` | Selected row IDs (bindable) |
|
|
323
|
+
| `selectOnRowClick` | `boolean` | `false` | Clicking anywhere on a row toggles its selection |
|
|
324
|
+
| `selectDisabledBy` | `(row, index) => boolean` | - | Return `true` to disable selection for a specific row |
|
|
325
|
+
| `allowSelectAllPages` | `boolean` | `false` | Show a banner offering "select all results" across paged data |
|
|
326
|
+
| `selectedAll` | `boolean` | `false` | All-pages mode flag (bindable). In this mode `excluded` drives selection |
|
|
327
|
+
| `excluded` | `Set<string \| number>` | `new Set()` | Deselected row IDs while in all-pages mode (bindable) |
|
|
328
|
+
| `reserveBatchBar` | `boolean` | `true` | Keep the selection bar in the layout while nothing is selected |
|
|
329
|
+
| `onRowClick` | `(row, index) => void` | - | Row click callback |
|
|
330
|
+
| `rowHref` | `(row, index) => string \| undefined` | - | Wrap the lead cell's content in an `<a href>` (keyboard/⌘-click reachable) |
|
|
331
|
+
| `rowHrefColumn` | `string` | first column | Which column is the lead cell for `rowHref` |
|
|
332
|
+
| `classRowLink` | `string` | - | Extra classes for the `rowHref` anchor |
|
|
333
|
+
| `rowActivatable` | `boolean` | `false` | Make the desktop `<tr>` focusable + `Enter`-activatable (no `role`) |
|
|
334
|
+
| `rowLabel` | `(row, index) => string \| undefined` | - | Accessible name for an activatable row |
|
|
335
|
+
| `loading` | `boolean` | `false` | Show loading overlay |
|
|
336
|
+
| `small` | `boolean` | `false` | Force mobile/card layout regardless of viewport |
|
|
337
|
+
| `t` | `TranslateFn` | built-in | Optional translation function |
|
|
338
|
+
| `cell` | `Snippet` | - | Custom cell renderer (desktop + mobile) |
|
|
339
|
+
| `row` | `Snippet` | - | Custom desktop `<tr>` renderer (overrides default row) |
|
|
340
|
+
| `mobileRow` | `Snippet` | - | Custom mobile card renderer |
|
|
341
|
+
| `batchActions` | `Snippet` | - | Selection bar content while something is selected |
|
|
342
|
+
| `selectAllBanner` | `Snippet` | - | Override default "select all across pages" offer |
|
|
343
|
+
| `empty` | `Snippet` | - | Custom empty state |
|
|
344
|
+
| `unstyled` | `boolean` | `false` | Skip default styling |
|
|
345
|
+
| `class` | `string` | - | Additional CSS classes |
|
|
346
|
+
| `el` | `HTMLDivElement` | - | Bindable element ref |
|
|
234
347
|
|
|
235
348
|
### Snippet signatures
|
|
236
349
|
|
|
@@ -260,31 +373,83 @@ Replace the entire `<tr>` on desktop. When this snippet is provided, DataTable d
|
|
|
260
373
|
| `hideOnMobile` | `boolean` | `false` | Hide in mobile view |
|
|
261
374
|
| `renderValue` | `(value, row) => string` | - | Value formatter |
|
|
262
375
|
|
|
376
|
+
## i18n
|
|
377
|
+
|
|
378
|
+
All built-in texts go through the `t` prop. English is the built-in default; Slovak ships
|
|
379
|
+
bundled and opt-in (importing it is what pulls it into your bundle — English-only
|
|
380
|
+
consumers pay nothing).
|
|
381
|
+
|
|
382
|
+
```svelte
|
|
383
|
+
<script>
|
|
384
|
+
import {
|
|
385
|
+
DataTable,
|
|
386
|
+
createDataTableT,
|
|
387
|
+
DATA_TABLE_MESSAGES_SK,
|
|
388
|
+
} from "@marianmeres/stuic";
|
|
389
|
+
|
|
390
|
+
const t = createDataTableT(DATA_TABLE_MESSAGES_SK);
|
|
391
|
+
</script>
|
|
392
|
+
|
|
393
|
+
<DataTable {columns} {data} {t} />
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
`createDataTableT(messages, fallbackMessages?)` falls back to `DATA_TABLE_MESSAGES_EN` for
|
|
397
|
+
any key the catalog does not define, so a partial catalog is fine and a raw key is never
|
|
398
|
+
rendered — pass your own object to translate into a language that is not bundled, or to
|
|
399
|
+
override individual bundled texts:
|
|
400
|
+
|
|
401
|
+
```ts
|
|
402
|
+
const t = createDataTableT({ ...DATA_TABLE_MESSAGES_SK, no_data: "Nič tu nie je" });
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
| Key | English | Used by |
|
|
406
|
+
| ---------------------- | ---------------------------------- | ------------------------------------ |
|
|
407
|
+
| `previous_page` | Prev | Paging |
|
|
408
|
+
| `next_page` | Next | Paging |
|
|
409
|
+
| `page_x_of_y` | Page {page} of {pageCount} | Paging |
|
|
410
|
+
| `no_data` | No data | Empty state (no `empty` snippet) |
|
|
411
|
+
| `select_all_rows` | Select all rows on this page | Header checkbox `aria-label` |
|
|
412
|
+
| `select_row` | Select row | Row checkbox `aria-label` |
|
|
413
|
+
| `no_rows_selected` | No rows selected | Idle selection bar |
|
|
414
|
+
| `x_rows_selected` | {count} selected | Selection bar without `batchActions` |
|
|
415
|
+
| `select_all_on_page_x` | All {count} on this page selected. | Select-all offer |
|
|
416
|
+
| `select_all_results` | Select all {totalCount} results | Select-all offer |
|
|
417
|
+
| `all_results_selected` | All {totalCount} results selected. | Select-all offer, all-pages mode |
|
|
418
|
+
| `clear_selection` | Clear selection | Select-all offer, all-pages mode |
|
|
419
|
+
|
|
263
420
|
## CSS Variables
|
|
264
421
|
|
|
265
|
-
| Variable
|
|
266
|
-
|
|
|
267
|
-
| `--stuic-data-table-radius`
|
|
268
|
-
| `--stuic-data-table-border-color`
|
|
269
|
-
| `--stuic-data-table-header-bg`
|
|
270
|
-
| `--stuic-data-table-header-color`
|
|
271
|
-
| `--stuic-data-table-header-font-size`
|
|
272
|
-
| `--stuic-data-table-header-font-weight`
|
|
273
|
-
| `--stuic-data-table-header-padding-x`
|
|
274
|
-
| `--stuic-data-table-header-padding-y`
|
|
275
|
-
| `--stuic-data-table-row-bg`
|
|
276
|
-
| `--stuic-data-table-row-bg-hover`
|
|
277
|
-
| `--stuic-data-table-row-bg-selected`
|
|
278
|
-
| `--stuic-data-table-row-border-color`
|
|
279
|
-
| `--stuic-data-table-
|
|
280
|
-
| `--stuic-data-table-
|
|
281
|
-
| `--stuic-data-table-
|
|
282
|
-
| `--stuic-data-table-
|
|
283
|
-
| `--stuic-data-table-
|
|
284
|
-
| `--stuic-data-table-
|
|
285
|
-
| `--stuic-data-table-
|
|
286
|
-
| `--stuic-data-table-
|
|
287
|
-
| `--stuic-data-table-
|
|
288
|
-
| `--stuic-data-table-
|
|
289
|
-
| `--stuic-data-table-
|
|
290
|
-
| `--stuic-data-table-
|
|
422
|
+
| Variable | Default | Description |
|
|
423
|
+
| ---------------------------------------------- | ------------------------------------- | --------------------------------- |
|
|
424
|
+
| `--stuic-data-table-radius` | `var(--radius-md)` | Border radius |
|
|
425
|
+
| `--stuic-data-table-border-color` | `var(--stuic-color-border)` | Border color |
|
|
426
|
+
| `--stuic-data-table-header-bg` | `var(--stuic-color-muted)` | Header background |
|
|
427
|
+
| `--stuic-data-table-header-color` | `var(--stuic-color-muted-foreground)` | Header text |
|
|
428
|
+
| `--stuic-data-table-header-font-size` | `0.875rem` | Header font size |
|
|
429
|
+
| `--stuic-data-table-header-font-weight` | `var(--font-weight-semibold)` | Header font weight |
|
|
430
|
+
| `--stuic-data-table-header-padding-x` | `0.75rem` | Header horizontal padding |
|
|
431
|
+
| `--stuic-data-table-header-padding-y` | `0.5rem` | Header vertical padding |
|
|
432
|
+
| `--stuic-data-table-row-bg` | `transparent` | Row background |
|
|
433
|
+
| `--stuic-data-table-row-bg-hover` | `var(--stuic-color-muted)` | Row hover background |
|
|
434
|
+
| `--stuic-data-table-row-bg-selected` | `color-mix(primary 10%)` | Selected row background |
|
|
435
|
+
| `--stuic-data-table-row-border-color` | `var(--stuic-color-border)` | Row border color |
|
|
436
|
+
| `--stuic-data-table-row-link-color` | `inherit` | `rowHref` anchor color |
|
|
437
|
+
| `--stuic-data-table-row-link-decoration` | `none` | `rowHref` anchor decoration |
|
|
438
|
+
| `--stuic-data-table-row-link-decoration-hover` | `underline` | …on hover |
|
|
439
|
+
| `--stuic-data-table-row-ring-width` | `3px` | Activatable row focus ring |
|
|
440
|
+
| `--stuic-data-table-row-ring-color` | `var(--stuic-color-ring)` | Activatable row ring color |
|
|
441
|
+
| `--stuic-data-table-cell-padding-x` | `0.75rem` | Cell horizontal padding |
|
|
442
|
+
| `--stuic-data-table-cell-padding-y` | `0.75rem` | Cell vertical padding |
|
|
443
|
+
| `--stuic-data-table-cell-font-size` | `0.875rem` | Cell font size |
|
|
444
|
+
| `--stuic-data-table-loading-opacity` | `0.5` | Loading state opacity |
|
|
445
|
+
| `--stuic-data-table-card-bg` | `var(--stuic-color-background)` | Mobile card background |
|
|
446
|
+
| `--stuic-data-table-card-border-color` | `var(--stuic-color-border)` | Mobile card border |
|
|
447
|
+
| `--stuic-data-table-card-radius` | `var(--radius-md)` | Mobile card radius |
|
|
448
|
+
| `--stuic-data-table-card-padding` | `0.75rem` | Mobile card padding |
|
|
449
|
+
| `--stuic-data-table-card-gap` | `0.5rem` | Gap between mobile cards |
|
|
450
|
+
| `--stuic-data-table-batch-bg` | `var(--stuic-color-muted)` | Selection bar background |
|
|
451
|
+
| `--stuic-data-table-batch-bg-idle` | `transparent` | …while nothing is selected |
|
|
452
|
+
| `--stuic-data-table-batch-padding-x` | `0.75rem` | Selection bar h. padding |
|
|
453
|
+
| `--stuic-data-table-batch-padding-y` | `0.5rem` | Selection bar v. padding |
|
|
454
|
+
| `--stuic-data-table-batch-min-height` | `sm Button + padding` | Reserved selection bar height |
|
|
455
|
+
| `--stuic-data-table-select-all-bg` | `color-mix(primary 10%)` | …while the select-all offer is up |
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { DataTableMessages } from "./i18n.js";
|
|
2
|
+
/**
|
|
3
|
+
* Slovak message catalog for `DataTable`. Opt-in — English stays the built-in default,
|
|
4
|
+
* and this module is only pulled into a bundle when it is actually imported (the
|
|
5
|
+
* component itself never references it).
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```svelte
|
|
9
|
+
* <script>
|
|
10
|
+
* import { DataTable, createDataTableT, DATA_TABLE_MESSAGES_SK } from "@marianmeres/stuic";
|
|
11
|
+
* const t = createDataTableT(DATA_TABLE_MESSAGES_SK);
|
|
12
|
+
* </script>
|
|
13
|
+
*
|
|
14
|
+
* <DataTable {columns} {data} {t} />
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export declare const DATA_TABLE_MESSAGES_SK: DataTableMessages;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slovak message catalog for `DataTable`. Opt-in — English stays the built-in default,
|
|
3
|
+
* and this module is only pulled into a bundle when it is actually imported (the
|
|
4
|
+
* component itself never references it).
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```svelte
|
|
8
|
+
* <script>
|
|
9
|
+
* import { DataTable, createDataTableT, DATA_TABLE_MESSAGES_SK } from "@marianmeres/stuic";
|
|
10
|
+
* const t = createDataTableT(DATA_TABLE_MESSAGES_SK);
|
|
11
|
+
* </script>
|
|
12
|
+
*
|
|
13
|
+
* <DataTable {columns} {data} {t} />
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export const DATA_TABLE_MESSAGES_SK = {
|
|
17
|
+
previous_page: "Späť",
|
|
18
|
+
next_page: "Ďalej",
|
|
19
|
+
page_x_of_y: "Strana {page} z {pageCount}",
|
|
20
|
+
no_data: "Žiadne údaje",
|
|
21
|
+
select_all_rows: "Vybrať všetky riadky na tejto strane",
|
|
22
|
+
select_row: "Vybrať riadok",
|
|
23
|
+
select_all_on_page_x: "Vybrané všetky na tejto strane ({count}).",
|
|
24
|
+
select_all_results: "Vybrať všetky výsledky ({totalCount})",
|
|
25
|
+
all_results_selected: "Vybrané všetky výsledky ({totalCount}).",
|
|
26
|
+
clear_selection: "Zrušiť výber",
|
|
27
|
+
no_rows_selected: "Nič nie je vybrané",
|
|
28
|
+
x_rows_selected: "Vybrané: {count}",
|
|
29
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { TranslateFn } from "../../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* The built-in (English) message catalog of `DataTable`. Also the fallback of every
|
|
4
|
+
* other bundled locale, so a locale missing a key still renders text.
|
|
5
|
+
*
|
|
6
|
+
* Placeholders are single-brace (`{page}`, `{totalCount}`, ...).
|
|
7
|
+
*/
|
|
8
|
+
export declare const DATA_TABLE_MESSAGES_EN: {
|
|
9
|
+
previous_page: string;
|
|
10
|
+
next_page: string;
|
|
11
|
+
page_x_of_y: string;
|
|
12
|
+
no_data: string;
|
|
13
|
+
select_all_rows: string;
|
|
14
|
+
select_row: string;
|
|
15
|
+
select_all_on_page_x: string;
|
|
16
|
+
select_all_results: string;
|
|
17
|
+
all_results_selected: string;
|
|
18
|
+
clear_selection: string;
|
|
19
|
+
no_rows_selected: string;
|
|
20
|
+
x_rows_selected: string;
|
|
21
|
+
};
|
|
22
|
+
/** Every message key `DataTable` may look up. */
|
|
23
|
+
export type DataTableMessageKey = keyof typeof DATA_TABLE_MESSAGES_EN;
|
|
24
|
+
/** A (possibly partial) catalog for one locale. */
|
|
25
|
+
export type DataTableMessages = Record<DataTableMessageKey, string>;
|
|
26
|
+
/**
|
|
27
|
+
* Builds the `t` prop of `DataTable` from a message catalog. Unknown or untranslated
|
|
28
|
+
* keys fall back to `fallbackMessages` (English by default), so a catalog may safely be
|
|
29
|
+
* partial and never renders a raw key.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```svelte
|
|
33
|
+
* <script>
|
|
34
|
+
* import { DataTable, createDataTableT, DATA_TABLE_MESSAGES_SK } from "@marianmeres/stuic";
|
|
35
|
+
* const t = createDataTableT(DATA_TABLE_MESSAGES_SK);
|
|
36
|
+
* </script>
|
|
37
|
+
*
|
|
38
|
+
* <DataTable {columns} {data} {t} />
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export declare function createDataTableT(messages: Partial<DataTableMessages> | Record<string, string>, fallbackMessages?: Partial<DataTableMessages> | Record<string, string>): TranslateFn;
|
|
42
|
+
/** The component's built-in English `t`. */
|
|
43
|
+
export declare const t_default: TranslateFn;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { isPlainObject } from "../../utils/is-plain-object.js";
|
|
2
|
+
import { replaceMap } from "../../utils/replace-map.js";
|
|
3
|
+
/**
|
|
4
|
+
* The built-in (English) message catalog of `DataTable`. Also the fallback of every
|
|
5
|
+
* other bundled locale, so a locale missing a key still renders text.
|
|
6
|
+
*
|
|
7
|
+
* Placeholders are single-brace (`{page}`, `{totalCount}`, ...).
|
|
8
|
+
*/
|
|
9
|
+
export const DATA_TABLE_MESSAGES_EN = {
|
|
10
|
+
previous_page: "Prev",
|
|
11
|
+
next_page: "Next",
|
|
12
|
+
page_x_of_y: "Page {page} of {pageCount}",
|
|
13
|
+
no_data: "No data",
|
|
14
|
+
select_all_rows: "Select all rows on this page",
|
|
15
|
+
select_row: "Select row",
|
|
16
|
+
select_all_on_page_x: "All {count} on this page selected.",
|
|
17
|
+
select_all_results: "Select all {totalCount} results",
|
|
18
|
+
all_results_selected: "All {totalCount} results selected.",
|
|
19
|
+
clear_selection: "Clear selection",
|
|
20
|
+
no_rows_selected: "No rows selected",
|
|
21
|
+
x_rows_selected: "{count} selected",
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Builds the `t` prop of `DataTable` from a message catalog. Unknown or untranslated
|
|
25
|
+
* keys fall back to `fallbackMessages` (English by default), so a catalog may safely be
|
|
26
|
+
* partial and never renders a raw key.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```svelte
|
|
30
|
+
* <script>
|
|
31
|
+
* import { DataTable, createDataTableT, DATA_TABLE_MESSAGES_SK } from "@marianmeres/stuic";
|
|
32
|
+
* const t = createDataTableT(DATA_TABLE_MESSAGES_SK);
|
|
33
|
+
* </script>
|
|
34
|
+
*
|
|
35
|
+
* <DataTable {columns} {data} {t} />
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export function createDataTableT(messages, fallbackMessages = DATA_TABLE_MESSAGES_EN) {
|
|
39
|
+
return (k, values = null, fallback = "") => {
|
|
40
|
+
const out = messages[k] ??
|
|
41
|
+
fallbackMessages[k] ??
|
|
42
|
+
(typeof fallback === "string" ? fallback : k);
|
|
43
|
+
return isPlainObject(values)
|
|
44
|
+
? replaceMap(out, values, {
|
|
45
|
+
preSearchKeyTransform: (k) => `{${k}}`,
|
|
46
|
+
})
|
|
47
|
+
: out;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** The component's built-in English `t`. */
|
|
51
|
+
export const t_default = createDataTableT(DATA_TABLE_MESSAGES_EN);
|
|
@@ -24,6 +24,15 @@
|
|
|
24
24
|
--stuic-data-table-row-bg-selected: color-mix(in srgb, var(--stuic-color-primary) 10%, var(--stuic-color-background));
|
|
25
25
|
--stuic-data-table-row-border-color: var(--stuic-color-border);
|
|
26
26
|
|
|
27
|
+
/* Row link (rowHref) — defaults to "looks like the text it replaced" */
|
|
28
|
+
--stuic-data-table-row-link-color: inherit;
|
|
29
|
+
--stuic-data-table-row-link-decoration: none;
|
|
30
|
+
--stuic-data-table-row-link-decoration-hover: underline;
|
|
31
|
+
|
|
32
|
+
/* Focus ring for an activatable row (rowActivatable) */
|
|
33
|
+
--stuic-data-table-row-ring-width: 3px;
|
|
34
|
+
--stuic-data-table-row-ring-color: var(--stuic-color-ring);
|
|
35
|
+
|
|
27
36
|
/* Cell */
|
|
28
37
|
--stuic-data-table-cell-padding-x: 0.75rem;
|
|
29
38
|
--stuic-data-table-cell-padding-y: 0.75rem;
|
|
@@ -33,15 +42,19 @@
|
|
|
33
42
|
--stuic-data-table-paging-gap: 0.5rem;
|
|
34
43
|
--stuic-data-table-paging-padding-y: 0.75rem;
|
|
35
44
|
|
|
36
|
-
/*
|
|
45
|
+
/* Selection (batch action) bar */
|
|
37
46
|
--stuic-data-table-batch-bg: var(--stuic-color-muted);
|
|
47
|
+
/* Nothing selected -- the bar is only holding its place, so it stays quiet. */
|
|
48
|
+
--stuic-data-table-batch-bg-idle: var(--stuic-color-muted);
|
|
38
49
|
--stuic-data-table-batch-padding-x: 0.75rem;
|
|
39
|
-
--stuic-data-table-batch-padding-y: 0.
|
|
50
|
+
--stuic-data-table-batch-padding-y: 0.25rem;
|
|
51
|
+
/* Sized to a `size="sm"` Button so the idle and active states are the same height. */
|
|
52
|
+
--stuic-data-table-batch-min-height: calc(var(--stuic-button-min-height-sm, 2.75rem) + var(--stuic-data-table-batch-padding-y) * 2);
|
|
53
|
+
/* */
|
|
54
|
+
--stuic-data-table-batch-margin: 0 0 0.125rem 0;
|
|
40
55
|
|
|
41
|
-
/* Select-all-across-pages
|
|
56
|
+
/* Select-all-across-pages offer (rendered inside the selection bar) */
|
|
42
57
|
--stuic-data-table-select-all-bg: color-mix(in srgb, var(--stuic-color-primary) 10%, var(--stuic-color-background));
|
|
43
|
-
--stuic-data-table-select-all-padding-x: 0.75rem;
|
|
44
|
-
--stuic-data-table-select-all-padding-y: 0.5rem;
|
|
45
58
|
|
|
46
59
|
/* Checkbox column */
|
|
47
60
|
--stuic-data-table-checkbox-width: 3rem;
|
|
@@ -152,6 +165,31 @@
|
|
|
152
165
|
);
|
|
153
166
|
}
|
|
154
167
|
|
|
168
|
+
/*
|
|
169
|
+
`rowActivatable` puts tabindex on the <tr>, and a focusable element with no
|
|
170
|
+
visible ring is worse than no tabindex at all. The negative offset keeps the
|
|
171
|
+
ring inside the row box — an outset ring on a <tr> is clipped unpredictably
|
|
172
|
+
by border-collapse.
|
|
173
|
+
*/
|
|
174
|
+
.stuic-data-table tbody tr:focus-visible {
|
|
175
|
+
outline: var(--stuic-data-table-row-ring-width) solid
|
|
176
|
+
var(--stuic-data-table-row-ring-color);
|
|
177
|
+
outline-offset: calc(var(--stuic-data-table-row-ring-width) * -1);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/* ============================================================================
|
|
181
|
+
ROW LINK (rowHref)
|
|
182
|
+
============================================================================ */
|
|
183
|
+
|
|
184
|
+
.stuic-data-table-row-link {
|
|
185
|
+
color: var(--stuic-data-table-row-link-color);
|
|
186
|
+
text-decoration: var(--stuic-data-table-row-link-decoration);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.stuic-data-table-row-link:hover {
|
|
190
|
+
text-decoration: var(--stuic-data-table-row-link-decoration-hover);
|
|
191
|
+
}
|
|
192
|
+
|
|
155
193
|
/* ============================================================================
|
|
156
194
|
CELL
|
|
157
195
|
============================================================================ */
|
|
@@ -204,34 +242,52 @@
|
|
|
204
242
|
BATCH ACTION BAR
|
|
205
243
|
============================================================================ */
|
|
206
244
|
|
|
245
|
+
/*
|
|
246
|
+
The min-height is the whole point of the reserved bar: an idle line of text is
|
|
247
|
+
shorter than a row of buttons, so without it the table would still shift -- just
|
|
248
|
+
by less. Keep it in sync with whatever button size the batch actions use.
|
|
249
|
+
*/
|
|
207
250
|
.stuic-data-table-batch {
|
|
208
251
|
display: flex;
|
|
209
252
|
align-items: center;
|
|
253
|
+
justify-content: space-between;
|
|
254
|
+
flex-wrap: wrap;
|
|
210
255
|
gap: var(--stuic-data-table-paging-gap);
|
|
256
|
+
min-height: var(--stuic-data-table-batch-min-height);
|
|
211
257
|
padding: var(--stuic-data-table-batch-padding-y)
|
|
212
258
|
var(--stuic-data-table-batch-padding-x);
|
|
213
259
|
background: var(--stuic-data-table-batch-bg);
|
|
214
260
|
border-radius: var(--stuic-data-table-radius, var(--stuic-radius));
|
|
215
|
-
margin
|
|
261
|
+
margin: var(--stuic-data-table-batch-margin, 0);
|
|
262
|
+
transition: background var(--stuic-data-table-transition, var(--stuic-transition));
|
|
216
263
|
}
|
|
217
264
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
265
|
+
.stuic-data-table-batch[data-idle="true"] {
|
|
266
|
+
background: var(--stuic-data-table-batch-bg-idle);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/* The select-all offer is the one state worth actively drawing attention to. */
|
|
270
|
+
.stuic-data-table-batch[data-select-all="true"] {
|
|
271
|
+
background: var(--stuic-data-table-select-all-bg);
|
|
272
|
+
}
|
|
221
273
|
|
|
222
|
-
.stuic-data-table-
|
|
274
|
+
.stuic-data-table-batch-main,
|
|
275
|
+
.stuic-data-table-batch-select-all {
|
|
223
276
|
display: flex;
|
|
224
277
|
align-items: center;
|
|
225
|
-
|
|
278
|
+
flex-wrap: wrap;
|
|
226
279
|
gap: var(--stuic-data-table-paging-gap);
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.stuic-data-table-batch-select-all {
|
|
283
|
+
margin-left: auto;
|
|
284
|
+
font-size: var(--stuic-data-table-cell-font-size);
|
|
285
|
+
color: var(--stuic-data-table-header-color);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.stuic-data-table-batch-status {
|
|
232
289
|
font-size: var(--stuic-data-table-cell-font-size);
|
|
233
290
|
color: var(--stuic-data-table-header-color);
|
|
234
|
-
text-align: center;
|
|
235
291
|
}
|
|
236
292
|
|
|
237
293
|
/* ============================================================================
|
|
@@ -1 +1,3 @@
|
|
|
1
1
|
export { default as DataTable, type Props as DataTableProps, type DataTableColumn, } from "./DataTable.svelte";
|
|
2
|
+
export { createDataTableT, DATA_TABLE_MESSAGES_EN, type DataTableMessageKey, type DataTableMessages, } from "./i18n.js";
|
|
3
|
+
export { DATA_TABLE_MESSAGES_SK } from "./i18n-sk.js";
|