Files
amqtrain/src/lib/components/ui/VirtualList.svelte
Yuri Tatishchev 63145c128e feat(queue): add scroll to currently playing button
Adds a scrollToIndex method to VirtualList and a locate button in the
Queue header that scrolls to center the currently playing track.
2026-02-15 22:14:36 -08:00

110 lines
2.5 KiB
Svelte

<!--
Generic fixed-height virtual list.
Usage:
<VirtualList items={myArray} itemHeight={64} overscan={5}>
{#snippet row({ item, index })}
<div>…</div>
{/snippet}
</VirtualList>
-->
<script lang="ts" generics="T">
import type { Snippet } from "svelte";
type Props = {
items: T[];
itemHeight: number;
overscan?: number;
class?: string;
row: Snippet<[{ item: T; index: number }]>;
empty?: Snippet;
key?: (item: T, index: number) => unknown;
};
let {
items,
itemHeight,
overscan = 5,
class: className = "",
row,
empty,
key,
}: Props = $props();
let containerEl = $state<HTMLDivElement | null>(null);
let scrollTop = $state(0);
let containerHeight = $state(0);
const totalHeight = $derived(items.length * itemHeight);
const startIndex = $derived(
Math.max(0, Math.floor(scrollTop / itemHeight) - overscan),
);
const endIndex = $derived(
Math.min(
items.length,
Math.ceil((scrollTop + containerHeight) / itemHeight) + overscan,
),
);
const visibleItems = $derived(
items.slice(startIndex, endIndex).map((item, i) => ({
item,
index: startIndex + i,
})),
);
function onScroll(e: Event) {
scrollTop = (e.target as HTMLDivElement).scrollTop;
}
export function scrollToIndex(index: number) {
if (!containerEl) return;
containerEl.scrollTop = Math.max(
0,
index * itemHeight - containerHeight / 2 + itemHeight / 2,
);
}
$effect(() => {
if (!containerEl) return;
const ro = new ResizeObserver((entries) => {
for (const entry of entries) {
containerHeight = entry.contentRect.height;
}
});
ro.observe(containerEl);
return () => ro.disconnect();
});
</script>
<div
class="virtual-list-container {className}"
bind:this={containerEl}
onscroll={onScroll}
>
{#if items.length === 0}
{@render empty?.()}
{:else}
<div
class="virtual-list-sentinel"
style="height: {totalHeight}px; position: relative;"
>
{#each visibleItems as entry (key ? key(entry.item, entry.index) : entry.index)}
<div
class="virtual-list-item"
style="position: absolute; top: {entry.index *
itemHeight}px; left: 0; right: 0; height: {itemHeight}px;"
>
{@render row(entry)}
</div>
{/each}
</div>
{/if}
</div>
<style>
.virtual-list-container {
overflow-y: auto;
flex: 1;
}
</style>