98 lines
2.7 KiB
JavaScript
98 lines
2.7 KiB
JavaScript
import { ref, onMounted, onUnmounted } from "vue";
|
|
import { router } from "@inertiajs/vue3";
|
|
|
|
/**
|
|
* Composable for infinite scroll with Inertia v2.
|
|
*
|
|
* @param {Function} getProp - () => the current paginator object from Inertia props
|
|
* @param {string} propName - the prop key name to reload
|
|
* @param {string} pageParam - query string parameter name for page number
|
|
* @param {Function} getRouteUrl - () => current URL to reload
|
|
*/
|
|
export function useInfiniteList(getProp, propName, pageParam, getRouteUrl) {
|
|
const items = ref([]);
|
|
const currentPage = ref(1);
|
|
const lastPage = ref(1);
|
|
const isLoadingMore = ref(false);
|
|
const sentinelRef = ref(null);
|
|
let observer = null;
|
|
|
|
function syncFromProp() {
|
|
const prop = getProp();
|
|
if (!prop) return;
|
|
lastPage.value = prop.last_page ?? 1;
|
|
}
|
|
|
|
function appendFromProp() {
|
|
const prop = getProp();
|
|
if (!prop?.data) return;
|
|
// append only new items (avoid duplicates by id)
|
|
const existingIds = new Set(items.value.map((i) => i.id));
|
|
const newItems = prop.data.filter((i) => !existingIds.has(i.id));
|
|
items.value.push(...newItems);
|
|
}
|
|
|
|
function reset(initialProp) {
|
|
items.value = initialProp?.data ?? [];
|
|
currentPage.value = initialProp?.current_page ?? 1;
|
|
lastPage.value = initialProp?.last_page ?? 1;
|
|
}
|
|
|
|
function loadMore() {
|
|
if (isLoadingMore.value) return;
|
|
if (currentPage.value >= lastPage.value) return;
|
|
|
|
const nextPage = currentPage.value + 1;
|
|
isLoadingMore.value = true;
|
|
|
|
const params = new URLSearchParams(window.location.search);
|
|
params.set(pageParam, nextPage);
|
|
|
|
router.reload({
|
|
url: `${window.location.pathname}?${params.toString()}`,
|
|
only: [propName],
|
|
preserveScroll: true,
|
|
preserveState: true,
|
|
onSuccess: () => {
|
|
appendFromProp();
|
|
currentPage.value = nextPage;
|
|
isLoadingMore.value = false;
|
|
},
|
|
onError: () => {
|
|
isLoadingMore.value = false;
|
|
},
|
|
});
|
|
}
|
|
|
|
onMounted(() => {
|
|
observer = new IntersectionObserver(
|
|
(entries) => {
|
|
if (entries[0].isIntersecting) {
|
|
loadMore();
|
|
}
|
|
},
|
|
{ rootMargin: "200px" }
|
|
);
|
|
|
|
if (sentinelRef.value) {
|
|
observer.observe(sentinelRef.value);
|
|
}
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
observer?.disconnect();
|
|
});
|
|
|
|
return {
|
|
items,
|
|
currentPage,
|
|
lastPage,
|
|
isLoadingMore,
|
|
sentinelRef,
|
|
reset,
|
|
syncFromProp,
|
|
appendFromProp,
|
|
loadMore,
|
|
};
|
|
}
|