sveltacular 1.0.15 → 1.0.16
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/forms/check-box/check-box.svelte +19 -9
- package/dist/forms/check-box/check-box.svelte.d.ts +1 -0
- package/dist/forms/radio-group/radio-box.svelte +20 -25
- package/dist/forms/radio-group/radio-box.svelte.d.ts +1 -0
- package/dist/tables/data-grid.svelte +36 -12
- package/dist/tables/data-grid.svelte.d.ts +4 -4
- package/dist/tables/table-context.svelte.d.ts +9 -5
- package/dist/tables/table-context.svelte.js +59 -12
- package/dist/tables/table-row.svelte +3 -39
- package/dist/tables/table-selection-cell.svelte +134 -0
- package/dist/tables/table-selection-cell.svelte.d.ts +8 -0
- package/dist/tables/table-selection-header-cell.svelte +103 -0
- package/dist/tables/table-selection-header-cell.svelte.d.ts +3 -0
- package/dist/tables/table.svelte +15 -9
- package/dist/tables/table.svelte.d.ts +4 -3
- package/package.json +1 -1
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
name = undefined,
|
|
13
13
|
onChange = undefined,
|
|
14
14
|
label,
|
|
15
|
+
ariaLabel,
|
|
15
16
|
children,
|
|
16
17
|
size = 'full' as FormFieldSizeOptions,
|
|
17
18
|
helperText = undefined,
|
|
@@ -25,6 +26,7 @@
|
|
|
25
26
|
name?: string | undefined;
|
|
26
27
|
onChange?: ((data: { isChecked: boolean; value: string }) => void) | undefined;
|
|
27
28
|
label?: string;
|
|
29
|
+
ariaLabel?: string;
|
|
28
30
|
children?: Snippet;
|
|
29
31
|
size?: FormFieldSizeOptions;
|
|
30
32
|
helperText?: string;
|
|
@@ -33,6 +35,9 @@
|
|
|
33
35
|
inline?: boolean;
|
|
34
36
|
} = $props();
|
|
35
37
|
|
|
38
|
+
// Use ariaLabel if provided, otherwise fall back to label for accessibility
|
|
39
|
+
let inputAriaLabel = $derived(ariaLabel ?? label);
|
|
40
|
+
|
|
36
41
|
const id = uniqueId();
|
|
37
42
|
|
|
38
43
|
const onChecked = (event: Event) => {
|
|
@@ -56,18 +61,21 @@
|
|
|
56
61
|
bind:checked={isChecked}
|
|
57
62
|
onchange={onChecked}
|
|
58
63
|
{required}
|
|
64
|
+
aria-label={inputAriaLabel}
|
|
59
65
|
/>
|
|
60
66
|
<span class="checkbox">
|
|
61
67
|
<span class="checkmark"><Icon type="check" size="sm" fill="#fff" mask /></span>
|
|
62
68
|
</span>
|
|
63
|
-
{#if
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
{#if !ariaLabel}
|
|
70
|
+
{#if children}
|
|
71
|
+
<div class="text">
|
|
72
|
+
{@render children()}
|
|
73
|
+
</div>
|
|
74
|
+
{:else if label}
|
|
75
|
+
<div class="text">
|
|
76
|
+
{label}
|
|
77
|
+
</div>
|
|
78
|
+
{/if}
|
|
71
79
|
{/if}
|
|
72
80
|
</label>
|
|
73
81
|
{:else}
|
|
@@ -82,6 +90,7 @@
|
|
|
82
90
|
bind:checked={isChecked}
|
|
83
91
|
onchange={onChecked}
|
|
84
92
|
{required}
|
|
93
|
+
aria-label={inputAriaLabel}
|
|
85
94
|
/>
|
|
86
95
|
<span class="checkbox">
|
|
87
96
|
<span class="checkmark"> <Icon type="check" size="sm" fill="#fff" mask /></span>
|
|
@@ -123,7 +132,7 @@
|
|
|
123
132
|
user-select: none;
|
|
124
133
|
}
|
|
125
134
|
.checkbox-label .checkbox .checkmark {
|
|
126
|
-
display:
|
|
135
|
+
display: none;
|
|
127
136
|
position: absolute;
|
|
128
137
|
top: 0;
|
|
129
138
|
left: 0;
|
|
@@ -145,6 +154,7 @@
|
|
|
145
154
|
border-color: var(--form-input-border);
|
|
146
155
|
}
|
|
147
156
|
.checkbox-label input:checked + .checkbox .checkmark {
|
|
157
|
+
display: block;
|
|
148
158
|
width: 100%;
|
|
149
159
|
height: 100%;
|
|
150
160
|
}</style>
|
|
@@ -9,12 +9,14 @@
|
|
|
9
9
|
value = undefined as RadioValue,
|
|
10
10
|
group = $bindable(undefined as string | undefined),
|
|
11
11
|
disabled = false,
|
|
12
|
+
ariaLabel,
|
|
12
13
|
children = undefined,
|
|
13
14
|
onChange = undefined
|
|
14
15
|
}: {
|
|
15
16
|
value?: RadioValue;
|
|
16
17
|
group?: string | undefined;
|
|
17
18
|
disabled?: boolean;
|
|
19
|
+
ariaLabel?: string;
|
|
18
20
|
children?: Snippet;
|
|
19
21
|
onChange?: ((value: string) => void) | undefined;
|
|
20
22
|
} = $props();
|
|
@@ -29,12 +31,13 @@
|
|
|
29
31
|
{value}
|
|
30
32
|
{disabled}
|
|
31
33
|
{id}
|
|
34
|
+
aria-label={ariaLabel}
|
|
32
35
|
onchange={() => onChange?.(String(value || ''))}
|
|
33
36
|
/>
|
|
34
|
-
<span class="
|
|
35
|
-
<span class="
|
|
37
|
+
<span class="radio-circle">
|
|
38
|
+
<span class="radio-dot"></span>
|
|
36
39
|
</span>
|
|
37
|
-
{#if children}
|
|
40
|
+
{#if !ariaLabel && children}
|
|
38
41
|
<div class="text">
|
|
39
42
|
{@render children?.()}
|
|
40
43
|
</div>
|
|
@@ -49,43 +52,35 @@
|
|
|
49
52
|
font-size: 1rem;
|
|
50
53
|
cursor: pointer;
|
|
51
54
|
}
|
|
52
|
-
label .
|
|
55
|
+
label .radio-circle {
|
|
53
56
|
position: relative;
|
|
54
57
|
width: 1.2rem;
|
|
55
58
|
height: 1.2rem;
|
|
56
|
-
border-radius:
|
|
59
|
+
border-radius: 50%;
|
|
57
60
|
border: 1px solid var(--form-input-border, black);
|
|
58
61
|
background-color: var(--form-input-bg, white);
|
|
59
|
-
|
|
60
|
-
font-size: 0.875rem;
|
|
61
|
-
font-weight: 500;
|
|
62
|
-
line-height: 1.25rem;
|
|
63
|
-
transition: background-color 0.2s ease-in-out, border-color 0.2s ease-in-out, color 0.2s ease-in-out, fill 0.2s ease-in-out, stroke 0.2s ease-in-out;
|
|
62
|
+
transition: background-color 0.2s ease-in-out, border-color 0.2s ease-in-out;
|
|
64
63
|
user-select: none;
|
|
65
64
|
display: flex;
|
|
66
65
|
align-items: center;
|
|
67
66
|
justify-content: center;
|
|
68
|
-
padding-top: 0.1rem;
|
|
69
67
|
}
|
|
70
|
-
label .
|
|
71
|
-
display:
|
|
72
|
-
width: 0;
|
|
73
|
-
height: 0;
|
|
74
|
-
|
|
75
|
-
color: var(--form-input-selected-
|
|
76
|
-
|
|
77
|
-
stroke: var(--form-input-selected-fg, white);
|
|
78
|
-
transition: width 0.2s ease-in-out, height 0.2s ease-in-out;
|
|
68
|
+
label .radio-circle .radio-dot {
|
|
69
|
+
display: none;
|
|
70
|
+
width: 0.5rem;
|
|
71
|
+
height: 0.5rem;
|
|
72
|
+
border-radius: 50%;
|
|
73
|
+
background-color: var(--form-input-selected-bg, #3182ce);
|
|
74
|
+
transition: opacity 0.2s ease-in-out;
|
|
79
75
|
}
|
|
80
76
|
label input {
|
|
81
77
|
width: 0;
|
|
82
78
|
height: 0;
|
|
83
79
|
position: absolute;
|
|
84
80
|
}
|
|
85
|
-
label input:checked + .
|
|
86
|
-
|
|
81
|
+
label input:checked + .radio-circle {
|
|
82
|
+
border-color: var(--form-input-selected-bg, #3182ce);
|
|
87
83
|
}
|
|
88
|
-
label input:checked + .
|
|
89
|
-
|
|
90
|
-
height: 100%;
|
|
84
|
+
label input:checked + .radio-circle .radio-dot {
|
|
85
|
+
display: block;
|
|
91
86
|
}</style>
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
import TableHeader from './table-header.svelte';
|
|
5
5
|
import TableRow from './table-row.svelte';
|
|
6
6
|
import Table from './table.svelte';
|
|
7
|
+
import TableSelectionCell from './table-selection-cell.svelte';
|
|
8
|
+
import TableSelectionHeaderCell from './table-selection-header-cell.svelte';
|
|
7
9
|
import type { ColumnDef, JsonObject, PaginationProperties } from '../types/data.js';
|
|
8
10
|
import Button from '../forms/button/button.svelte';
|
|
9
11
|
import DropdownItem from '../generic/dropdown-item/dropdown-item.svelte';
|
|
@@ -21,10 +23,8 @@
|
|
|
21
23
|
getCellTypeClass,
|
|
22
24
|
sortRows
|
|
23
25
|
} from './cell-renderers.js';
|
|
24
|
-
import { getTableContext } from './table-context.svelte.js';
|
|
25
26
|
import type { Snippet } from 'svelte';
|
|
26
27
|
import { useVirtualList } from '../helpers/use-virtual-list.svelte.js';
|
|
27
|
-
import { onMount } from 'svelte';
|
|
28
28
|
|
|
29
29
|
type PaginationEvent = (pagination: PaginationProperties) => void;
|
|
30
30
|
|
|
@@ -48,12 +48,12 @@
|
|
|
48
48
|
actions = undefined,
|
|
49
49
|
stickyHeader = false,
|
|
50
50
|
enableSorting = true,
|
|
51
|
-
|
|
52
|
-
selectionMode = 'multi',
|
|
51
|
+
selectionMode = 'none',
|
|
53
52
|
rowIdKey = 'id',
|
|
54
53
|
onPageChange = null,
|
|
55
54
|
onSort = undefined,
|
|
56
55
|
onSelectionChange = undefined,
|
|
56
|
+
selectedCount = $bindable(0),
|
|
57
57
|
children = undefined,
|
|
58
58
|
virtualScroll = false,
|
|
59
59
|
rowHeight = 48,
|
|
@@ -67,12 +67,12 @@
|
|
|
67
67
|
actions?: Actions;
|
|
68
68
|
stickyHeader?: boolean;
|
|
69
69
|
enableSorting?: boolean;
|
|
70
|
-
|
|
71
|
-
selectionMode?: 'single' | 'multi';
|
|
70
|
+
selectionMode?: 'none' | 'single' | 'multi';
|
|
72
71
|
rowIdKey?: string;
|
|
73
72
|
onPageChange?: PaginationEvent | null;
|
|
74
73
|
onSort?: (column: string, direction: 'asc' | 'desc') => void;
|
|
75
|
-
onSelectionChange?: (
|
|
74
|
+
onSelectionChange?: (selectedRows: JsonObject[]) => void;
|
|
75
|
+
selectedCount?: number;
|
|
76
76
|
children?: Snippet;
|
|
77
77
|
virtualScroll?: boolean;
|
|
78
78
|
rowHeight?: number;
|
|
@@ -108,8 +108,19 @@
|
|
|
108
108
|
|
|
109
109
|
// Computed values
|
|
110
110
|
let hasActionCol = $derived(actions?.items && actions.items.length > 0);
|
|
111
|
+
let hasSelectionCol = $derived(selectionMode !== 'none');
|
|
111
112
|
let visibleCols = $derived(cols.filter((col) => !col.hidden));
|
|
112
|
-
let colCount = $derived(
|
|
113
|
+
let colCount = $derived(
|
|
114
|
+
Math.max(1, visibleCols.length) + (hasActionCol ? 1 : 0) + (hasSelectionCol ? 1 : 0)
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
// Track selected count from selection change callbacks
|
|
118
|
+
let internalSelectedCount = $state(0);
|
|
119
|
+
|
|
120
|
+
// Sync selectedCount with internal tracking
|
|
121
|
+
$effect(() => {
|
|
122
|
+
selectedCount = internalSelectedCount;
|
|
123
|
+
});
|
|
113
124
|
|
|
114
125
|
// Manage sort state directly in DataGrid (not via context)
|
|
115
126
|
let currentSortColumn = $state<string | null>(null);
|
|
@@ -181,13 +192,17 @@
|
|
|
181
192
|
</script>
|
|
182
193
|
|
|
183
194
|
<Table
|
|
195
|
+
rows={filteredRows ?? []}
|
|
184
196
|
{stickyHeader}
|
|
185
197
|
enableSorting={false}
|
|
186
|
-
{enableSelection}
|
|
187
198
|
{selectionMode}
|
|
188
199
|
{rowIdKey}
|
|
189
200
|
onSort={handleSortChange}
|
|
190
|
-
{
|
|
201
|
+
onSelectionChange={(selectedRows) => {
|
|
202
|
+
onSelectionChange?.(selectedRows);
|
|
203
|
+
// Track selected count from the callback
|
|
204
|
+
internalSelectedCount = selectedRows.length;
|
|
205
|
+
}}
|
|
191
206
|
>
|
|
192
207
|
{#if children}
|
|
193
208
|
<TableCaption side={captionSide} align={captionAlign}>{@render children?.()}</TableCaption>
|
|
@@ -195,6 +210,9 @@
|
|
|
195
210
|
|
|
196
211
|
<TableHeader sticky={stickyHeader}>
|
|
197
212
|
<tr>
|
|
213
|
+
{#if hasSelectionCol}
|
|
214
|
+
<TableSelectionHeaderCell />
|
|
215
|
+
{/if}
|
|
198
216
|
{#each visibleCols as col}
|
|
199
217
|
<TableHeaderCell
|
|
200
218
|
type={col.type}
|
|
@@ -245,7 +263,10 @@
|
|
|
245
263
|
<div
|
|
246
264
|
style="position: absolute; top: {vItem.offsetTop}px; height: {vItem.height}px; width: 100%; display: table; table-layout: fixed;"
|
|
247
265
|
>
|
|
248
|
-
<TableRow {row} rowIndex={index} selectable={
|
|
266
|
+
<TableRow {row} rowIndex={index} selectable={hasSelectionCol}>
|
|
267
|
+
{#if hasSelectionCol}
|
|
268
|
+
<TableSelectionCell {row} rowIndex={index} />
|
|
269
|
+
{/if}
|
|
249
270
|
{#each visibleCols as col}
|
|
250
271
|
{@const cellValue = formatCell(row, col)}
|
|
251
272
|
{@const cellLink = getCellLink(row, col)}
|
|
@@ -294,7 +315,10 @@
|
|
|
294
315
|
{:else}
|
|
295
316
|
<!-- Regular rendering mode -->
|
|
296
317
|
{#each filteredRows as row, index}
|
|
297
|
-
<TableRow {row} rowIndex={index} selectable={
|
|
318
|
+
<TableRow {row} rowIndex={index} selectable={hasSelectionCol}>
|
|
319
|
+
{#if hasSelectionCol}
|
|
320
|
+
<TableSelectionCell {row} rowIndex={index} />
|
|
321
|
+
{/if}
|
|
298
322
|
{#each visibleCols as col}
|
|
299
323
|
{@const cellValue = formatCell(row, col)}
|
|
300
324
|
{@const cellLink = getCellLink(row, col)}
|
|
@@ -19,17 +19,17 @@ type $$ComponentProps = {
|
|
|
19
19
|
actions?: Actions;
|
|
20
20
|
stickyHeader?: boolean;
|
|
21
21
|
enableSorting?: boolean;
|
|
22
|
-
|
|
23
|
-
selectionMode?: 'single' | 'multi';
|
|
22
|
+
selectionMode?: 'none' | 'single' | 'multi';
|
|
24
23
|
rowIdKey?: string;
|
|
25
24
|
onPageChange?: PaginationEvent | null;
|
|
26
25
|
onSort?: (column: string, direction: 'asc' | 'desc') => void;
|
|
27
|
-
onSelectionChange?: (
|
|
26
|
+
onSelectionChange?: (selectedRows: JsonObject[]) => void;
|
|
27
|
+
selectedCount?: number;
|
|
28
28
|
children?: Snippet;
|
|
29
29
|
virtualScroll?: boolean;
|
|
30
30
|
rowHeight?: number;
|
|
31
31
|
maxHeight?: string;
|
|
32
32
|
};
|
|
33
|
-
declare const DataGrid: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
33
|
+
declare const DataGrid: import("svelte").Component<$$ComponentProps, {}, "selectedCount">;
|
|
34
34
|
type DataGrid = ReturnType<typeof DataGrid>;
|
|
35
35
|
export default DataGrid;
|
|
@@ -1,27 +1,31 @@
|
|
|
1
1
|
import type { JsonObject, SortDirection, SortState } from '../types/data.js';
|
|
2
2
|
export interface TableContextConfig<T extends JsonObject = JsonObject> {
|
|
3
3
|
enableSorting?: boolean;
|
|
4
|
-
|
|
5
|
-
selectionMode?: 'single' | 'multi';
|
|
4
|
+
selectionMode?: 'none' | 'single' | 'multi';
|
|
6
5
|
rowIdKey?: keyof T & string;
|
|
7
6
|
onSort?: (column: string, direction: SortDirection) => void;
|
|
8
|
-
onSelectionChange?: (
|
|
7
|
+
onSelectionChange?: (selectedRows: T[]) => void;
|
|
8
|
+
rows?: T[];
|
|
9
9
|
}
|
|
10
10
|
export declare class TableContext<T extends JsonObject = JsonObject> {
|
|
11
11
|
sortColumn: string | null;
|
|
12
12
|
sortDirection: SortDirection;
|
|
13
13
|
selectedIds: Set<string | number>;
|
|
14
14
|
lastSelectedIndex: number | null;
|
|
15
|
+
radioGroup: string | undefined;
|
|
16
|
+
selectedCount: number;
|
|
15
17
|
config: TableContextConfig<T>;
|
|
16
18
|
constructor(config?: TableContextConfig<T>);
|
|
17
19
|
toggleSort(columnKey: string): void;
|
|
18
20
|
setSortColumn(columnKey: string | null, direction?: SortDirection): void;
|
|
19
21
|
clearSort(): void;
|
|
20
22
|
getSortState(): SortState;
|
|
21
|
-
toggleRow(id: string | number, index: number, shiftKey?: boolean): void;
|
|
23
|
+
toggleRow(id: string | number, index: number, shiftKey?: boolean, rows?: T[]): void;
|
|
24
|
+
private notifySelectionChange;
|
|
22
25
|
selectRange(startIndex: number, endIndex: number, rows?: T[]): void;
|
|
23
26
|
selectAll(rows: T[]): void;
|
|
24
|
-
deselectAll(): void;
|
|
27
|
+
deselectAll(rows?: T[]): void;
|
|
28
|
+
setRadioSelection(value: string | undefined, rows?: T[]): void;
|
|
25
29
|
isRowSelected(id: string | number): boolean;
|
|
26
30
|
isAllSelected(rows: T[]): boolean;
|
|
27
31
|
isSomeSelected(rows: T[]): boolean;
|
|
@@ -7,13 +7,16 @@ export class TableContext {
|
|
|
7
7
|
// Selection state
|
|
8
8
|
selectedIds = $state(new Set());
|
|
9
9
|
lastSelectedIndex = $state(null);
|
|
10
|
+
// Radio button group state (for single selection mode)
|
|
11
|
+
radioGroup = $state(undefined);
|
|
12
|
+
// Reactive selected count
|
|
13
|
+
selectedCount = $derived(this.selectedIds.size);
|
|
10
14
|
// Configuration
|
|
11
15
|
config;
|
|
12
16
|
constructor(config = {}) {
|
|
13
17
|
this.config = {
|
|
14
18
|
enableSorting: true,
|
|
15
|
-
|
|
16
|
-
selectionMode: 'multi',
|
|
19
|
+
selectionMode: 'none',
|
|
17
20
|
rowIdKey: 'id',
|
|
18
21
|
...config
|
|
19
22
|
};
|
|
@@ -56,25 +59,31 @@ export class TableContext {
|
|
|
56
59
|
};
|
|
57
60
|
}
|
|
58
61
|
// Selection methods
|
|
59
|
-
toggleRow(id, index, shiftKey = false) {
|
|
60
|
-
if (
|
|
62
|
+
toggleRow(id, index, shiftKey = false, rows) {
|
|
63
|
+
if (this.config.selectionMode === 'none')
|
|
61
64
|
return;
|
|
62
65
|
if (this.config.selectionMode === 'single') {
|
|
63
66
|
// Single selection mode
|
|
64
67
|
if (this.selectedIds.has(id)) {
|
|
65
68
|
this.selectedIds.delete(id);
|
|
69
|
+
this.radioGroup = undefined;
|
|
70
|
+
// Trigger reactivity
|
|
71
|
+
this.selectedIds = new Set(this.selectedIds);
|
|
66
72
|
}
|
|
67
73
|
else {
|
|
68
74
|
this.selectedIds.clear();
|
|
69
75
|
this.selectedIds.add(id);
|
|
76
|
+
this.radioGroup = String(id);
|
|
77
|
+
// Trigger reactivity
|
|
78
|
+
this.selectedIds = new Set(this.selectedIds);
|
|
70
79
|
}
|
|
71
80
|
this.lastSelectedIndex = index;
|
|
72
81
|
}
|
|
73
82
|
else {
|
|
74
83
|
// Multi selection mode
|
|
75
|
-
if (shiftKey && this.lastSelectedIndex !== null) {
|
|
84
|
+
if (shiftKey && this.lastSelectedIndex !== null && rows) {
|
|
76
85
|
// Range selection
|
|
77
|
-
this.selectRange(this.lastSelectedIndex, index);
|
|
86
|
+
this.selectRange(this.lastSelectedIndex, index, rows);
|
|
78
87
|
}
|
|
79
88
|
else {
|
|
80
89
|
// Toggle single row
|
|
@@ -84,10 +93,18 @@ export class TableContext {
|
|
|
84
93
|
else {
|
|
85
94
|
this.selectedIds.add(id);
|
|
86
95
|
}
|
|
96
|
+
// Trigger reactivity
|
|
97
|
+
this.selectedIds = new Set(this.selectedIds);
|
|
87
98
|
this.lastSelectedIndex = index;
|
|
88
99
|
}
|
|
89
100
|
}
|
|
90
|
-
this.
|
|
101
|
+
this.notifySelectionChange(rows);
|
|
102
|
+
}
|
|
103
|
+
notifySelectionChange(rows) {
|
|
104
|
+
if (this.config.onSelectionChange && rows) {
|
|
105
|
+
const selectedRows = this.getSelectedRows(rows);
|
|
106
|
+
this.config.onSelectionChange(selectedRows);
|
|
107
|
+
}
|
|
91
108
|
}
|
|
92
109
|
selectRange(startIndex, endIndex, rows) {
|
|
93
110
|
if (!rows)
|
|
@@ -103,23 +120,52 @@ export class TableContext {
|
|
|
103
120
|
}
|
|
104
121
|
}
|
|
105
122
|
}
|
|
106
|
-
|
|
123
|
+
// Trigger reactivity
|
|
124
|
+
this.selectedIds = new Set(this.selectedIds);
|
|
125
|
+
this.notifySelectionChange(rows);
|
|
107
126
|
}
|
|
108
127
|
selectAll(rows) {
|
|
109
|
-
if (
|
|
128
|
+
if (this.config.selectionMode === 'none')
|
|
110
129
|
return;
|
|
130
|
+
// Clear first, then add all to ensure proper state update
|
|
131
|
+
this.selectedIds.clear();
|
|
111
132
|
rows.forEach((row) => {
|
|
112
133
|
const id = row[this.config.rowIdKey];
|
|
113
134
|
if (id !== undefined) {
|
|
114
135
|
this.selectedIds.add(id);
|
|
115
136
|
}
|
|
116
137
|
});
|
|
117
|
-
|
|
138
|
+
// Trigger reactivity by reassigning (Svelte 5 tracks Set mutations, but ensure it's reactive)
|
|
139
|
+
this.selectedIds = new Set(this.selectedIds);
|
|
140
|
+
this.notifySelectionChange(rows);
|
|
118
141
|
}
|
|
119
|
-
deselectAll() {
|
|
142
|
+
deselectAll(rows) {
|
|
120
143
|
this.selectedIds.clear();
|
|
144
|
+
// Trigger reactivity by reassigning
|
|
145
|
+
this.selectedIds = new Set(this.selectedIds);
|
|
121
146
|
this.lastSelectedIndex = null;
|
|
122
|
-
this.
|
|
147
|
+
this.radioGroup = undefined;
|
|
148
|
+
this.notifySelectionChange(rows);
|
|
149
|
+
}
|
|
150
|
+
// Method to handle radio group changes (from radio button bindings)
|
|
151
|
+
setRadioSelection(value, rows) {
|
|
152
|
+
if (this.config.selectionMode !== 'single')
|
|
153
|
+
return;
|
|
154
|
+
if (value) {
|
|
155
|
+
this.selectedIds.clear();
|
|
156
|
+
this.selectedIds.add(value);
|
|
157
|
+
// Trigger reactivity
|
|
158
|
+
this.selectedIds = new Set(this.selectedIds);
|
|
159
|
+
this.radioGroup = value;
|
|
160
|
+
this.notifySelectionChange(rows);
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
this.selectedIds.clear();
|
|
164
|
+
// Trigger reactivity
|
|
165
|
+
this.selectedIds = new Set(this.selectedIds);
|
|
166
|
+
this.radioGroup = undefined;
|
|
167
|
+
this.notifySelectionChange(rows);
|
|
168
|
+
}
|
|
123
169
|
}
|
|
124
170
|
isRowSelected(id) {
|
|
125
171
|
return this.selectedIds.has(id);
|
|
@@ -140,6 +186,7 @@ export class TableContext {
|
|
|
140
186
|
return id !== undefined && this.selectedIds.has(id);
|
|
141
187
|
});
|
|
142
188
|
}
|
|
189
|
+
// selectedCount is now a reactive derived state, but keep this method for backwards compatibility
|
|
143
190
|
getSelectedCount() {
|
|
144
191
|
return this.selectedIds.size;
|
|
145
192
|
}
|
|
@@ -22,36 +22,11 @@
|
|
|
22
22
|
row && context?.config.rowIdKey ? (row[context.config.rowIdKey] as string | number) : undefined
|
|
23
23
|
);
|
|
24
24
|
|
|
25
|
-
let
|
|
26
|
-
|
|
27
|
-
let canSelect = $derived(selectable && context?.config.enableSelection && rowId !== undefined);
|
|
28
|
-
|
|
29
|
-
function handleClick(event: MouseEvent) {
|
|
30
|
-
if (canSelect && rowId !== undefined && rowIndex !== undefined) {
|
|
31
|
-
const shiftKey = event.shiftKey;
|
|
32
|
-
context?.toggleRow(rowId, rowIndex, shiftKey);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function handleKeyDown(event: KeyboardEvent) {
|
|
37
|
-
if (canSelect && (event.key === 'Enter' || event.key === ' ')) {
|
|
38
|
-
event.preventDefault();
|
|
39
|
-
if (rowId !== undefined && rowIndex !== undefined) {
|
|
40
|
-
context?.toggleRow(rowId, rowIndex, event.shiftKey);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
25
|
+
let selectionMode = $derived(context?.config.selectionMode ?? 'none');
|
|
26
|
+
let canSelect = $derived(selectable && selectionMode !== 'none' && rowId !== undefined);
|
|
44
27
|
</script>
|
|
45
28
|
|
|
46
|
-
<tr
|
|
47
|
-
class:selectable={canSelect}
|
|
48
|
-
class:selected={isSelected}
|
|
49
|
-
aria-selected={canSelect ? isSelected : undefined}
|
|
50
|
-
tabindex={canSelect ? 0 : undefined}
|
|
51
|
-
onclick={handleClick}
|
|
52
|
-
onkeydown={handleKeyDown}
|
|
53
|
-
role={canSelect ? 'row' : undefined}
|
|
54
|
-
>
|
|
29
|
+
<tr class:selectable={canSelect}>
|
|
55
30
|
{@render children?.()}
|
|
56
31
|
</tr>
|
|
57
32
|
|
|
@@ -69,8 +44,6 @@
|
|
|
69
44
|
}
|
|
70
45
|
|
|
71
46
|
tr.selectable {
|
|
72
|
-
cursor: pointer;
|
|
73
|
-
|
|
74
47
|
&:hover {
|
|
75
48
|
background-color: var(--table-row-hover-bg, rgba(0, 0, 0, 0.05));
|
|
76
49
|
}
|
|
@@ -80,13 +53,4 @@
|
|
|
80
53
|
outline-offset: -2px;
|
|
81
54
|
}
|
|
82
55
|
}
|
|
83
|
-
|
|
84
|
-
tr.selected {
|
|
85
|
-
background-color: var(--table-row-selected-bg, rgba(0, 102, 204, 0.1)) !important;
|
|
86
|
-
border-color: var(--table-row-selected-border, #0066cc);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
tr.selected.selectable:hover {
|
|
90
|
-
background-color: var(--table-row-selected-hover-bg, rgba(0, 102, 204, 0.15)) !important;
|
|
91
|
-
}
|
|
92
56
|
</style>
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { JsonObject } from '../types/data.js';
|
|
3
|
+
import { getTableContext } from './table-context.svelte.js';
|
|
4
|
+
import CheckBox from '../forms/check-box/check-box.svelte';
|
|
5
|
+
import RadioBox from '../forms/radio-group/radio-box.svelte';
|
|
6
|
+
|
|
7
|
+
let {
|
|
8
|
+
row = undefined,
|
|
9
|
+
rowIndex = undefined
|
|
10
|
+
}: {
|
|
11
|
+
row?: JsonObject;
|
|
12
|
+
rowIndex?: number;
|
|
13
|
+
} = $props();
|
|
14
|
+
|
|
15
|
+
const context = getTableContext();
|
|
16
|
+
|
|
17
|
+
if (!context) {
|
|
18
|
+
throw new Error('TableSelectionCell must be used within a Table component');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Get row ID for selection
|
|
22
|
+
let rowId = $derived(
|
|
23
|
+
row && context?.config.rowIdKey ? (row[context.config.rowIdKey] as string | number) : undefined
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
// Derive selection state from context - this is the source of truth
|
|
27
|
+
// Access selectedIds directly to ensure reactivity is tracked properly
|
|
28
|
+
let isSelectedInContext = $derived.by(() => {
|
|
29
|
+
if (rowId === undefined || !context) return false;
|
|
30
|
+
// Explicitly access selectedIds to set up reactivity tracking
|
|
31
|
+
const selectedIds = context.selectedIds;
|
|
32
|
+
return selectedIds.has(rowId);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
let selectionMode = $derived(context?.config.selectionMode ?? 'none');
|
|
36
|
+
|
|
37
|
+
// Local state for checkbox binding - synced from context
|
|
38
|
+
let localChecked = $state(false);
|
|
39
|
+
|
|
40
|
+
// Sync local state FROM context whenever context selection changes
|
|
41
|
+
// This ensures external changes (like "select all") update the checkbox
|
|
42
|
+
$effect(() => {
|
|
43
|
+
localChecked = isSelectedInContext;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// For radio buttons in single selection mode
|
|
47
|
+
let radioGroupLocal = $state<string | undefined>(undefined);
|
|
48
|
+
|
|
49
|
+
// Sync radio state FROM context
|
|
50
|
+
$effect(() => {
|
|
51
|
+
if (selectionMode === 'single' && context) {
|
|
52
|
+
radioGroupLocal = context.radioGroup;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Handle checkbox change - update context (source of truth)
|
|
57
|
+
function handleCheckboxChange(data: { isChecked: boolean; value: string }) {
|
|
58
|
+
if (rowId !== undefined && rowIndex !== undefined && context) {
|
|
59
|
+
const rows = context.config.rows ?? [];
|
|
60
|
+
// Toggle the row in context - context is the source of truth
|
|
61
|
+
context.toggleRow(rowId, rowIndex, false, rows);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Handle radio button change
|
|
66
|
+
function handleRadioChange(value: string) {
|
|
67
|
+
if (context && selectionMode === 'single') {
|
|
68
|
+
const rows = context.config.rows ?? [];
|
|
69
|
+
// Toggle: if already selected, deselect; otherwise select
|
|
70
|
+
if (context.radioGroup === value) {
|
|
71
|
+
context.setRadioSelection(undefined, rows);
|
|
72
|
+
} else {
|
|
73
|
+
context.setRadioSelection(value, rows);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Handle cell click - toggle selection when clicking anywhere in the cell
|
|
79
|
+
function handleCellClick(event: MouseEvent) {
|
|
80
|
+
const target = event.target as HTMLElement;
|
|
81
|
+
|
|
82
|
+
if (selectionMode === 'multi') {
|
|
83
|
+
// For checkboxes: skip if clicking on input to avoid double-toggle
|
|
84
|
+
if (target.tagName === 'INPUT') return;
|
|
85
|
+
handleCheckboxChange({ isChecked: !localChecked, value: '' });
|
|
86
|
+
} else if (selectionMode === 'single' && rowId !== undefined) {
|
|
87
|
+
// For radios: always handle the click ourselves to enable deselection
|
|
88
|
+
// Native radio buttons don't allow unchecking, so we take control
|
|
89
|
+
if (target.tagName === 'INPUT') {
|
|
90
|
+
event.preventDefault();
|
|
91
|
+
}
|
|
92
|
+
handleRadioChange(String(rowId));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
</script>
|
|
96
|
+
|
|
97
|
+
{#if selectionMode === 'multi'}
|
|
98
|
+
<td class="selection-cell" role="gridcell" onclick={handleCellClick}>
|
|
99
|
+
<CheckBox
|
|
100
|
+
bind:isChecked={localChecked}
|
|
101
|
+
onChange={handleCheckboxChange}
|
|
102
|
+
inline={true}
|
|
103
|
+
ariaLabel="Select row"
|
|
104
|
+
/>
|
|
105
|
+
</td>
|
|
106
|
+
{:else if selectionMode === 'single'}
|
|
107
|
+
<td class="selection-cell" role="gridcell" onclick={handleCellClick}>
|
|
108
|
+
<RadioBox
|
|
109
|
+
value={String(rowId ?? '')}
|
|
110
|
+
bind:group={radioGroupLocal}
|
|
111
|
+
onChange={handleRadioChange}
|
|
112
|
+
ariaLabel="Select row"
|
|
113
|
+
/>
|
|
114
|
+
</td>
|
|
115
|
+
{/if}
|
|
116
|
+
|
|
117
|
+
<style>.selection-cell {
|
|
118
|
+
text-align: center;
|
|
119
|
+
padding: 0.5rem 0.25rem;
|
|
120
|
+
width: 48px;
|
|
121
|
+
min-width: 48px;
|
|
122
|
+
vertical-align: middle;
|
|
123
|
+
cursor: pointer;
|
|
124
|
+
display: table-cell;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
:global(.selection-cell .checkbox-label),
|
|
128
|
+
:global(.selection-cell label) {
|
|
129
|
+
margin: 0 auto;
|
|
130
|
+
cursor: pointer;
|
|
131
|
+
display: flex;
|
|
132
|
+
align-items: center;
|
|
133
|
+
justify-content: center;
|
|
134
|
+
}</style>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { JsonObject } from '../types/data.js';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
row?: JsonObject;
|
|
4
|
+
rowIndex?: number;
|
|
5
|
+
};
|
|
6
|
+
declare const TableSelectionCell: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
7
|
+
type TableSelectionCell = ReturnType<typeof TableSelectionCell>;
|
|
8
|
+
export default TableSelectionCell;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getTableContext } from './table-context.svelte.js';
|
|
3
|
+
import CheckBox from '../forms/check-box/check-box.svelte';
|
|
4
|
+
|
|
5
|
+
const context = getTableContext();
|
|
6
|
+
|
|
7
|
+
if (!context) {
|
|
8
|
+
throw new Error('TableSelectionHeaderCell must be used within a Table component');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let selectionMode = $derived(context?.config.selectionMode ?? 'none');
|
|
12
|
+
let rows = $derived(context?.config.rows ?? []);
|
|
13
|
+
|
|
14
|
+
// Compute whether all rows are selected - source of truth from context
|
|
15
|
+
let isAllSelectedInContext = $derived.by(() => {
|
|
16
|
+
if (!context || !rows.length) return false;
|
|
17
|
+
// Access selectedIds directly to ensure reactivity is tracked
|
|
18
|
+
const selectedIds = context.selectedIds;
|
|
19
|
+
const rowIdKey = context.config.rowIdKey!;
|
|
20
|
+
// Check if every row's ID is in the selected set
|
|
21
|
+
return rows.every((row) => {
|
|
22
|
+
const id = row[rowIdKey] as string | number;
|
|
23
|
+
return id !== undefined && selectedIds.has(id);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Local state for checkbox binding - synced from context
|
|
28
|
+
let localChecked = $state(false);
|
|
29
|
+
|
|
30
|
+
// Sync local state FROM context whenever selection changes
|
|
31
|
+
// This ensures the header checkbox reflects the actual selection state
|
|
32
|
+
$effect(() => {
|
|
33
|
+
localChecked = isAllSelectedInContext;
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Handle "select all" checkbox change
|
|
37
|
+
function handleSelectAllChange(data: { isChecked: boolean; value: string }) {
|
|
38
|
+
if (!context) return;
|
|
39
|
+
const currentRows = rows;
|
|
40
|
+
if (!currentRows || currentRows.length === 0) return;
|
|
41
|
+
|
|
42
|
+
// data.isChecked is the NEW desired state from user interaction
|
|
43
|
+
if (data.isChecked) {
|
|
44
|
+
// User wants to select all
|
|
45
|
+
context.selectAll(currentRows);
|
|
46
|
+
} else {
|
|
47
|
+
// User wants to deselect all
|
|
48
|
+
context.deselectAll(currentRows);
|
|
49
|
+
}
|
|
50
|
+
// The effect will sync localChecked from context after this
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Handle header cell click - toggle select all when clicking anywhere in the cell
|
|
54
|
+
function handleCellClick(event: MouseEvent) {
|
|
55
|
+
// Don't double-toggle if the click was on the actual input element
|
|
56
|
+
const target = event.target as HTMLElement;
|
|
57
|
+
if (target.tagName === 'INPUT') return;
|
|
58
|
+
|
|
59
|
+
handleSelectAllChange({ isChecked: !localChecked, value: '' });
|
|
60
|
+
}
|
|
61
|
+
</script>
|
|
62
|
+
|
|
63
|
+
{#if selectionMode === 'multi'}
|
|
64
|
+
<th
|
|
65
|
+
class="selection-header"
|
|
66
|
+
style="width: 48px; min-width: 48px; text-align: center;"
|
|
67
|
+
onclick={handleCellClick}
|
|
68
|
+
>
|
|
69
|
+
<div class="header-content">
|
|
70
|
+
<CheckBox
|
|
71
|
+
bind:isChecked={localChecked}
|
|
72
|
+
onChange={handleSelectAllChange}
|
|
73
|
+
inline={true}
|
|
74
|
+
ariaLabel="Select all rows"
|
|
75
|
+
/>
|
|
76
|
+
</div>
|
|
77
|
+
</th>
|
|
78
|
+
{:else if selectionMode === 'single'}
|
|
79
|
+
<th class="selection-header" style="width: 48px; min-width: 48px; text-align: center;"></th>
|
|
80
|
+
{/if}
|
|
81
|
+
|
|
82
|
+
<style>.selection-header {
|
|
83
|
+
padding: 0.5rem 0.25rem !important;
|
|
84
|
+
vertical-align: middle;
|
|
85
|
+
text-align: center;
|
|
86
|
+
cursor: pointer;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.header-content {
|
|
90
|
+
display: flex;
|
|
91
|
+
align-items: center;
|
|
92
|
+
justify-content: center;
|
|
93
|
+
width: 100%;
|
|
94
|
+
height: 100%;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
:global(.header-content .checkbox-label) {
|
|
98
|
+
margin: 0;
|
|
99
|
+
cursor: pointer;
|
|
100
|
+
display: flex;
|
|
101
|
+
align-items: center;
|
|
102
|
+
justify-content: center;
|
|
103
|
+
}</style>
|
package/dist/tables/table.svelte
CHANGED
|
@@ -6,42 +6,48 @@
|
|
|
6
6
|
|
|
7
7
|
let {
|
|
8
8
|
children,
|
|
9
|
+
rows = [],
|
|
9
10
|
enableSorting = true,
|
|
10
|
-
|
|
11
|
-
selectionMode = 'multi',
|
|
11
|
+
selectionMode = 'none',
|
|
12
12
|
rowIdKey = 'id',
|
|
13
13
|
stickyHeader = false,
|
|
14
14
|
onSort = undefined,
|
|
15
15
|
onSelectionChange = undefined
|
|
16
16
|
}: {
|
|
17
17
|
children?: Snippet;
|
|
18
|
+
rows?: JsonObject[];
|
|
18
19
|
enableSorting?: boolean;
|
|
19
|
-
|
|
20
|
-
selectionMode?: 'single' | 'multi';
|
|
20
|
+
selectionMode?: 'none' | 'single' | 'multi';
|
|
21
21
|
rowIdKey?: string;
|
|
22
22
|
stickyHeader?: boolean;
|
|
23
23
|
onSort?: (column: string, direction: 'asc' | 'desc') => void;
|
|
24
|
-
onSelectionChange?: (
|
|
24
|
+
onSelectionChange?: (selectedRows: JsonObject[]) => void;
|
|
25
25
|
} = $props();
|
|
26
26
|
|
|
27
27
|
// Create table context for child components
|
|
28
28
|
// Using untrack() to indicate we intentionally want non-reactive initial values
|
|
29
29
|
const config: TableContextConfig<JsonObject> = {
|
|
30
30
|
enableSorting: untrack(() => enableSorting),
|
|
31
|
-
enableSelection: untrack(() => enableSelection),
|
|
32
31
|
selectionMode: untrack(() => selectionMode),
|
|
33
32
|
rowIdKey: untrack(() => rowIdKey),
|
|
34
33
|
onSort: untrack(() => onSort),
|
|
35
|
-
onSelectionChange: untrack(() => onSelectionChange)
|
|
34
|
+
onSelectionChange: untrack(() => onSelectionChange),
|
|
35
|
+
rows: untrack(() => rows)
|
|
36
36
|
};
|
|
37
37
|
|
|
38
|
-
createTableContext(config);
|
|
38
|
+
const context = createTableContext(config);
|
|
39
|
+
|
|
40
|
+
// Keep context.config.rows updated reactively when rows prop changes
|
|
41
|
+
// This is essential for selection features to work correctly
|
|
42
|
+
$effect(() => {
|
|
43
|
+
context.config.rows = rows;
|
|
44
|
+
});
|
|
39
45
|
</script>
|
|
40
46
|
|
|
41
47
|
<table
|
|
42
48
|
class:sticky-header={stickyHeader}
|
|
43
49
|
role="grid"
|
|
44
|
-
aria-rowcount={
|
|
50
|
+
aria-rowcount={undefined}
|
|
45
51
|
aria-colcount={undefined}
|
|
46
52
|
>
|
|
47
53
|
{@render children?.()}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { JsonObject } from '../types/data.js';
|
|
2
3
|
type $$ComponentProps = {
|
|
3
4
|
children?: Snippet;
|
|
5
|
+
rows?: JsonObject[];
|
|
4
6
|
enableSorting?: boolean;
|
|
5
|
-
|
|
6
|
-
selectionMode?: 'single' | 'multi';
|
|
7
|
+
selectionMode?: 'none' | 'single' | 'multi';
|
|
7
8
|
rowIdKey?: string;
|
|
8
9
|
stickyHeader?: boolean;
|
|
9
10
|
onSort?: (column: string, direction: 'asc' | 'desc') => void;
|
|
10
|
-
onSelectionChange?: (
|
|
11
|
+
onSelectionChange?: (selectedRows: JsonObject[]) => void;
|
|
11
12
|
};
|
|
12
13
|
declare const Table: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
13
14
|
type Table = ReturnType<typeof Table>;
|