94 lines
2.3 KiB
TypeScript
94 lines
2.3 KiB
TypeScript
import { computed, onBeforeUnmount, onMounted, ref, unref, watch, type ComputedRef, type Ref } from 'vue';
|
|
|
|
type ReactiveValue<T> = Ref<T> | ComputedRef<T>;
|
|
|
|
type UseIncrementalListOptions = {
|
|
pageSize?: number;
|
|
enabled?: ReactiveValue<boolean>;
|
|
resetKeys?: Array<ReactiveValue<unknown> | unknown>;
|
|
};
|
|
|
|
export function useIncrementalList<T>(
|
|
items: ReactiveValue<T[]>,
|
|
options: UseIncrementalListOptions = {},
|
|
) {
|
|
const pageSize = options.pageSize ?? 24;
|
|
const enabled = computed(() => unref(options.enabled ?? true));
|
|
const visibleCount = ref(pageSize);
|
|
const loadMoreSentinel = ref<HTMLElement | null>(null);
|
|
let observer: IntersectionObserver | null = null;
|
|
|
|
const visibleItems = computed(() => (
|
|
enabled.value
|
|
? items.value.slice(0, visibleCount.value)
|
|
: items.value
|
|
));
|
|
|
|
const canLoadMore = computed(() => (
|
|
enabled.value && visibleCount.value < items.value.length
|
|
));
|
|
|
|
const remainingCount = computed(() => Math.max(items.value.length - visibleCount.value, 0));
|
|
|
|
function loadMore() {
|
|
visibleCount.value = Math.min(visibleCount.value + pageSize, items.value.length);
|
|
}
|
|
|
|
function resetVisibleCount() {
|
|
visibleCount.value = pageSize;
|
|
}
|
|
|
|
function disconnectObserver() {
|
|
observer?.disconnect();
|
|
observer = null;
|
|
}
|
|
|
|
function connectObserver() {
|
|
disconnectObserver();
|
|
|
|
if (!canLoadMore.value || !loadMoreSentinel.value) {
|
|
return;
|
|
}
|
|
|
|
observer = new IntersectionObserver((entries) => {
|
|
if (!entries.some((entry) => entry.isIntersecting)) {
|
|
return;
|
|
}
|
|
|
|
loadMore();
|
|
}, {
|
|
rootMargin: '240px 0px',
|
|
});
|
|
|
|
observer.observe(loadMoreSentinel.value);
|
|
}
|
|
|
|
const resetSignature = computed(() => JSON.stringify([
|
|
enabled.value,
|
|
items.value.length,
|
|
...((options.resetKeys ?? []).map((entry) => unref(entry as ReactiveValue<unknown>))),
|
|
]));
|
|
|
|
watch(resetSignature, resetVisibleCount, { immediate: true });
|
|
watch(canLoadMore, () => {
|
|
if (!canLoadMore.value) {
|
|
disconnectObserver();
|
|
}
|
|
});
|
|
|
|
onMounted(() => {
|
|
watch([loadMoreSentinel, canLoadMore], connectObserver, { immediate: true });
|
|
});
|
|
|
|
onBeforeUnmount(disconnectObserver);
|
|
|
|
return {
|
|
canLoadMore,
|
|
loadMore,
|
|
loadMoreSentinel,
|
|
remainingCount,
|
|
visibleCount,
|
|
visibleItems,
|
|
};
|
|
}
|