feat(mobile): add infinite scroll and hide pagination on mobile
Add IntersectionObserver-based infinite scroll for mobile viewports that fetches next pages as the user scrolls. Uses MutationObserver to re-attach after DOM replacements. Hide pagination controls on screens narrower than 768px.
This commit is contained in:
parent
8e92385c56
commit
39bad42f9c
3 changed files with 81 additions and 1 deletions
|
|
@ -11,6 +11,82 @@
|
||||||
src="https://cdn.jsdelivr.net/gh/starfederation/datastar@1.0.0-RC.6/bundles/datastar.js"
|
src="https://cdn.jsdelivr.net/gh/starfederation/datastar@1.0.0-RC.6/bundles/datastar.js"
|
||||||
></script>
|
></script>
|
||||||
{% block head %}{% endblock %}
|
{% block head %}{% endblock %}
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
var mobileQuery = window.matchMedia("(max-width: 767px)");
|
||||||
|
if (!mobileQuery.matches) return;
|
||||||
|
|
||||||
|
function setupInfiniteScroll() {
|
||||||
|
document.querySelectorAll("[data-infinite-scroll]").forEach(function (container) {
|
||||||
|
var sentinel = container.querySelector(".infinite-scroll-sentinel");
|
||||||
|
if (!sentinel || sentinel.dataset.observed) return;
|
||||||
|
sentinel.dataset.observed = "true";
|
||||||
|
|
||||||
|
var observer = new IntersectionObserver(
|
||||||
|
function (entries) {
|
||||||
|
entries.forEach(function (entry) {
|
||||||
|
if (!entry.isIntersecting) return;
|
||||||
|
|
||||||
|
var nextUrl = container.getAttribute("data-next-url");
|
||||||
|
var targetSelector = container.getAttribute("data-target");
|
||||||
|
if (!nextUrl || !targetSelector) {
|
||||||
|
observer.disconnect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
observer.disconnect();
|
||||||
|
sentinel.remove();
|
||||||
|
|
||||||
|
fetch(nextUrl, {
|
||||||
|
headers: { "datastar-request": "true" },
|
||||||
|
})
|
||||||
|
.then(function (res) { return res.text(); })
|
||||||
|
.then(function (html) {
|
||||||
|
var doc = new DOMParser().parseFromString(html, "text/html");
|
||||||
|
var newTarget = doc.querySelector(targetSelector);
|
||||||
|
if (!newTarget) return;
|
||||||
|
|
||||||
|
// Append new rows to existing tbody
|
||||||
|
var existingTbody = container.querySelector("tbody");
|
||||||
|
var newTbody = newTarget.querySelector("tbody");
|
||||||
|
if (existingTbody && newTbody) {
|
||||||
|
Array.from(newTbody.children).forEach(function (row) {
|
||||||
|
existingTbody.appendChild(document.adoptNode(row));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there are more pages
|
||||||
|
var newContainer = newTarget.querySelector("[data-infinite-scroll]");
|
||||||
|
var newNextUrl = newContainer && newContainer.getAttribute("data-next-url");
|
||||||
|
if (newNextUrl) {
|
||||||
|
container.setAttribute("data-next-url", newNextUrl);
|
||||||
|
var newSentinel = document.createElement("div");
|
||||||
|
newSentinel.className = "infinite-scroll-sentinel h-4";
|
||||||
|
newSentinel.setAttribute("aria-hidden", "true");
|
||||||
|
container.appendChild(newSentinel);
|
||||||
|
setupInfiniteScroll();
|
||||||
|
} else {
|
||||||
|
container.removeAttribute("data-next-url");
|
||||||
|
container.removeAttribute("data-infinite-scroll");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ rootMargin: "200px" }
|
||||||
|
);
|
||||||
|
|
||||||
|
observer.observe(sentinel);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setupInfiniteScroll();
|
||||||
|
|
||||||
|
var bodyObserver = new MutationObserver(function () {
|
||||||
|
setupInfiniteScroll();
|
||||||
|
});
|
||||||
|
bodyObserver.observe(document.body, { childList: true, subtree: true });
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body class="min-h-screen bg-amber-50 text-stone-900">
|
<body class="min-h-screen bg-amber-50 text-stone-900">
|
||||||
<main class="mx-auto flex w-full max-w-5xl flex-col gap-6 px-6 py-10">
|
<main class="mx-auto flex w-full max-w-5xl flex-col gap-6 px-6 py-10">
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
|
|
||||||
{% macro pagination_header(items, navigator, target_selector) %}
|
{% macro pagination_header(items, navigator, target_selector) %}
|
||||||
<div
|
<div
|
||||||
class="pagination-controls flex flex-wrap items-center justify-between gap-x-3 border-t border-amber-200/70 px-4 py-1.5 text-xs leading-none text-stone-600"
|
class="pagination-controls hidden md:flex flex-wrap items-center justify-between gap-x-3 border-t border-amber-200/70 px-4 py-1.5 text-xs leading-none text-stone-600"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
Showing {{ items.start_index() }}–{{ items.end_index() }} of {{ items.total }}
|
Showing {{ items.start_index() }}–{{ items.end_index() }} of {{ items.total }}
|
||||||
|
|
|
||||||
|
|
@ -147,6 +147,10 @@ a {
|
||||||
.responsive-table td.mobile-hidden {
|
.responsive-table td.mobile-hidden {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pagination-controls {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Desktop: alternating cards with central line */
|
/* Desktop: alternating cards with central line */
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue