// SPDX-License-Identifier: AGPL-3.0-or-later import { Plugin } from "../Plugin.ts"; import { http, settings } from "../toolkit.ts"; import { assertElement } from "../util/assertElement.ts"; import { getElement } from "../util/getElement.ts"; /** * Automatically loads the next page when scrolling to bottom of the current page. */ export default class InfiniteScroll extends Plugin { public constructor() { super("infiniteScroll"); } protected async run(): Promise { const resultsElement = getElement("results"); const onlyImages: boolean = resultsElement.classList.contains("only_template_images"); const observedSelector = "article.result:last-child"; const spinnerElement = document.createElement("div"); spinnerElement.className = "loader"; const loadNextPage = async (callback: () => void): Promise => { const searchForm = document.querySelector("#search"); assertElement(searchForm); const form = document.querySelector("#pagination form.next_page"); assertElement(form); const action = searchForm.getAttribute("action"); if (!action) { throw new Error("Form action not defined"); } const paginationElement = document.querySelector("#pagination"); assertElement(paginationElement); paginationElement.replaceChildren(spinnerElement); try { const res = await http("POST", action, { body: new FormData(form) }); const nextPage = await res.text(); if (!nextPage) return; const nextPageDoc = new DOMParser().parseFromString(nextPage, "text/html"); const articleList = nextPageDoc.querySelectorAll("#urls article"); const nextPaginationElement = nextPageDoc.querySelector("#pagination"); document.querySelector("#pagination")?.remove(); const urlsElement = document.querySelector("#urls"); if (!urlsElement) { throw new Error("URLs element not found"); } if (articleList.length > 0 && !onlyImages) { // do not add
element when there are only images urlsElement.appendChild(document.createElement("hr")); } urlsElement.append(...articleList); if (nextPaginationElement) { const results = document.querySelector("#results"); results?.appendChild(nextPaginationElement); callback(); } } catch (error) { console.error("Error loading next page:", error); const errorElement = Object.assign(document.createElement("div"), { textContent: settings.translations?.error_loading_next_page ?? "Error loading next page", className: "dialog-error" }); errorElement.setAttribute("role", "alert"); document.querySelector("#pagination")?.replaceChildren(errorElement); } }; const intersectionObserveOptions: IntersectionObserverInit = { rootMargin: "320px" }; const observer: IntersectionObserver = new IntersectionObserver(async (entries: IntersectionObserverEntry[]) => { const [paginationEntry] = entries; if (paginationEntry?.isIntersecting) { observer.unobserve(paginationEntry.target); await loadNextPage(() => { const nextObservedElement = document.querySelector(observedSelector); if (nextObservedElement) { observer.observe(nextObservedElement); } }); } }, intersectionObserveOptions); const initialObservedElement: HTMLElement | null = document.querySelector(observedSelector); if (initialObservedElement) { observer.observe(initialObservedElement); } } protected async post(): Promise { // noop } }