feat(VirtualList): VirtualList now supports pagination, it loads batches when user scrolls near the end of current batch
This commit is contained in:
@@ -59,6 +59,11 @@ export class UnifiedFontStore extends BaseFontStore<ProxyFontsParams> {
|
|||||||
} | null
|
} | null
|
||||||
>(null);
|
>(null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accumulated fonts from all pages (for infinite scroll)
|
||||||
|
*/
|
||||||
|
#accumulatedFonts = $state<UnifiedFont[]>([]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pagination metadata (derived from proxy API response)
|
* Pagination metadata (derived from proxy API response)
|
||||||
*/
|
*/
|
||||||
@@ -84,8 +89,53 @@ export class UnifiedFontStore extends BaseFontStore<ProxyFontsParams> {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Track previous filter params to detect changes and reset pagination
|
||||||
|
*/
|
||||||
|
#previousFilterParams = $state<string>('');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleanup function for the filter tracking effect
|
||||||
|
*/
|
||||||
|
#filterCleanup: (() => void) | null = null;
|
||||||
|
|
||||||
constructor(initialParams: ProxyFontsParams = {}) {
|
constructor(initialParams: ProxyFontsParams = {}) {
|
||||||
super(initialParams);
|
super(initialParams);
|
||||||
|
|
||||||
|
// Track filter params (excluding pagination params)
|
||||||
|
// Wrapped in $effect.root() to prevent effect_orphan error
|
||||||
|
this.#filterCleanup = $effect.root(() => {
|
||||||
|
$effect(() => {
|
||||||
|
const filterParams = JSON.stringify({
|
||||||
|
provider: this.params.provider,
|
||||||
|
category: this.params.category,
|
||||||
|
subset: this.params.subset,
|
||||||
|
q: this.params.q,
|
||||||
|
});
|
||||||
|
|
||||||
|
// If filters changed, reset offset to 0
|
||||||
|
if (filterParams !== this.#previousFilterParams) {
|
||||||
|
if (this.#previousFilterParams && this.params.offset !== 0) {
|
||||||
|
this.setParams({ offset: 0 });
|
||||||
|
}
|
||||||
|
this.#previousFilterParams = filterParams;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up both parent and child effects
|
||||||
|
*/
|
||||||
|
destroy() {
|
||||||
|
// Call parent cleanup (TanStack observer effect)
|
||||||
|
super.destroy();
|
||||||
|
|
||||||
|
// Call filter tracking effect cleanup
|
||||||
|
if (this.#filterCleanup) {
|
||||||
|
this.#filterCleanup();
|
||||||
|
this.#filterCleanup = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -136,17 +186,25 @@ export class UnifiedFontStore extends BaseFontStore<ProxyFontsParams> {
|
|||||||
offset: response.offset ?? this.params.offset ?? 0,
|
offset: response.offset ?? this.params.offset ?? 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Accumulate fonts for infinite scroll
|
||||||
|
if (params.offset === 0) {
|
||||||
|
// Reset when starting from beginning (new search/filter)
|
||||||
|
this.#accumulatedFonts = response.fonts;
|
||||||
|
} else {
|
||||||
|
// Append new fonts to existing ones
|
||||||
|
this.#accumulatedFonts = [...this.#accumulatedFonts, ...response.fonts];
|
||||||
|
}
|
||||||
|
|
||||||
return response.fonts;
|
return response.fonts;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Getters (proxied from BaseFontStore) ---
|
// --- Getters (proxied from BaseFontStore) ---
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all fonts from current query result
|
* Get all accumulated fonts (for infinite scroll)
|
||||||
*/
|
*/
|
||||||
get fonts(): UnifiedFont[] {
|
get fonts(): UnifiedFont[] {
|
||||||
// The result.data is UnifiedFont[] (from TanStack Query)
|
return this.#accumulatedFonts;
|
||||||
return (this.result.data as UnifiedFont[] | undefined) ?? [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -288,5 +346,9 @@ export function createUnifiedFontStore(params: ProxyFontsParams = {}) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Singleton instance for global use
|
* Singleton instance for global use
|
||||||
|
* Initialized with a default limit to prevent fetching all fonts at once
|
||||||
*/
|
*/
|
||||||
export const unifiedFontStore = new UnifiedFontStore();
|
export const unifiedFontStore = new UnifiedFontStore({
|
||||||
|
limit: 50,
|
||||||
|
offset: 0,
|
||||||
|
});
|
||||||
|
|||||||
@@ -15,7 +15,10 @@ import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
|||||||
* Load more fonts by moving to the next page
|
* Load more fonts by moving to the next page
|
||||||
*/
|
*/
|
||||||
function loadMore() {
|
function loadMore() {
|
||||||
if (!unifiedFontStore.pagination.hasMore || unifiedFontStore.isFetching) {
|
if (
|
||||||
|
!unifiedFontStore.pagination.hasMore
|
||||||
|
|| unifiedFontStore.isFetching
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
unifiedFontStore.nextPage();
|
unifiedFontStore.nextPage();
|
||||||
@@ -24,15 +27,14 @@ function loadMore() {
|
|||||||
/**
|
/**
|
||||||
* Handle scroll near bottom - auto-load next page
|
* Handle scroll near bottom - auto-load next page
|
||||||
*
|
*
|
||||||
* Triggered when the user scrolls within 5 items of the end of the list.
|
* Triggered by VirtualList when the user scrolls within 5 items of the end
|
||||||
* Only fetches if there are more pages available and not already fetching.
|
* of the loaded items. Only fetches if there are more pages available.
|
||||||
*/
|
*/
|
||||||
function handleNearBottom(lastVisibleIndex: number) {
|
function handleNearBottom(_lastVisibleIndex: number) {
|
||||||
const { hasMore, total } = unifiedFontStore.pagination;
|
const { hasMore } = unifiedFontStore.pagination;
|
||||||
const itemsRemaining = total - lastVisibleIndex;
|
|
||||||
|
|
||||||
// Only trigger if within 5 items of the end, more data exists, and not already fetching
|
// VirtualList already checks if we're near the bottom of loaded items
|
||||||
if (itemsRemaining <= 5 && hasMore && !unifiedFontStore.isFetching) {
|
if (hasMore && !unifiedFontStore.isFetching) {
|
||||||
loadMore();
|
loadMore();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -47,13 +49,8 @@ const displayRange = $derived.by(() => {
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if unifiedFontStore.pagination.total > 0 && !unifiedFontStore.isLoading}
|
{#if unifiedFontStore.isFetching || unifiedFontStore.isLoading}
|
||||||
<div class="text-sm text-muted-foreground px-2 py-2">
|
<span class="ml-2 text-xs text-muted-foreground/70">(Loading...)</span>
|
||||||
{displayRange}
|
|
||||||
{#if unifiedFontStore.isFetching}
|
|
||||||
<span class="ml-2 text-xs text-muted-foreground/70">(Loading...)</span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<FontVirtualList
|
<FontVirtualList
|
||||||
|
|||||||
@@ -84,7 +84,9 @@ interface Props {
|
|||||||
*
|
*
|
||||||
* @template T - The type of items in the list
|
* @template T - The type of items in the list
|
||||||
*/
|
*/
|
||||||
children: Snippet<[{ item: T; index: number; isVisible: boolean; proximity: number }]>;
|
children: Snippet<
|
||||||
|
[{ item: T; index: number; isVisible: boolean; proximity: number }]
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
@@ -99,7 +101,7 @@ let {
|
|||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
const virtualizer = createVirtualizer(() => ({
|
const virtualizer = createVirtualizer(() => ({
|
||||||
count: total,
|
count: items.length,
|
||||||
data: items,
|
data: items,
|
||||||
estimateSize: typeof itemHeight === 'function' ? itemHeight : () => itemHeight,
|
estimateSize: typeof itemHeight === 'function' ? itemHeight : () => itemHeight,
|
||||||
overscan,
|
overscan,
|
||||||
@@ -109,10 +111,11 @@ $effect(() => {
|
|||||||
const visibleItems = virtualizer.items.map(item => items[item.index]);
|
const visibleItems = virtualizer.items.map(item => items[item.index]);
|
||||||
onVisibleItemsChange?.(visibleItems);
|
onVisibleItemsChange?.(visibleItems);
|
||||||
|
|
||||||
// Trigger onNearBottom when user scrolls near the end (within 5 items)
|
// Trigger onNearBottom when user scrolls near the end of loaded items (within 5 items)
|
||||||
if (virtualizer.items.length > 0 && onNearBottom) {
|
if (virtualizer.items.length > 0 && onNearBottom) {
|
||||||
const lastVisibleItem = virtualizer.items[virtualizer.items.length - 1];
|
const lastVisibleItem = virtualizer.items[virtualizer.items.length - 1];
|
||||||
const itemsRemaining = total - lastVisibleItem.index;
|
// Compare against loaded items length, not total
|
||||||
|
const itemsRemaining = items.length - lastVisibleItem.index;
|
||||||
|
|
||||||
if (itemsRemaining <= 5) {
|
if (itemsRemaining <= 5) {
|
||||||
onNearBottom(lastVisibleItem.index);
|
onNearBottom(lastVisibleItem.index);
|
||||||
|
|||||||
Reference in New Issue
Block a user