Skip to content

Instantly share code, notes, and snippets.

@Plaristote
Created October 9, 2024 16:17
Show Gist options
  • Save Plaristote/36250fa999da634082d324c27b8cefdc to your computer and use it in GitHub Desktop.
Save Plaristote/36250fa999da634082d324c27b8cefdc to your computer and use it in GitHub Desktop.
Simple infinite scroll implementation for wordpress themes
/*
* The InfiniteScroll object fetches the HTML of the next page
* as soon as the "anchor" element (typically, the wordpress
* pagination element) becomes visible on screen.
*
* It then uses "postSelector" and "containerSelector" to extract
* posts from the page and append them to the current page.
*
* Example:
*
import InfiniteScroll from "./infinite-scroll.js";
InfiniteScroll.factory({
anchorSelector: "nav.pagination",
postSelector: ".post",
containerSelector: ".post-list",
loadingElement: document.getElementById("loading-element"),
endElement: document.getElementById("end-element")
});
*
* We also assume the URL for the next page will be the current
* page's URL appended by `/page/${pageNumber}`. In case this
* assumption is not true in your case, you may overload the
* `nextUrl` getter, like this:
class MyInfiniteScroll extens InfiniteScroll {
get nextUrl() {
return `${window.location.href}?p=${this.currentPage + 1}`;
}
}
*
*/
function toggleAfterContainerElement(element, container, value) {
if (value) {
container.parentNode.insertBefore(element, container.nextSibling);
} else {
element.remove();
}
}
function isScrolledIntoView(el) {
const rect = el.getBoundingClientRect();
const elemTop = rect.top;
const elemBottom = rect.bottom;
// Only completely visible elements return true:
return (elemTop >= 0) && (elemBottom <= window.innerHeight);
// Partially visible elements return true:
return elemTop < window.innerHeight && elemBottom >= 0;
}
function defaultLoadingElement() {
const element = document.createElement("div");
element.classList.add("infinite-scroll-loading-hint");
return element;
}
export default class InfiniteScroll {
static factory(options) {
return new Promise(function (resolve, reject) {
document.addEventListener("DOMContentLoaded", function() {
document.querySelector(options.anchorSelector)
? resolve(new InfiniteScroll(options))
: reject("No anchor found for infinite scroll");
});
});
}
constructor(options) {
this.options = options;
this.loadingElement = this.options.loadingElement || defaultLoadingElement();
this.endElement = this.options.endElement;
this.reallow();
this.loading = false;
this.hasMore = true;
this.delay = 10;
this.currentPage = 1;
window.addEventListener("scroll", this.onScroll.bind(this));
document.querySelector(this.options.anchorSelector).style.opacity = 0;
}
get container() {
return document.querySelector(this.options.containerSelector);
}
get element() {
return document.querySelector(this.options.anchorSelector || "nav.pagination");
}
get nextUrl() {
return `${window.location.href}page/${this.currentPage + 1}`;
}
set loading(value) {
this._loading = value;
toggleAfterContainerElement(this.loadingElement, this.container, value);
}
get loading() {
return this._loading;
}
set hasMore(value) {
this._hasMore = value;
if (this.endElement) {
toggleAfterContainerElement(this.endElement, this.container, !value);
}
}
get hasMore() {
return this._hasMore;
}
reallow() {
this.allow = true;
}
onScroll(event) {
if (this.hasMore && !this.loading && this.allow) {
this.allow = false;
setTimeout(this.reallow.bind(this), this.delay);
if (isScrolledIntoView(this.element)) this.fetchMore();
}
}
fetchMore() {
this.loading = true;
return fetch(this.nextUrl)
.then(response => response.text())
.then(html => {
const elements = this.newElementsFromHtml(html);
this.hasMore = elements.length > 0;
this.currentPage += 1;
this.appendNewElements(elements);
}).catch(error => {
console.error("Infinite scroll failed to fetch more:", error);
}).finally(() => {
this.loading = false;
});
}
newElementsFromHtml(html) {
const container = document.createElement("div");
container.innerHTML = html;
return Array.from(
container.querySelector(this.options.containerSelector)
.querySelectorAll(this.options.postSelector)
);
}
appendNewElements(elements) {
const container = this.container;
elements.forEach(element => {
container.appendChild(element);
});
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment