summaryrefslogtreecommitdiff
path: root/client/simple/src/js/main/infinite_scroll.ts
blob: b286bce37358ea8e6eb1e95e648689ca28e4fea1 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
// SPDX-License-Identifier: AGPL-3.0-or-later

import { assertElement, http, settings } from "../core/toolkit.ts";

const newLoadSpinner = (): HTMLDivElement => {
  return Object.assign(document.createElement("div"), {
    className: "loader"
  });
};

const loadNextPage = async (onlyImages: boolean, callback: () => void): Promise<void> => {
  const searchForm = document.querySelector<HTMLFormElement>("#search");
  assertElement(searchForm);

  const form = document.querySelector<HTMLFormElement>("#pagination form.next_page");
  assertElement(form);

  const action = searchForm.getAttribute("action");
  if (!action) {
    throw new Error("Form action not defined");
  }

  const paginationElement = document.querySelector<HTMLElement>("#pagination");
  assertElement(paginationElement);

  paginationElement.replaceChildren(newLoadSpinner());

  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<HTMLElement>("#urls article");
    const nextPaginationElement = nextPageDoc.querySelector<HTMLElement>("#pagination");

    document.querySelector("#pagination")?.remove();

    const urlsElement = document.querySelector<HTMLElement>("#urls");
    if (!urlsElement) {
      throw new Error("URLs element not found");
    }

    if (articleList.length > 0 && !onlyImages) {
      // do not add <hr> element when there are only images
      urlsElement.appendChild(document.createElement("hr"));
    }

    urlsElement.append(...Array.from(articleList));

    if (nextPaginationElement) {
      const results = document.querySelector<HTMLElement>("#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 resultsElement: HTMLElement | null = document.getElementById("results");
if (!resultsElement) {
  throw new Error("Results element not found");
}

const onlyImages: boolean = resultsElement.classList.contains("only_template_images");
const observedSelector = "article.result:last-child";

const intersectionObserveOptions: IntersectionObserverInit = {
  rootMargin: "320px"
};

const observer: IntersectionObserver = new IntersectionObserver((entries: IntersectionObserverEntry[]) => {
  const [paginationEntry] = entries;

  if (paginationEntry?.isIntersecting) {
    observer.unobserve(paginationEntry.target);

    void loadNextPage(onlyImages, () => {
      const nextObservedElement = document.querySelector<HTMLElement>(observedSelector);
      if (nextObservedElement) {
        observer.observe(nextObservedElement);
      }
    }).then(() => {
      // wait until promise is resolved
    });
  }
}, intersectionObserveOptions);

const initialObservedElement: HTMLElement | null = document.querySelector<HTMLElement>(observedSelector);
if (initialObservedElement) {
  observer.observe(initialObservedElement);
}