← back to Dw Theme Hamburger
assets/dw-native-infinite.js
138 lines
/* DW Native Infinite Scroll — progressive enhancement for theme {% paginate %} grids.
*
* Converts the theme's native numbered-pager Liquid grids (collection-list /
* blog / blog-masonry) to append-on-scroll, WITHOUT touching the Boost AI
* Search & Discovery surfaces (collection/search product grids — those are
* handled by Boost's own global infinite_scroll + assets/boost-sd-custom.js).
*
* Markup contract (added by the section .liquid files):
* <div data-dw-infinite ← scroll container wrapper
* data-dw-infinite-items=".blog-posts" ← selector of the element whose
* CHILDREN are the cards
* data-dw-infinite-next="/blogs/news?page=2" ← next page URL (paginate.next.url)
* data-dw-infinite-page="1">
* <div class="blog-posts"> … cards … </div>
* <ul class="pagination" data-dw-infinite-pager> … numbered links … </ul> ← noscript fallback
* </div>
*
* Behavior: when the viewport nears the bottom, fetch the next URL, extract the
* same items-container from the returned HTML, append its children to the live
* items-container, then read the NEXT next-url from the fetched page. Stops when
* there is no further next URL. The numbered <ul.pagination> is hidden once JS
* takes over (kept in the DOM for no-JS users + SEO crawl).
*/
(function () {
'use strict';
function initContainer(root) {
if (root.__dwInfiniteInit) return;
root.__dwInfiniteInit = true;
var itemsSel = root.getAttribute('data-dw-infinite-items');
var itemsEl = itemsSel ? root.querySelector(itemsSel) : root;
if (!itemsEl) return;
var nextUrl = root.getAttribute('data-dw-infinite-next') || '';
var loading = false;
var done = !nextUrl;
// Hide the numbered pager — JS is now driving. Kept in DOM as no-JS fallback.
var pager = root.querySelector('[data-dw-infinite-pager]');
if (pager) pager.setAttribute('hidden', 'hidden');
// Sentinel + loading indicator appended after the items container.
var sentinel = document.createElement('div');
sentinel.className = 'dw-infinite-sentinel';
sentinel.setAttribute('aria-hidden', 'true');
sentinel.style.cssText = 'min-height:1px;width:100%';
var spinner = document.createElement('div');
spinner.className = 'dw-infinite-loading';
spinner.style.cssText = 'display:none;text-align:center;padding:24px 0;font:600 11px/1 -apple-system,system-ui,sans-serif;letter-spacing:1.5px;text-transform:uppercase;color:#999';
spinner.textContent = 'Loading more…';
itemsEl.parentNode.insertBefore(spinner, itemsEl.nextSibling);
itemsEl.parentNode.insertBefore(sentinel, spinner.nextSibling);
function appendFrom(html, url) {
var doc = new DOMParser().parseFromString(html, 'text/html');
// Find the matching items container on the fetched page.
var src = null;
var fetchedRoot = doc.querySelector('[data-dw-infinite][data-dw-infinite-items="' + itemsSel + '"]') ||
doc.querySelector('[data-dw-infinite]');
if (fetchedRoot) src = itemsSel ? fetchedRoot.querySelector(itemsSel) : fetchedRoot;
if (!src) src = doc.querySelector(itemsSel || '*');
if (src) {
var frag = document.createDocumentFragment();
// Skip masonry sizer elements (they must stay single).
Array.prototype.slice.call(src.children).forEach(function (child) {
if (child.hasAttribute && (child.hasAttribute('data-masonry-sizer') ||
(child.classList && child.classList.contains('masonry-grid-sizer')))) return;
frag.appendChild(child.cloneNode(true));
});
itemsEl.appendChild(frag);
// Re-layout masonry if the theme's masonry lib is present.
if (window.dwRelayoutMasonry) { try { window.dwRelayoutMasonry(itemsEl); } catch (e) {} }
document.dispatchEvent(new CustomEvent('dw:infinite:appended', { detail: { container: itemsEl, url: url } }));
}
// Read the next-next url from the fetched root (or its pager).
var newNext = '';
if (fetchedRoot) newNext = fetchedRoot.getAttribute('data-dw-infinite-next') || '';
if (!newNext) {
var np = doc.querySelector('[data-dw-infinite-pager] .next a, [data-dw-infinite-pager] a[rel="next"]');
if (np) newNext = np.getAttribute('href') || '';
}
return newNext;
}
function loadMore() {
if (loading || done) return;
loading = true;
spinner.style.display = 'block';
fetch(nextUrl, { headers: { 'X-Requested-With': 'XMLHttpRequest' }, credentials: 'same-origin' })
.then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.text(); })
.then(function (html) {
nextUrl = appendFrom(html, nextUrl);
if (!nextUrl) done = true;
})
.catch(function () {
// On any error, fall back to showing the numbered pager again.
done = true;
if (pager) pager.removeAttribute('hidden');
})
.then(function () {
loading = false;
spinner.style.display = 'none';
// If the new content didn't fill the viewport, keep loading.
if (!done) maybeLoad();
});
}
function maybeLoad() {
if (done || loading) return;
var rect = sentinel.getBoundingClientRect();
if (rect.top - window.innerHeight < 700) loadMore();
}
if ('IntersectionObserver' in window) {
var io = new IntersectionObserver(function (entries) {
if (entries.some(function (e) { return e.isIntersecting; })) loadMore();
}, { rootMargin: '700px 0px' });
io.observe(sentinel);
} else {
window.addEventListener('scroll', maybeLoad, { passive: true });
}
// Initial check in case the first page is short.
maybeLoad();
}
function initAll() {
var roots = document.querySelectorAll('[data-dw-infinite]');
for (var i = 0; i < roots.length; i++) initContainer(roots[i]);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initAll);
} else {
initAll();
}
})();