@classic-homes/theme-svelte 1.25.1 → 1.27.1
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/lib/components/AlertDialog.svelte +14 -6
- package/dist/lib/components/AlertDialog.svelte.d.ts +8 -2
- package/dist/lib/components/Badge.svelte +6 -2
- package/dist/lib/components/CodeField.svelte +24 -8
- package/dist/lib/components/CodeField.svelte.d.ts +8 -0
- package/dist/lib/components/Combobox.svelte +25 -111
- package/dist/lib/components/DataPanel.svelte +31 -24
- package/dist/lib/components/DataPanelHeader.svelte +66 -26
- package/dist/lib/components/DataTable.svelte +303 -138
- package/dist/lib/components/DataTable.svelte.d.ts +37 -13
- package/dist/lib/components/DateTimePicker.svelte +159 -34
- package/dist/lib/components/Dialog.svelte +14 -6
- package/dist/lib/components/Dialog.svelte.d.ts +8 -2
- package/dist/lib/components/FileUpload.svelte +23 -6
- package/dist/lib/components/FileUpload.svelte.d.ts +7 -2
- package/dist/lib/components/MultiSelect.svelte +28 -155
- package/dist/lib/components/NumberInput.svelte +28 -110
- package/dist/lib/components/OTPInput.svelte +73 -85
- package/dist/lib/components/Select.svelte +24 -29
- package/dist/lib/components/Signature.svelte +80 -48
- package/dist/lib/components/Signature.svelte.d.ts +6 -1
- package/dist/lib/components/SkipLink.svelte +7 -2
- package/dist/lib/components/SkipLink.svelte.d.ts +2 -0
- package/dist/lib/components/Slider.svelte +4 -4
- package/dist/lib/components/Toast.svelte +11 -1
- package/dist/lib/components/ToastContainer.svelte +38 -24
- package/dist/lib/components/Tooltip.svelte +19 -6
- package/dist/lib/components/Tooltip.svelte.d.ts +8 -2
- package/dist/lib/components/data-table/DataTableColumnFilter.svelte +3 -1
- package/dist/lib/components/data-table/DataTableEditableCell.svelte +25 -3
- package/dist/lib/components/data-table/DataTableFacetedFilter.svelte +50 -40
- package/dist/lib/components/data-table/DataTableFacetedFilter.svelte.d.ts +0 -10
- package/dist/lib/components/data-table/DataTableVirtual.svelte +109 -57
- package/dist/lib/components/data-table/DataTableVirtual.svelte.d.ts +2 -0
- package/dist/lib/components/data-table/createDataTable.svelte.d.ts +26 -4
- package/dist/lib/components/data-table/createDataTable.svelte.js +97 -40
- package/dist/lib/components/data-table/index.d.ts +1 -1
- package/dist/lib/components/data-table/types.d.ts +14 -4
- package/dist/lib/components/data-table/types.js +5 -1
- package/dist/lib/components/internal/TriggerChild.svelte +60 -0
- package/dist/lib/components/internal/TriggerChild.svelte.d.ts +32 -0
- package/dist/lib/components/layout/Sidebar.svelte +33 -3
- package/dist/lib/components/layout/Sidebar.svelte.d.ts +1 -1
- package/dist/lib/components/layout/sidebar/SidebarFlyout.svelte +6 -1
- package/dist/lib/components/layout/sidebar/SidebarNavItem.svelte +6 -1
- package/dist/lib/components/layout/sidebar/SidebarSection.svelte +6 -1
- package/dist/lib/components/media/Audio.svelte +16 -0
- package/dist/lib/components/media/Lightbox.svelte +44 -9
- package/dist/lib/components/media/Lightbox.svelte.d.ts +9 -2
- package/dist/lib/components/media/MediaCarousel.svelte +19 -0
- package/dist/lib/components/media/MediaEmbed.svelte +32 -1
- package/dist/lib/components/media/Video.svelte +16 -0
- package/dist/lib/components/media/internal/MediaControls.svelte +9 -1
- package/dist/lib/components/notifications/NotificationBadge.svelte +1 -1
- package/dist/lib/components/notifications/NotificationItem.svelte +19 -28
- package/dist/lib/composables/useEscapeKey.svelte.d.ts +4 -0
- package/dist/lib/composables/useEscapeKey.svelte.js +15 -0
- package/dist/lib/composables/useHlsSource.svelte.js +31 -4
- package/dist/lib/composables/useMediaPlayer.svelte.js +38 -9
- package/dist/lib/composables/useMediaZoom.svelte.js +32 -0
- package/dist/lib/composables/usePersistedForm.svelte.d.ts +6 -2
- package/dist/lib/composables/usePersistedForm.svelte.js +7 -7
- package/dist/lib/composables/useReducedMotion.svelte.js +6 -1
- package/dist/lib/composables/useRovingTabindex.svelte.js +12 -0
- package/dist/lib/index.d.ts +1 -1
- package/dist/lib/stores/toast.svelte.d.ts +13 -0
- package/dist/lib/stores/toast.svelte.js +36 -5
- package/dist/lib/utils/media.d.ts +7 -0
- package/dist/lib/utils/media.js +18 -3
- package/package.json +1 -1
|
@@ -18,8 +18,7 @@
|
|
|
18
18
|
* Create the table with `enablePagination: false` so `table.rows` holds every
|
|
19
19
|
* row rather than one page.
|
|
20
20
|
*/
|
|
21
|
-
import type { DataTableInstance
|
|
22
|
-
import type { DataTableColumn } from './types.js';
|
|
21
|
+
import type { DataTableInstance } from './createDataTable.svelte.js';
|
|
23
22
|
import Checkbox from '../Checkbox.svelte';
|
|
24
23
|
import { cn } from '../../utils.js';
|
|
25
24
|
import { getAlignClass, getColumnValue, getColumnWidthStyle, getAriaSortValue } from './utils.js';
|
|
@@ -37,6 +36,8 @@
|
|
|
37
36
|
onRowClick?: (row: T) => void;
|
|
38
37
|
/** Whether row selection is enabled */
|
|
39
38
|
enableRowSelection?: boolean;
|
|
39
|
+
/** Show the select-all checkbox in the header (default: true) */
|
|
40
|
+
enableSelectAll?: boolean;
|
|
40
41
|
/** Additional CSS classes */
|
|
41
42
|
class?: string;
|
|
42
43
|
}
|
|
@@ -48,16 +49,32 @@
|
|
|
48
49
|
height = '500px',
|
|
49
50
|
onRowClick,
|
|
50
51
|
enableRowSelection = false,
|
|
52
|
+
enableSelectAll = true,
|
|
51
53
|
class: className,
|
|
52
54
|
}: Props = $props();
|
|
53
55
|
|
|
54
56
|
let containerRef: HTMLDivElement | null = $state(null);
|
|
55
57
|
let scrollTop = $state(0);
|
|
58
|
+
let measuredHeight = $state(0);
|
|
59
|
+
|
|
60
|
+
// The rendered height of the scroll body. `height` can be any CSS length
|
|
61
|
+
// (80vh, 30rem, calc(...)), so measure it rather than parse it; the parse
|
|
62
|
+
// is only a fallback before layout (or where there is none, e.g. SSR).
|
|
63
|
+
$effect(() => {
|
|
64
|
+
const node = containerRef;
|
|
65
|
+
if (!node) return;
|
|
66
|
+
measuredHeight = node.clientHeight;
|
|
67
|
+
if (typeof ResizeObserver === 'undefined') return;
|
|
68
|
+
const observer = new ResizeObserver(() => {
|
|
69
|
+
measuredHeight = node.clientHeight;
|
|
70
|
+
});
|
|
71
|
+
observer.observe(node);
|
|
72
|
+
return () => observer.disconnect();
|
|
73
|
+
});
|
|
56
74
|
|
|
57
|
-
// Parse container height to number for calculations
|
|
58
75
|
const containerHeightPx = $derived.by(() => {
|
|
59
|
-
if (
|
|
60
|
-
const num = parseInt(height, 10);
|
|
76
|
+
if (measuredHeight > 0) return measuredHeight;
|
|
77
|
+
const num = parseInt(height ?? '', 10);
|
|
61
78
|
return isNaN(num) ? 500 : num;
|
|
62
79
|
});
|
|
63
80
|
|
|
@@ -104,63 +121,96 @@
|
|
|
104
121
|
if (!sortable) return;
|
|
105
122
|
table.toggleSort(columnId);
|
|
106
123
|
}
|
|
124
|
+
|
|
125
|
+
function handleHeaderKeydown(e: KeyboardEvent, columnId: string, sortable: boolean | undefined) {
|
|
126
|
+
if (!sortable || (e.key !== 'Enter' && e.key !== ' ')) return;
|
|
127
|
+
// Space would otherwise scroll the page
|
|
128
|
+
e.preventDefault();
|
|
129
|
+
handleHeaderClick(columnId, sortable);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Ignore keys aimed at the row's own controls (the checkbox handles its
|
|
133
|
+
// own Enter/Space).
|
|
134
|
+
function handleRowKeydown(e: KeyboardEvent, row: T) {
|
|
135
|
+
if (e.target !== e.currentTarget) return;
|
|
136
|
+
if (e.key !== 'Enter' && e.key !== ' ') return;
|
|
137
|
+
e.preventDefault();
|
|
138
|
+
handleRowClick(row);
|
|
139
|
+
}
|
|
107
140
|
</script>
|
|
108
141
|
|
|
109
|
-
|
|
142
|
+
<!-- Only a window of rows is in the DOM, so aria-rowcount/aria-rowindex
|
|
143
|
+
give assistive tech the real size and position (header is row 1). -->
|
|
144
|
+
<div
|
|
145
|
+
class={cn('rounded-md border', className)}
|
|
146
|
+
role="table"
|
|
147
|
+
aria-rowcount={allRows.length + 1}
|
|
148
|
+
aria-colcount={table.columns.length + (enableRowSelection ? 1 : 0)}
|
|
149
|
+
>
|
|
110
150
|
<!-- Sticky Header -->
|
|
111
|
-
<div
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
)}
|
|
130
|
-
style={column.width ? `width: ${column.width}` : 'flex: 1'}
|
|
131
|
-
onclick={() => handleHeaderClick(column.id, column.sortable)}
|
|
132
|
-
onkeydown={(e) =>
|
|
133
|
-
(e.key === 'Enter' || e.key === ' ') && handleHeaderClick(column.id, column.sortable)}
|
|
134
|
-
role="columnheader"
|
|
135
|
-
tabindex={column.sortable ? 0 : -1}
|
|
136
|
-
aria-sort={getAriaSortValue(isSorted, table.sortDirection)}
|
|
137
|
-
>
|
|
138
|
-
<span class="flex items-center gap-1">
|
|
139
|
-
{column.header}
|
|
140
|
-
{#if isSorted}
|
|
141
|
-
<svg
|
|
142
|
-
class={cn('h-4 w-4', table.sortDirection === 'desc' && 'rotate-180')}
|
|
143
|
-
fill="none"
|
|
144
|
-
viewBox="0 0 24 24"
|
|
145
|
-
stroke="currentColor"
|
|
146
|
-
aria-hidden="true"
|
|
147
|
-
>
|
|
148
|
-
<path
|
|
149
|
-
stroke-linecap="round"
|
|
150
|
-
stroke-linejoin="round"
|
|
151
|
-
stroke-width="2"
|
|
152
|
-
d="M5 15l7-7 7 7"
|
|
153
|
-
/>
|
|
154
|
-
</svg>
|
|
151
|
+
<div role="rowgroup">
|
|
152
|
+
<div
|
|
153
|
+
class="flex items-center border-b bg-muted/50"
|
|
154
|
+
style:height="{rowHeight}px"
|
|
155
|
+
role="row"
|
|
156
|
+
aria-rowindex={1}
|
|
157
|
+
>
|
|
158
|
+
{#if enableRowSelection}
|
|
159
|
+
<div class="flex-shrink-0 px-4" style="width: 48px" role="columnheader">
|
|
160
|
+
{#if enableSelectAll}
|
|
161
|
+
<Checkbox
|
|
162
|
+
checked={table.isAllSelected}
|
|
163
|
+
indeterminate={table.isSomeSelected}
|
|
164
|
+
onCheckedChange={() => table.toggleAllRows()}
|
|
165
|
+
aria-label="Select all rows"
|
|
166
|
+
/>
|
|
167
|
+
{:else}
|
|
168
|
+
<span class="sr-only">Select</span>
|
|
155
169
|
{/if}
|
|
156
|
-
</
|
|
157
|
-
|
|
158
|
-
|
|
170
|
+
</div>
|
|
171
|
+
{/if}
|
|
172
|
+
{#each table.columns as column (column.id)}
|
|
173
|
+
{@const isSorted = table.sortColumn === column.id}
|
|
174
|
+
<div
|
|
175
|
+
class={cn(
|
|
176
|
+
'flex-shrink-0 px-4 font-medium text-muted-foreground',
|
|
177
|
+
getAlignClass(column.align),
|
|
178
|
+
column.sortable && 'cursor-pointer select-none hover:text-foreground'
|
|
179
|
+
)}
|
|
180
|
+
style={column.width ? `width: ${column.width}` : 'flex: 1'}
|
|
181
|
+
onclick={() => handleHeaderClick(column.id, column.sortable)}
|
|
182
|
+
onkeydown={(e) => handleHeaderKeydown(e, column.id, column.sortable)}
|
|
183
|
+
role="columnheader"
|
|
184
|
+
tabindex={column.sortable ? 0 : undefined}
|
|
185
|
+
aria-sort={getAriaSortValue(isSorted, table.sortDirection)}
|
|
186
|
+
>
|
|
187
|
+
<span class="flex items-center gap-1">
|
|
188
|
+
{column.header}
|
|
189
|
+
{#if isSorted}
|
|
190
|
+
<svg
|
|
191
|
+
class={cn('h-4 w-4', table.sortDirection === 'desc' && 'rotate-180')}
|
|
192
|
+
fill="none"
|
|
193
|
+
viewBox="0 0 24 24"
|
|
194
|
+
stroke="currentColor"
|
|
195
|
+
aria-hidden="true"
|
|
196
|
+
>
|
|
197
|
+
<path
|
|
198
|
+
stroke-linecap="round"
|
|
199
|
+
stroke-linejoin="round"
|
|
200
|
+
stroke-width="2"
|
|
201
|
+
d="M5 15l7-7 7 7"
|
|
202
|
+
/>
|
|
203
|
+
</svg>
|
|
204
|
+
{/if}
|
|
205
|
+
</span>
|
|
206
|
+
</div>
|
|
207
|
+
{/each}
|
|
208
|
+
</div>
|
|
159
209
|
</div>
|
|
160
210
|
|
|
161
211
|
<!-- Virtualized Body -->
|
|
162
212
|
<div bind:this={containerRef} class="overflow-auto" style:height onscroll={handleScroll}>
|
|
163
|
-
<div style:height="{totalHeight}px" style:position="relative">
|
|
213
|
+
<div style:height="{totalHeight}px" style:position="relative" role="rowgroup">
|
|
164
214
|
{#each visibleRows as { row, index, offset } (row.id)}
|
|
165
215
|
<div
|
|
166
216
|
class={cn(
|
|
@@ -171,12 +221,14 @@
|
|
|
171
221
|
style:height="{rowHeight}px"
|
|
172
222
|
style:transform="translateY({offset}px)"
|
|
173
223
|
onclick={() => handleRowClick(row.original)}
|
|
174
|
-
onkeydown={(e) => e
|
|
224
|
+
onkeydown={onRowClick ? (e) => handleRowKeydown(e, row.original) : undefined}
|
|
175
225
|
role="row"
|
|
176
|
-
|
|
226
|
+
aria-rowindex={index + 2}
|
|
227
|
+
aria-selected={enableRowSelection ? row.isSelected : undefined}
|
|
228
|
+
tabindex={onRowClick ? 0 : undefined}
|
|
177
229
|
>
|
|
178
230
|
{#if enableRowSelection}
|
|
179
|
-
<div class="flex-shrink-0 px-4" style="width: 48px">
|
|
231
|
+
<div class="flex-shrink-0 px-4" style="width: 48px" role="cell">
|
|
180
232
|
<Checkbox
|
|
181
233
|
checked={row.isSelected}
|
|
182
234
|
onCheckedChange={() => table.toggleRowSelection(row.id)}
|
|
@@ -32,6 +32,8 @@ declare function $$render<T extends object>(): {
|
|
|
32
32
|
onRowClick?: (row: T) => void;
|
|
33
33
|
/** Whether row selection is enabled */
|
|
34
34
|
enableRowSelection?: boolean;
|
|
35
|
+
/** Show the select-all checkbox in the header (default: true) */
|
|
36
|
+
enableSelectAll?: boolean;
|
|
35
37
|
/** Additional CSS classes */
|
|
36
38
|
class?: string;
|
|
37
39
|
};
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* - Standard Svelte patterns for debugging
|
|
24
24
|
* - Excellent performance characteristics
|
|
25
25
|
*/
|
|
26
|
-
import type { DataTableColumn } from './types.js';
|
|
26
|
+
import type { ColumnFilterValue, ColumnFiltersState, DataTableColumn } from './types.js';
|
|
27
27
|
/**
|
|
28
28
|
* Options for creating a native data table instance.
|
|
29
29
|
*/
|
|
@@ -48,6 +48,13 @@ export interface DataTableOptions<T> {
|
|
|
48
48
|
initialHiddenColumns?: string[];
|
|
49
49
|
/** Initially selected rows (by row ID) */
|
|
50
50
|
initialSelectedRows?: string[];
|
|
51
|
+
/** Initial column filters */
|
|
52
|
+
initialColumnFilters?: ColumnFiltersState;
|
|
53
|
+
/**
|
|
54
|
+
* Allow more than one selected row (default true). When false, selecting a
|
|
55
|
+
* row replaces the previous selection.
|
|
56
|
+
*/
|
|
57
|
+
enableMultiRowSelection?: boolean;
|
|
51
58
|
/** Use server-side sorting (skip client-side sort) */
|
|
52
59
|
manualSorting?: boolean;
|
|
53
60
|
/** Use server-side filtering (skip client-side filter) */
|
|
@@ -66,7 +73,7 @@ export interface DataTableOptions<T> {
|
|
|
66
73
|
/** Callback when global filter changes */
|
|
67
74
|
onGlobalFilterChange?: (filter: string) => void;
|
|
68
75
|
/** Callback when column filters change */
|
|
69
|
-
onColumnFiltersChange?: (filters:
|
|
76
|
+
onColumnFiltersChange?: (filters: ColumnFiltersState) => void;
|
|
70
77
|
/** Callback when pagination changes */
|
|
71
78
|
onPaginationChange?: (state: {
|
|
72
79
|
pageIndex: number;
|
|
@@ -77,6 +84,20 @@ export interface DataTableOptions<T> {
|
|
|
77
84
|
/** Callback when column visibility changes */
|
|
78
85
|
onColumnVisibilityChange?: (hiddenColumns: Set<string>) => void;
|
|
79
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* State that {@link DataTableInstance.syncState} can set directly, e.g. to
|
|
89
|
+
* mirror a parent's controlled props.
|
|
90
|
+
*/
|
|
91
|
+
export interface DataTableSyncState {
|
|
92
|
+
sortColumn?: string | null;
|
|
93
|
+
sortDirection?: 'asc' | 'desc';
|
|
94
|
+
globalFilter?: string;
|
|
95
|
+
columnFilters?: ColumnFiltersState;
|
|
96
|
+
pageIndex?: number;
|
|
97
|
+
pageSize?: number;
|
|
98
|
+
selectedRows?: Set<string>;
|
|
99
|
+
hiddenColumns?: Set<string>;
|
|
100
|
+
}
|
|
80
101
|
/**
|
|
81
102
|
* Row object with computed properties.
|
|
82
103
|
*/
|
|
@@ -127,10 +148,11 @@ export declare function createDataTable<T extends object>(optionsFn: () => DataT
|
|
|
127
148
|
setSortColumn: (columnId: string | null) => void;
|
|
128
149
|
setSortDirection: (direction: "asc" | "desc") => void;
|
|
129
150
|
readonly globalFilter: string;
|
|
130
|
-
readonly columnFilters:
|
|
151
|
+
readonly columnFilters: ColumnFiltersState;
|
|
131
152
|
setGlobalFilter: (value: string) => void;
|
|
132
|
-
setColumnFilter: (columnId: string, value:
|
|
153
|
+
setColumnFilter: (columnId: string, value: ColumnFilterValue) => void;
|
|
133
154
|
clearColumnFilters: () => void;
|
|
155
|
+
syncState: (state: DataTableSyncState) => void;
|
|
134
156
|
readonly pageIndex: number;
|
|
135
157
|
readonly pageSize: number;
|
|
136
158
|
readonly totalRows: number;
|
|
@@ -82,6 +82,8 @@ export function createDataTable(optionsFn) {
|
|
|
82
82
|
hiddenColumns = new Set(options.initialHiddenColumns);
|
|
83
83
|
if (options.initialSelectedRows)
|
|
84
84
|
selectedRows = new Set(options.initialSelectedRows);
|
|
85
|
+
if (options.initialColumnFilters)
|
|
86
|
+
columnFilters = new Map(options.initialColumnFilters);
|
|
85
87
|
initialized = true;
|
|
86
88
|
}
|
|
87
89
|
});
|
|
@@ -93,30 +95,27 @@ export function createDataTable(optionsFn) {
|
|
|
93
95
|
if (options.manualFiltering)
|
|
94
96
|
return data;
|
|
95
97
|
let result = data;
|
|
96
|
-
// Global filter
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
result = result.filter((row) => visibleColumns.some((col) =>
|
|
100
|
-
const value = getColumnValue(row, col);
|
|
101
|
-
return String(value ?? '')
|
|
102
|
-
.toLowerCase()
|
|
103
|
-
.includes(search);
|
|
104
|
-
}));
|
|
98
|
+
// Global filter: matches what the cell shows as well as its raw value
|
|
99
|
+
const globalSearch = globalFilter.trim().toLowerCase();
|
|
100
|
+
if (globalSearch) {
|
|
101
|
+
result = result.filter((row) => visibleColumns.some((col) => cellMatchesText(row, col, globalSearch)));
|
|
105
102
|
}
|
|
106
103
|
// Column filters
|
|
107
104
|
for (const [columnId, filterValue] of columnFilters) {
|
|
108
|
-
if (
|
|
105
|
+
if (isEmptyFilter(filterValue))
|
|
109
106
|
continue;
|
|
110
107
|
const column = columns.find((c) => c.id === columnId);
|
|
111
108
|
if (!column)
|
|
112
109
|
continue;
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
110
|
+
if (Array.isArray(filterValue)) {
|
|
111
|
+
// Faceted filter: exact membership, so 'active' never matches 'inactive'
|
|
112
|
+
const accepted = new Set(filterValue);
|
|
113
|
+
result = result.filter((row) => accepted.has(String(getColumnValue(row, column) ?? '')));
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
const search = filterValue.trim().toLowerCase();
|
|
117
|
+
result = result.filter((row) => cellMatchesText(row, column, search));
|
|
118
|
+
}
|
|
120
119
|
}
|
|
121
120
|
return result;
|
|
122
121
|
});
|
|
@@ -149,7 +148,8 @@ export function createDataTable(optionsFn) {
|
|
|
149
148
|
cmp = aVal.getTime() - bVal.getTime();
|
|
150
149
|
}
|
|
151
150
|
else {
|
|
152
|
-
|
|
151
|
+
// numeric: "9" sorts before "10", not after "100"
|
|
152
|
+
cmp = String(aVal).localeCompare(String(bVal), undefined, { numeric: true });
|
|
153
153
|
}
|
|
154
154
|
return sortDirection === 'asc' ? cmp : -cmp;
|
|
155
155
|
});
|
|
@@ -191,6 +191,38 @@ export function createDataTable(optionsFn) {
|
|
|
191
191
|
const isAllSelected = $derived(rows.length > 0 && rows.every((row) => selectedRows.has(row.id)));
|
|
192
192
|
const isSomeSelected = $derived(rows.some((row) => selectedRows.has(row.id)) && !isAllSelected);
|
|
193
193
|
// === ACTIONS ===
|
|
194
|
+
/**
|
|
195
|
+
* Set state directly without firing any `on*Change` callback. Used to
|
|
196
|
+
* mirror controlled props into the table; user-driven changes should use
|
|
197
|
+
* the setters below.
|
|
198
|
+
*/
|
|
199
|
+
function syncState(state) {
|
|
200
|
+
if (state.sortColumn !== undefined)
|
|
201
|
+
sortColumn = state.sortColumn;
|
|
202
|
+
if (state.sortDirection !== undefined)
|
|
203
|
+
sortDirection = state.sortDirection;
|
|
204
|
+
if (state.globalFilter !== undefined)
|
|
205
|
+
globalFilter = state.globalFilter;
|
|
206
|
+
if (state.columnFilters !== undefined)
|
|
207
|
+
columnFilters = new Map(state.columnFilters);
|
|
208
|
+
if (state.pageIndex !== undefined)
|
|
209
|
+
pageIndex = state.pageIndex;
|
|
210
|
+
if (state.pageSize !== undefined)
|
|
211
|
+
pageSize = state.pageSize;
|
|
212
|
+
if (state.selectedRows !== undefined)
|
|
213
|
+
selectedRows = new Set(state.selectedRows);
|
|
214
|
+
if (state.hiddenColumns !== undefined)
|
|
215
|
+
hiddenColumns = new Set(state.hiddenColumns);
|
|
216
|
+
}
|
|
217
|
+
/** Filtering moves back to the first page; tell a controlled pager so. */
|
|
218
|
+
function resetPageForFilter() {
|
|
219
|
+
if (options.manualPagination)
|
|
220
|
+
return;
|
|
221
|
+
const moved = pageIndex !== 0;
|
|
222
|
+
pageIndex = 0;
|
|
223
|
+
if (moved)
|
|
224
|
+
options.onPaginationChange?.({ pageIndex: 0, pageSize });
|
|
225
|
+
}
|
|
194
226
|
function setSort(columnId) {
|
|
195
227
|
if (columnId === sortColumn) {
|
|
196
228
|
// Toggle direction or clear
|
|
@@ -224,23 +256,23 @@ export function createDataTable(optionsFn) {
|
|
|
224
256
|
}
|
|
225
257
|
function setGlobalFilter(value) {
|
|
226
258
|
globalFilter = value;
|
|
227
|
-
|
|
228
|
-
pageIndex = 0; // Reset to first page
|
|
229
|
-
}
|
|
259
|
+
resetPageForFilter();
|
|
230
260
|
options.onGlobalFilterChange?.(value);
|
|
231
261
|
}
|
|
262
|
+
/**
|
|
263
|
+
* Set one column's filter. A string is a text filter; an array is a set of
|
|
264
|
+
* accepted values. An empty string or array clears the filter.
|
|
265
|
+
*/
|
|
232
266
|
function setColumnFilter(columnId, value) {
|
|
233
267
|
const newFilters = new Map(columnFilters);
|
|
234
|
-
if (value
|
|
235
|
-
newFilters.
|
|
268
|
+
if (isEmptyFilter(value)) {
|
|
269
|
+
newFilters.delete(columnId);
|
|
236
270
|
}
|
|
237
271
|
else {
|
|
238
|
-
newFilters.
|
|
272
|
+
newFilters.set(columnId, Array.isArray(value) ? [...value] : value);
|
|
239
273
|
}
|
|
240
274
|
columnFilters = newFilters;
|
|
241
|
-
|
|
242
|
-
pageIndex = 0;
|
|
243
|
-
}
|
|
275
|
+
resetPageForFilter();
|
|
244
276
|
options.onColumnFiltersChange?.(columnFilters);
|
|
245
277
|
}
|
|
246
278
|
function clearColumnFilters() {
|
|
@@ -279,9 +311,10 @@ export function createDataTable(optionsFn) {
|
|
|
279
311
|
pageIndex = Math.max(0, totalPages - 1);
|
|
280
312
|
options.onPaginationChange?.({ pageIndex: Math.max(0, totalPages - 1), pageSize });
|
|
281
313
|
}
|
|
314
|
+
const multiSelect = $derived(options.enableMultiRowSelection !== false);
|
|
282
315
|
function toggleRowSelection(rowId) {
|
|
283
|
-
const newSelection = new Set(selectedRows);
|
|
284
|
-
if (
|
|
316
|
+
const newSelection = multiSelect ? new Set(selectedRows) : new Set();
|
|
317
|
+
if (selectedRows.has(rowId)) {
|
|
285
318
|
newSelection.delete(rowId);
|
|
286
319
|
}
|
|
287
320
|
else {
|
|
@@ -291,7 +324,7 @@ export function createDataTable(optionsFn) {
|
|
|
291
324
|
options.onRowSelectionChange?.(selectedRows);
|
|
292
325
|
}
|
|
293
326
|
function setRowSelection(rowId, selected) {
|
|
294
|
-
const newSelection = new Set(selectedRows);
|
|
327
|
+
const newSelection = selected && !multiSelect ? new Set() : new Set(selectedRows);
|
|
295
328
|
if (selected) {
|
|
296
329
|
newSelection.add(rowId);
|
|
297
330
|
}
|
|
@@ -302,6 +335,9 @@ export function createDataTable(optionsFn) {
|
|
|
302
335
|
options.onRowSelectionChange?.(selectedRows);
|
|
303
336
|
}
|
|
304
337
|
function toggleAllRows() {
|
|
338
|
+
// Selecting every row makes no sense when only one may be selected
|
|
339
|
+
if (!multiSelect && !isAllSelected)
|
|
340
|
+
return;
|
|
305
341
|
if (isAllSelected) {
|
|
306
342
|
// Deselect all visible rows
|
|
307
343
|
const newSelection = new Set(selectedRows);
|
|
@@ -317,6 +353,8 @@ export function createDataTable(optionsFn) {
|
|
|
317
353
|
options.onRowSelectionChange?.(selectedRows);
|
|
318
354
|
}
|
|
319
355
|
function selectAllRows() {
|
|
356
|
+
if (!multiSelect)
|
|
357
|
+
return;
|
|
320
358
|
const newSelection = new Set(selectedRows);
|
|
321
359
|
rows.forEach((row) => newSelection.add(row.id));
|
|
322
360
|
selectedRows = newSelection;
|
|
@@ -357,16 +395,7 @@ export function createDataTable(optionsFn) {
|
|
|
357
395
|
return getColumnValue(row, column);
|
|
358
396
|
}
|
|
359
397
|
function getFormattedCellValue(row, column) {
|
|
360
|
-
|
|
361
|
-
if (column.format) {
|
|
362
|
-
try {
|
|
363
|
-
return column.format(value, row);
|
|
364
|
-
}
|
|
365
|
-
catch {
|
|
366
|
-
return String(value ?? '');
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
return String(value ?? '');
|
|
398
|
+
return formatValue(row, column);
|
|
370
399
|
}
|
|
371
400
|
// === RETURN API ===
|
|
372
401
|
return {
|
|
@@ -404,6 +433,8 @@ export function createDataTable(optionsFn) {
|
|
|
404
433
|
setGlobalFilter,
|
|
405
434
|
setColumnFilter,
|
|
406
435
|
clearColumnFilters,
|
|
436
|
+
// Controlled-state mirroring
|
|
437
|
+
syncState,
|
|
407
438
|
// Pagination
|
|
408
439
|
get pageIndex() {
|
|
409
440
|
return clampedPageIndex;
|
|
@@ -470,3 +501,29 @@ function getColumnValue(row, column) {
|
|
|
470
501
|
}
|
|
471
502
|
return row[column.accessor];
|
|
472
503
|
}
|
|
504
|
+
function formatValue(row, column) {
|
|
505
|
+
const value = getColumnValue(row, column);
|
|
506
|
+
if (column.format) {
|
|
507
|
+
try {
|
|
508
|
+
return column.format(value, row);
|
|
509
|
+
}
|
|
510
|
+
catch {
|
|
511
|
+
return String(value ?? '');
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return String(value ?? '');
|
|
515
|
+
}
|
|
516
|
+
/** Case-insensitive substring match against the raw and the formatted value. */
|
|
517
|
+
function cellMatchesText(row, column, search) {
|
|
518
|
+
if (String(getColumnValue(row, column) ?? '')
|
|
519
|
+
.toLowerCase()
|
|
520
|
+
.includes(search)) {
|
|
521
|
+
return true;
|
|
522
|
+
}
|
|
523
|
+
return !!column.format && formatValue(row, column).toLowerCase().includes(search);
|
|
524
|
+
}
|
|
525
|
+
function isEmptyFilter(value) {
|
|
526
|
+
if (value === undefined)
|
|
527
|
+
return true;
|
|
528
|
+
return Array.isArray(value) ? value.length === 0 : !value.trim();
|
|
529
|
+
}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* - `utils`: Shared utilities for formatting, alignment, etc.
|
|
15
15
|
*/
|
|
16
16
|
export { createDataTable, type DataTableOptions, type DataTableRow, type DataTableInstance, } from './createDataTable.svelte.js';
|
|
17
|
-
export { type DataTableColumn, type DataTableFilterFn, type DataTableFilterOption, type ColumnMeta, type BaseColumnMeta, type DataColumnMeta, type FilterableColumnMeta, type SelectionColumnMeta, type RowSelectionState, type ColumnVisibilityState, type ColumnFiltersState, type PaginationState, type LegacyRowSelectionState, type LegacyVisibilityState, type LegacyColumnFiltersState, legacyToSetSelection, setToLegacySelection, legacyToSetVisibility, setToLegacyVisibility, legacyToMapFilters, mapToLegacyFilters, type DataTableProps, type DataTableCoreProps, type DataTableSortingProps, type DataTableSelectionProps, type DataTableVisibilityProps, type DataTableFilteringProps, type DataTablePaginationProps, type DataTableVirtualizationProps, type DataTableEditingProps, type DataTableInteractionProps, type InferRowType, type FormatResult, } from './types.js';
|
|
17
|
+
export { type DataTableColumn, type DataTableFilterFn, type DataTableFilterOption, type ColumnMeta, type BaseColumnMeta, type DataColumnMeta, type FilterableColumnMeta, type SelectionColumnMeta, type RowSelectionState, type ColumnVisibilityState, type ColumnFiltersState, type ColumnFilterValue, type PaginationState, type LegacyRowSelectionState, type LegacyVisibilityState, type LegacyColumnFiltersState, legacyToSetSelection, setToLegacySelection, legacyToSetVisibility, setToLegacyVisibility, legacyToMapFilters, mapToLegacyFilters, type DataTableProps, type DataTableCoreProps, type DataTableSortingProps, type DataTableSelectionProps, type DataTableVisibilityProps, type DataTableFilteringProps, type DataTablePaginationProps, type DataTableVirtualizationProps, type DataTableEditingProps, type DataTableInteractionProps, type InferRowType, type FormatResult, } from './types.js';
|
|
18
18
|
export { getAlignClass, getColumnAlign, formatCellValue, formatCellValueSafe, getColumnValue, getColumnWidthStyle, getAriaSortValue, defaultGetRowId, debounce, } from './utils.js';
|
|
19
19
|
export { default as DataTableToolbar } from './DataTableToolbar.svelte';
|
|
20
20
|
export { default as DataTableSearch } from './DataTableSearch.svelte';
|
|
@@ -93,10 +93,19 @@ export declare function legacyToSetVisibility(legacy: LegacyVisibilityState, all
|
|
|
93
93
|
* Convert Set-based visibility state to legacy format.
|
|
94
94
|
*/
|
|
95
95
|
export declare function setToLegacyVisibility(hiddenSet: ColumnVisibilityState, allColumnIds: string[]): LegacyVisibilityState;
|
|
96
|
+
/**
|
|
97
|
+
* A single column filter value.
|
|
98
|
+
*
|
|
99
|
+
* - `string`: a text filter, matched as a case-insensitive substring of the
|
|
100
|
+
* cell's raw or formatted value.
|
|
101
|
+
* - `string[]`: a set of accepted values (faceted filter), matched exactly
|
|
102
|
+
* against the cell's raw value. An empty array filters nothing.
|
|
103
|
+
*/
|
|
104
|
+
export type ColumnFilterValue = string | string[];
|
|
96
105
|
/**
|
|
97
106
|
* Column filters state is a Map from column ID to filter value.
|
|
98
107
|
*/
|
|
99
|
-
export type ColumnFiltersState = Map<string,
|
|
108
|
+
export type ColumnFiltersState = Map<string, ColumnFilterValue>;
|
|
100
109
|
/**
|
|
101
110
|
* Legacy column filter format (array of {id, value} objects).
|
|
102
111
|
*/
|
|
@@ -152,8 +161,9 @@ export interface DataTableSortingProps {
|
|
|
152
161
|
/**
|
|
153
162
|
* Callback when sort changes.
|
|
154
163
|
* When provided, enables server-side sorting (manual mode).
|
|
164
|
+
* Called with `column: null` when the sort is cleared.
|
|
155
165
|
*/
|
|
156
|
-
onSort?: (column: string, direction: 'asc' | 'desc') => void;
|
|
166
|
+
onSort?: (column: string | null, direction: 'asc' | 'desc') => void;
|
|
157
167
|
}
|
|
158
168
|
/**
|
|
159
169
|
* Props for row selection functionality.
|
|
@@ -236,9 +246,9 @@ export interface DataTableFilteringProps {
|
|
|
236
246
|
/** Callback when global filter changes */
|
|
237
247
|
onGlobalFilterChange?: (filter: string) => void;
|
|
238
248
|
/** Controlled column filters */
|
|
239
|
-
columnFilters?:
|
|
249
|
+
columnFilters?: ColumnFiltersState;
|
|
240
250
|
/** Callback when column filters change */
|
|
241
|
-
onColumnFiltersChange?: (filters:
|
|
251
|
+
onColumnFiltersChange?: (filters: ColumnFiltersState) => void;
|
|
242
252
|
/**
|
|
243
253
|
* Use server-side filtering (manual mode).
|
|
244
254
|
*
|
|
@@ -51,7 +51,11 @@ export function setToLegacyVisibility(hiddenSet, allColumnIds) {
|
|
|
51
51
|
export function legacyToMapFilters(legacy) {
|
|
52
52
|
const map = new Map();
|
|
53
53
|
for (const { id, value } of legacy) {
|
|
54
|
-
if (value
|
|
54
|
+
if (Array.isArray(value)) {
|
|
55
|
+
if (value.length > 0)
|
|
56
|
+
map.set(id, value.map(String));
|
|
57
|
+
}
|
|
58
|
+
else if (value !== undefined && value !== null && value !== '') {
|
|
55
59
|
map.set(id, String(value));
|
|
56
60
|
}
|
|
57
61
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
Renders a consumer's trigger snippet inside a bits-ui `child` snippet.
|
|
4
|
+
|
|
5
|
+
The snippet receives the trigger props (`{ props }`) and should spread them
|
|
6
|
+
on its own focusable element (`<Button {...props}>`), so that element is the
|
|
7
|
+
real trigger: it carries aria-expanded/aria-describedby and gets focus back
|
|
8
|
+
when a dialog closes.
|
|
9
|
+
|
|
10
|
+
Snippets written before the props were passed ignore them. Those are
|
|
11
|
+
detected after mount (no element inside carries the trigger id) and the
|
|
12
|
+
props move to a wrapping span, the old behaviour. `onfallback` tells the
|
|
13
|
+
parent so it can change the trigger id, which makes bits-ui look the
|
|
14
|
+
trigger node up again.
|
|
15
|
+
-->
|
|
16
|
+
<script lang="ts" module>
|
|
17
|
+
/** What a trigger snippet receives: spread `props` on the focusable element */
|
|
18
|
+
export type TriggerSnippetProps = { props: Record<string, unknown> };
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<script lang="ts">
|
|
22
|
+
import type { Snippet } from 'svelte';
|
|
23
|
+
import { cn } from '../../utils.js';
|
|
24
|
+
|
|
25
|
+
interface Props {
|
|
26
|
+
/** The props bits-ui hands to its `child` snippet */
|
|
27
|
+
props: Record<string, unknown>;
|
|
28
|
+
/** The consumer's trigger snippet */
|
|
29
|
+
trigger: Snippet<[TriggerSnippetProps]>;
|
|
30
|
+
/** Class for the fallback wrapper span */
|
|
31
|
+
fallbackClass?: string;
|
|
32
|
+
/** Called once when the snippet ignored the props */
|
|
33
|
+
onfallback?: () => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let { props, trigger, fallbackClass, onfallback }: Props = $props();
|
|
37
|
+
|
|
38
|
+
let fallback = $state(false);
|
|
39
|
+
let probe = $state<HTMLElement | null>(null);
|
|
40
|
+
|
|
41
|
+
$effect(() => {
|
|
42
|
+
if (fallback || !probe) return;
|
|
43
|
+
const id = props.id;
|
|
44
|
+
const spread = [...probe.querySelectorAll('[id]')].some((el) => el.id === id);
|
|
45
|
+
if (!spread) {
|
|
46
|
+
fallback = true;
|
|
47
|
+
onfallback?.();
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
{#if fallback}
|
|
53
|
+
<span {...props} class={cn(fallbackClass, props.class as string | undefined)}>
|
|
54
|
+
{@render trigger({ props: {} })}
|
|
55
|
+
</span>
|
|
56
|
+
{:else}
|
|
57
|
+
<span bind:this={probe} class="contents">
|
|
58
|
+
{@render trigger({ props })}
|
|
59
|
+
</span>
|
|
60
|
+
{/if}
|