mirror of
https://github.com/searxng/searxng.git
synced 2026-06-22 17:48:33 +02:00
[mod] client/simple: client plugins (#5406)
* [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`](https://github.com/inetol/searxng/blob/1ad832b1dc33f3f388da361ff2459b05dc86a164/client/simple/src/js/toolkit.ts#L134) array has the plugin id. * [mod] client/simple: rebuild static
This commit is contained in:
@@ -1,88 +1,49 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import { assertElement, listen, settings } from "../core/toolkit.ts";
|
||||
import { listen } from "../toolkit.ts";
|
||||
import { getElement } from "../util/getElement.ts";
|
||||
|
||||
const submitIfQuery = (qInput: HTMLInputElement): void => {
|
||||
if (qInput.value.length > 0) {
|
||||
const search = document.getElementById("search") as HTMLFormElement | null;
|
||||
search?.submit();
|
||||
}
|
||||
};
|
||||
|
||||
const updateClearButton = (qInput: HTMLInputElement, cs: HTMLElement): void => {
|
||||
cs.classList.toggle("empty", qInput.value.length === 0);
|
||||
};
|
||||
|
||||
const createClearButton = (qInput: HTMLInputElement): void => {
|
||||
const cs = document.getElementById("clear_search");
|
||||
assertElement(cs);
|
||||
|
||||
updateClearButton(qInput, cs);
|
||||
|
||||
listen("click", cs, (event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
qInput.value = "";
|
||||
qInput.focus();
|
||||
updateClearButton(qInput, cs);
|
||||
});
|
||||
|
||||
listen("input", qInput, () => updateClearButton(qInput, cs), { passive: true });
|
||||
};
|
||||
|
||||
const qInput = document.getElementById("q") as HTMLInputElement | null;
|
||||
assertElement(qInput);
|
||||
const searchForm: HTMLFormElement = getElement<HTMLFormElement>("search");
|
||||
const searchInput: HTMLInputElement = getElement<HTMLInputElement>("q");
|
||||
const searchReset: HTMLButtonElement = getElement<HTMLButtonElement>("clear_search");
|
||||
|
||||
const isMobile: boolean = window.matchMedia("(max-width: 50em)").matches;
|
||||
const isResultsPage: boolean = document.querySelector("main")?.id === "main_results";
|
||||
|
||||
const categoryButtons: HTMLButtonElement[] = Array.from(
|
||||
document.querySelectorAll<HTMLButtonElement>("#categories_container button.category")
|
||||
);
|
||||
|
||||
if (searchInput.value.length === 0) {
|
||||
searchReset.classList.add("empty");
|
||||
}
|
||||
|
||||
// focus search input on large screens
|
||||
if (!(isMobile || isResultsPage)) {
|
||||
qInput.focus();
|
||||
searchInput.focus();
|
||||
}
|
||||
|
||||
// On mobile, move cursor to the end of the input on focus
|
||||
if (isMobile) {
|
||||
listen("focus", qInput, () => {
|
||||
listen("focus", searchInput, () => {
|
||||
// Defer cursor move until the next frame to prevent a visual jump
|
||||
requestAnimationFrame(() => {
|
||||
const end = qInput.value.length;
|
||||
qInput.setSelectionRange(end, end);
|
||||
qInput.scrollLeft = qInput.scrollWidth;
|
||||
const end = searchInput.value.length;
|
||||
searchInput.setSelectionRange(end, end);
|
||||
searchInput.scrollLeft = searchInput.scrollWidth;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
createClearButton(qInput);
|
||||
listen("input", searchInput, () => {
|
||||
searchReset.classList.toggle("empty", searchInput.value.length === 0);
|
||||
});
|
||||
|
||||
// Additionally to searching when selecting a new category, we also
|
||||
// automatically start a new search request when the user changes a search
|
||||
// filter (safesearch, time range or language) (this requires JavaScript
|
||||
// though)
|
||||
if (
|
||||
settings.search_on_category_select &&
|
||||
// If .search_filters is undefined (invisible) we are on the homepage and
|
||||
// hence don't have to set any listeners
|
||||
document.querySelector(".search_filters")
|
||||
) {
|
||||
const safesearchElement = document.getElementById("safesearch");
|
||||
if (safesearchElement) {
|
||||
listen("change", safesearchElement, () => submitIfQuery(qInput));
|
||||
}
|
||||
listen("click", searchReset, () => {
|
||||
searchReset.classList.add("empty");
|
||||
searchInput.focus();
|
||||
});
|
||||
|
||||
const timeRangeElement = document.getElementById("time_range");
|
||||
if (timeRangeElement) {
|
||||
listen("change", timeRangeElement, () => submitIfQuery(qInput));
|
||||
}
|
||||
|
||||
const languageElement = document.getElementById("language");
|
||||
if (languageElement) {
|
||||
listen("change", languageElement, () => submitIfQuery(qInput));
|
||||
}
|
||||
}
|
||||
|
||||
const categoryButtons: HTMLButtonElement[] = [
|
||||
...document.querySelectorAll<HTMLButtonElement>("button.category_button")
|
||||
];
|
||||
for (const button of categoryButtons) {
|
||||
listen("click", button, (event: MouseEvent) => {
|
||||
if (event.shiftKey) {
|
||||
@@ -98,21 +59,34 @@ for (const button of categoryButtons) {
|
||||
});
|
||||
}
|
||||
|
||||
const form: HTMLFormElement | null = document.querySelector<HTMLFormElement>("#search");
|
||||
assertElement(form);
|
||||
|
||||
// override form submit action to update the actually selected categories
|
||||
listen("submit", form, (event: Event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const categoryValuesInput = document.querySelector<HTMLInputElement>("#selected-categories");
|
||||
if (categoryValuesInput) {
|
||||
const categoryValues = categoryButtons
|
||||
.filter((button) => button.classList.contains("selected"))
|
||||
.map((button) => button.name.replace("category_", ""));
|
||||
|
||||
categoryValuesInput.value = categoryValues.join(",");
|
||||
if (document.querySelector("div.search_filters")) {
|
||||
const safesearchElement = document.getElementById("safesearch");
|
||||
if (safesearchElement) {
|
||||
listen("change", safesearchElement, () => searchForm.submit());
|
||||
}
|
||||
|
||||
form.submit();
|
||||
const timeRangeElement = document.getElementById("time_range");
|
||||
if (timeRangeElement) {
|
||||
listen("change", timeRangeElement, () => searchForm.submit());
|
||||
}
|
||||
|
||||
const languageElement = document.getElementById("language");
|
||||
if (languageElement) {
|
||||
listen("change", languageElement, () => searchForm.submit());
|
||||
}
|
||||
}
|
||||
|
||||
// override searchForm submit event
|
||||
listen("submit", searchForm, (event: Event) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (categoryButtons.length > 0) {
|
||||
const searchCategories = getElement<HTMLInputElement>("selected-categories");
|
||||
searchCategories.value = categoryButtons
|
||||
.filter((button) => button.classList.contains("selected"))
|
||||
.map((button) => button.name.replace("category_", ""))
|
||||
.join(",");
|
||||
}
|
||||
|
||||
searchForm.submit();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user