137 lines
3.5 KiB
Svelte
137 lines
3.5 KiB
Svelte
<script lang="ts" generics="TData, TValue">
|
|
import { onMount } from 'svelte';
|
|
import { type ColumnDef, getCoreRowModel } from '@tanstack/table-core';
|
|
import { createSvelteTable, FlexRender } from '$lib/components/ui/data-table/index.js';
|
|
import * as Table from '$lib/components/ui/table/index.js';
|
|
|
|
type DataTableProps<TData, TValue> = {
|
|
columns: ColumnDef<TData, TValue>[];
|
|
data: TData[];
|
|
loading?: boolean;
|
|
hasMore?: boolean;
|
|
loadMore?: () => void;
|
|
// Props para selección
|
|
selectedId?: number | null;
|
|
onRowClick?: (row: TData) => void;
|
|
};
|
|
|
|
let {
|
|
data,
|
|
columns,
|
|
loading = false,
|
|
hasMore = false,
|
|
loadMore,
|
|
selectedId = null,
|
|
onRowClick
|
|
}: DataTableProps<TData, TValue> = $props();
|
|
|
|
const table = createSvelteTable({
|
|
get data() {
|
|
return data;
|
|
},
|
|
columns,
|
|
getCoreRowModel: getCoreRowModel(),
|
|
getRowId: (row: any) => row.id?.toString(),
|
|
state: {
|
|
get rowSelection() {
|
|
return selectedId ? { [selectedId]: true } : {};
|
|
}
|
|
},
|
|
enableRowSelection: true,
|
|
enableMultiRowSelection: false
|
|
});
|
|
|
|
let scrollContainer = $state<HTMLDivElement>();
|
|
let loadingTrigger = $state<HTMLDivElement>();
|
|
|
|
onMount(() => {
|
|
if (!loadMore) return;
|
|
|
|
const observer = new IntersectionObserver(
|
|
(entries) => {
|
|
const [entry] = entries;
|
|
if (entry.isIntersecting && hasMore && !loading) {
|
|
loadMore();
|
|
}
|
|
},
|
|
{
|
|
root: null, // Relative to viewport if scrollContainer is used as max-h div
|
|
threshold: 0.1
|
|
}
|
|
);
|
|
|
|
if (loadingTrigger) {
|
|
observer.observe(loadingTrigger);
|
|
}
|
|
|
|
return () => {
|
|
observer.disconnect();
|
|
};
|
|
});
|
|
</script>
|
|
|
|
<div class="w-full">
|
|
<div class="max-h-[600px] overflow-y-auto rounded-md border" bind:this={scrollContainer}>
|
|
<Table.Root>
|
|
<Table.Header class="sticky top-0 z-10 bg-background">
|
|
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
|
<Table.Row>
|
|
{#each headerGroup.headers as header (header.id)}
|
|
<Table.Head>
|
|
{#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
|
|
data-state={row.getIsSelected() && 'selected'}
|
|
class="cursor-pointer transition-colors {row.getIsSelected()
|
|
? 'bg-muted'
|
|
: 'hover:bg-muted/50'}"
|
|
onclick={() => onRowClick && onRowClick(row.original)}
|
|
>
|
|
{#each row.getVisibleCells() as cell (cell.id)}
|
|
<Table.Cell>
|
|
<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">
|
|
No hay resultados.
|
|
</Table.Cell>
|
|
</Table.Row>
|
|
{/each}
|
|
|
|
{#if hasMore}
|
|
<Table.Row>
|
|
<Table.Cell colspan={columns.length} class="h-20 text-center">
|
|
<div bind:this={loadingTrigger}>
|
|
{#if loading}
|
|
<div class="flex items-center justify-center gap-2">
|
|
<div
|
|
class="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent"
|
|
></div>
|
|
<span class="text-sm text-muted-foreground">Cargando más...</span>
|
|
</div>
|
|
{:else}
|
|
<div class="text-sm text-muted-foreground">Desplázate para cargar más</div>
|
|
{/if}
|
|
</div>
|
|
</Table.Cell>
|
|
</Table.Row>
|
|
{/if}
|
|
</Table.Body>
|
|
</Table.Root>
|
|
</div>
|
|
</div>
|