mirror of
https://github.com/searxng/searxng.git
synced 2026-07-23 16:31:32 +00:00
* [mod] client/simple: client plugins
Defines a new interface for client side *"plugins"* that coexist with server
side plugin system. Each plugin (e.g., `InfiniteScroll`) extends the base
`ts Plugin`. Client side plugins are independent and lazy‑loaded via `router.ts`
when their `load()` conditions are met. On each navigation request, all
applicable plugins are instanced.
Since these are client side plugins, we can only invoke them once DOM is fully
loaded. E.g. `Calculator` will not render a new `answer` block until fully
loaded and executed.
For some plugins, we might want to handle its availability in `settings.yml`
and toggle in UI, like we do for server side plugins. In that case, we extend
`py Plugin` instancing only the information and then checking client side if
[`settings.plugins`](1ad832b1dc/client/simple/src/js/toolkit.ts (L134))
array has the plugin id.
* [mod] client/simple: rebuild static
111 lines
3.8 KiB
TypeScript
111 lines
3.8 KiB
TypeScript
// 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<void> {
|
|
const resultsElement = getElement<HTMLElement>("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<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(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<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(...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 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<HTMLElement>(observedSelector);
|
|
if (nextObservedElement) {
|
|
observer.observe(nextObservedElement);
|
|
}
|
|
});
|
|
}
|
|
}, intersectionObserveOptions);
|
|
|
|
const initialObservedElement: HTMLElement | null = document.querySelector<HTMLElement>(observedSelector);
|
|
if (initialObservedElement) {
|
|
observer.observe(initialObservedElement);
|
|
}
|
|
}
|
|
|
|
protected async post(): Promise<void> {
|
|
// noop
|
|
}
|
|
}
|