summaryrefslogtreecommitdiff
path: root/client/simple/src/js/main/search.ts
diff options
context:
space:
mode:
Diffstat (limited to 'client/simple/src/js/main/search.ts')
-rw-r--r--client/simple/src/js/main/search.ts233
1 files changed, 233 insertions, 0 deletions
diff --git a/client/simple/src/js/main/search.ts b/client/simple/src/js/main/search.ts
new file mode 100644
index 000000000..5e68965b1
--- /dev/null
+++ b/client/simple/src/js/main/search.ts
@@ -0,0 +1,233 @@
+import { assertElement, searxng } from "./00_toolkit.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);
+
+ searxng.listen("click", cs, (event: MouseEvent) => {
+ event.preventDefault();
+ qInput.value = "";
+ qInput.focus();
+ updateClearButton(qInput, cs);
+ });
+
+ searxng.listen("input", qInput, () => updateClearButton(qInput, cs), { passive: true });
+};
+
+const fetchResults = async (qInput: HTMLInputElement, query: string): Promise<void> => {
+ try {
+ let res: Response;
+
+ if (searxng.settings.method === "GET") {
+ res = await searxng.http("GET", `./autocompleter?q=${query}`);
+ } else {
+ res = await searxng.http("POST", "./autocompleter", new URLSearchParams({ q: query }));
+ }
+
+ const results = await res.json();
+
+ const autocomplete = document.querySelector<HTMLElement>(".autocomplete");
+ assertElement(autocomplete);
+
+ const autocompleteList = document.querySelector<HTMLUListElement>(".autocomplete ul");
+ assertElement(autocompleteList);
+
+ autocomplete.classList.add("open");
+ autocompleteList.replaceChildren();
+
+ // show an error message that no result was found
+ if (!results?.[1]?.length) {
+ const noItemFoundMessage = Object.assign(document.createElement("li"), {
+ className: "no-item-found",
+ textContent: searxng.settings.translations?.no_item_found ?? "No results found"
+ });
+ autocompleteList.append(noItemFoundMessage);
+ return;
+ }
+
+ const fragment = new DocumentFragment();
+
+ for (const result of results[1]) {
+ const li = Object.assign(document.createElement("li"), { textContent: result });
+
+ searxng.listen("mousedown", li, () => {
+ qInput.value = result;
+
+ const form = document.querySelector<HTMLFormElement>("#search");
+ form?.submit();
+
+ autocomplete.classList.remove("open");
+ });
+
+ fragment.append(li);
+ }
+
+ autocompleteList.append(fragment);
+ } catch (error) {
+ console.error("Error fetching autocomplete results:", error);
+ }
+};
+
+searxng.ready(
+ () => {
+ const qInput = document.getElementById("q") as HTMLInputElement | null;
+ assertElement(qInput);
+
+ const isMobile = window.matchMedia("(max-width: 50em)").matches;
+ const isResultsPage = document.querySelector("main")?.id === "main_results";
+
+ // focus search input on large screens
+ if (!isMobile && !isResultsPage) {
+ qInput.focus();
+ }
+
+ createClearButton(qInput);
+
+ // autocompleter
+ if (searxng.settings.autocomplete) {
+ let timeoutId: number;
+
+ searxng.listen("input", qInput, () => {
+ clearTimeout(timeoutId);
+
+ const query = qInput.value;
+ const minLength = searxng.settings.autocomplete_min ?? 2;
+
+ if (query.length < minLength) return;
+
+ timeoutId = window.setTimeout(async () => {
+ if (query === qInput.value) {
+ await fetchResults(qInput, query);
+ }
+ }, 300);
+ });
+
+ const autocomplete = document.querySelector<HTMLElement>(".autocomplete");
+ const autocompleteList = document.querySelector<HTMLUListElement>(".autocomplete ul");
+ if (autocompleteList) {
+ searxng.listen("keyup", qInput, (event: KeyboardEvent) => {
+ const listItems = [...autocompleteList.children] as HTMLElement[];
+
+ const currentIndex = listItems.findIndex((item) => item.classList.contains("active"));
+ let newCurrentIndex = -1;
+
+ switch (event.key) {
+ case "ArrowUp": {
+ const currentItem = listItems[currentIndex];
+ if (currentItem && currentIndex >= 0) {
+ currentItem.classList.remove("active");
+ }
+ // we need to add listItems.length to the index calculation here because the JavaScript modulos
+ // operator doesn't work with negative numbers
+ newCurrentIndex = (currentIndex - 1 + listItems.length) % listItems.length;
+ break;
+ }
+ case "ArrowDown": {
+ const currentItem = listItems[currentIndex];
+ if (currentItem && currentIndex >= 0) {
+ currentItem.classList.remove("active");
+ }
+ newCurrentIndex = (currentIndex + 1) % listItems.length;
+ break;
+ }
+ case "Tab":
+ case "Enter":
+ if (autocomplete) {
+ autocomplete.classList.remove("open");
+ }
+ break;
+ }
+
+ if (newCurrentIndex !== -1) {
+ const selectedItem = listItems[newCurrentIndex];
+ if (selectedItem) {
+ selectedItem.classList.add("active");
+
+ if (!selectedItem.classList.contains("no-item-found")) {
+ const qInput = document.getElementById("q") as HTMLInputElement | null;
+ if (qInput) {
+ qInput.value = selectedItem.textContent ?? "";
+ }
+ }
+ }
+ }
+ });
+ }
+ }
+
+ // 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 (
+ searxng.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) {
+ searxng.listen("change", safesearchElement, () => submitIfQuery(qInput));
+ }
+
+ const timeRangeElement = document.getElementById("time_range");
+ if (timeRangeElement) {
+ searxng.listen("change", timeRangeElement, () => submitIfQuery(qInput));
+ }
+
+ const languageElement = document.getElementById("language");
+ if (languageElement) {
+ searxng.listen("change", languageElement, () => submitIfQuery(qInput));
+ }
+ }
+
+ const categoryButtons = [...document.querySelectorAll<HTMLButtonElement>("button.category_button")];
+ for (const button of categoryButtons) {
+ searxng.listen("click", button, (event: MouseEvent) => {
+ if (event.shiftKey) {
+ event.preventDefault();
+ button.classList.toggle("selected");
+ return;
+ }
+
+ // deselect all other categories
+ for (const categoryButton of categoryButtons) {
+ categoryButton.classList.toggle("selected", categoryButton === button);
+ }
+ });
+ }
+
+ const form = document.querySelector<HTMLFormElement>("#search");
+ assertElement(form);
+
+ // override form submit action to update the actually selected categories
+ searxng.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(",");
+ }
+
+ form.submit();
+ });
+ },
+ { on: [searxng.endpoint === "index" || searxng.endpoint === "results"] }
+);