File size: 4,970 Bytes
91677d6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c92e42a
 
 
 
 
 
 
91677d6
 
 
 
 
 
 
 
c92e42a
 
 
 
 
 
91677d6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0f00bbc
91677d6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0f00bbc
95e3d2a
91677d6
 
 
 
 
95e3d2a
 
8899818
95e3d2a
91677d6
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
<script lang="ts" generics="TData">
	import {
		type ColumnDef,
		type ColumnFiltersState,
		type PaginationState,
		type SortingState,
		type VisibilityState,
		getCoreRowModel,
		getFilteredRowModel,
		getPaginationRowModel,
		getSortedRowModel
	} from '@tanstack/table-core';
	import { createSvelteTable, FlexRender } from '$lib/components/ui/data-table';
	import * as Table from '$lib/components/ui/table';
	import DataTableToolbar from './data-table-toolbar.svelte';
	import DataTablePagination from './data-table-pagination.svelte';
	import { goto } from '$app/navigation';
	import { cn } from '$lib/utils';
	import { untrack } from 'svelte';

	interface Props {
		data: TData[];
		columns: ColumnDef<TData>[];
		searchPlaceholder?: string;
		mapOptions?: string[];
		getRowHref?: (row: TData) => string;
		emptyMessage?: string;
		// Bindable state so the parent route can persist it across navigations
		// via SvelteKit's snapshot API.
		pagination?: PaginationState;
		sorting?: SortingState;
		columnFilters?: ColumnFiltersState;
		columnVisibility?: VisibilityState;
		globalFilter?: string;
	}

	let {
		data,
		columns,
		searchPlaceholder,
		mapOptions = [],
		getRowHref,
		emptyMessage = 'No results.',
		pagination = $bindable<PaginationState>({ pageIndex: 0, pageSize: 25 }),
		sorting = $bindable<SortingState>([]),
		columnFilters = $bindable<ColumnFiltersState>([]),
		columnVisibility = $bindable<VisibilityState>({}),
		globalFilter = $bindable<string>('')
	}: Props = $props();

	// Reset to first page whenever filters change so users don't end up on an
	// empty page after narrowing the result set.
	$effect(() => {
		void globalFilter;
		void columnFilters;
		untrack(() => {
			if (pagination.pageIndex !== 0) {
				pagination = { ...pagination, pageIndex: 0 };
			}
		});
	});

	const table = createSvelteTable<TData>({
		get data() {
			return data;
		},
		get columns() {
			return columns;
		},
		state: {
			get pagination() {
				return pagination;
			},
			get sorting() {
				return sorting;
			},
			get columnFilters() {
				return columnFilters;
			},
			get columnVisibility() {
				return columnVisibility;
			},
			get globalFilter() {
				return globalFilter;
			}
		},
		globalFilterFn: 'includesString',
		getCoreRowModel: getCoreRowModel(),
		getSortedRowModel: getSortedRowModel(),
		getFilteredRowModel: getFilteredRowModel(),
		getPaginationRowModel: getPaginationRowModel(),
		onPaginationChange: (u) => {
			pagination = typeof u === 'function' ? u(pagination) : u;
		},
		onSortingChange: (u) => {
			sorting = typeof u === 'function' ? u(sorting) : u;
		},
		onColumnFiltersChange: (u) => {
			columnFilters = typeof u === 'function' ? u(columnFilters) : u;
		},
		onColumnVisibilityChange: (u) => {
			columnVisibility = typeof u === 'function' ? u(columnVisibility) : u;
		},
		onGlobalFilterChange: (u) => {
			globalFilter = typeof u === 'function' ? u(globalFilter) : u;
		}
	});

	function handleRowClick(row: TData, e: MouseEvent | KeyboardEvent) {
		if (!getRowHref) return;
		const href = getRowHref(row);
		if (e instanceof MouseEvent && (e.metaKey || e.ctrlKey || e.button === 1)) {
			window.open(href, '_blank', 'noopener,noreferrer');
			return;
		}
		goto(href);
	}
</script>

<div>
	<DataTableToolbar {table} {searchPlaceholder} {mapOptions} />

	<div class="rounded-md border">
		<Table.Root>
			<Table.Header>
				{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
					<Table.Row class="bg-muted/40 hover:bg-muted/40">
						{#each headerGroup.headers as header (header.id)}
							<Table.Head class={cn('h-9 text-xs', header.column.columnDef.meta?.headClass)}>
								{#if !header.isPlaceholder}
									<FlexRender
										content={header.column.columnDef.header}
										context={header.getContext()}
									/>
								{/if}
							</Table.Head>
						{/each}
					</Table.Row>
				{/each}
			</Table.Header>
			<Table.Body>
				{#each table.getRowModel().rows as row (row.id)}
					<Table.Row
						class={cn(getRowHref && 'cursor-pointer')}
						onclick={(e: MouseEvent) => handleRowClick(row.original, e)}
						onkeydown={(e: KeyboardEvent) => {
							if (e.key === 'Enter' || e.key === ' ') {
								e.preventDefault();
								handleRowClick(row.original, e);
							}
						}}
						tabindex={getRowHref ? 0 : undefined}
						role={getRowHref ? 'link' : undefined}
					>
						{#each row.getVisibleCells() as cell (cell.id)}
							<Table.Cell class={cn('py-2 align-middle', cell.column.columnDef.meta?.cellClass)}>
								<FlexRender content={cell.column.columnDef.cell} context={cell.getContext()} />
							</Table.Cell>
						{/each}
					</Table.Row>
				{:else}
					<Table.Row>
						<Table.Cell
							colspan={columns.length}
							class="h-24 text-center text-sm text-muted-foreground"
						>
							{emptyMessage}
						</Table.Cell>
					</Table.Row>
				{/each}
			</Table.Body>
		</Table.Root>
	</div>

	<DataTablePagination {table} />
</div>