summaryrefslogtreecommitdiff
path: root/client/simple/src/js/main
diff options
context:
space:
mode:
Diffstat (limited to 'client/simple/src/js/main')
-rw-r--r--client/simple/src/js/main/00_toolkit.ts118
-rw-r--r--client/simple/src/js/main/autocomplete.ts129
-rw-r--r--client/simple/src/js/main/index.ts13
-rw-r--r--client/simple/src/js/main/infinite_scroll.ts72
-rw-r--r--client/simple/src/js/main/keyboard.ts133
-rw-r--r--client/simple/src/js/main/mapresult.ts151
-rw-r--r--client/simple/src/js/main/preferences.ts71
-rw-r--r--client/simple/src/js/main/results.ts324
-rw-r--r--client/simple/src/js/main/search.ts263
9 files changed, 559 insertions, 715 deletions
diff --git a/client/simple/src/js/main/00_toolkit.ts b/client/simple/src/js/main/00_toolkit.ts
deleted file mode 100644
index 05cfc4b6b..000000000
--- a/client/simple/src/js/main/00_toolkit.ts
+++ /dev/null
@@ -1,118 +0,0 @@
-import type { KeyBindingLayout } from "./keyboard.ts";
-
-type Settings = {
- theme_static_path?: string;
- method?: string;
- hotkeys?: KeyBindingLayout;
- infinite_scroll?: boolean;
- autocomplete?: boolean;
- autocomplete_min?: number;
- search_on_category_select?: boolean;
- translations?: Record<string, string>;
- [key: string]: unknown;
-};
-
-type ReadyOptions = {
- // all values must be truthy for the callback to be executed
- on?: (boolean | undefined)[];
-};
-
-const getEndpoint = (): string => {
- const endpointClass = Array.from(document.body.classList).find((className) => className.endsWith("_endpoint"));
- return endpointClass?.split("_")[0] ?? "";
-};
-
-const getSettings = (): Settings => {
- const settings = document.querySelector("script[client_settings]")?.getAttribute("client_settings");
- if (!settings) return {};
-
- try {
- return JSON.parse(atob(settings));
- } catch (error) {
- console.error("Failed to load client_settings:", error);
- return {};
- }
-};
-
-type AssertElement = (element?: Element | null) => asserts element is Element;
-export const assertElement: AssertElement = (element?: Element | null): asserts element is Element => {
- if (!element) {
- throw new Error("Bad assertion: DOM element not found");
- }
-};
-
-export const searxng = {
- // dynamic functions
- closeDetail: undefined as (() => void) | undefined,
- scrollPageToSelected: undefined as (() => void) | undefined,
- selectImage: undefined as ((resultElement: Element) => void) | undefined,
- selectNext: undefined as ((openDetailView?: boolean) => void) | undefined,
- selectPrevious: undefined as ((openDetailView?: boolean) => void) | undefined,
-
- endpoint: getEndpoint(),
-
- http: async (method: string, url: string | URL, data?: BodyInit): Promise<Response> => {
- const controller = new AbortController();
- const timeoutId = setTimeout(() => controller.abort(), 30000);
-
- const res = await fetch(url, {
- body: data,
- method,
- signal: controller.signal
- }).finally(() => clearTimeout(timeoutId));
- if (!res.ok) {
- throw new Error(res.statusText);
- }
-
- return res;
- },
-
- listen: <K extends keyof DocumentEventMap, E extends Element>(
- type: string | K,
- target: string | Document | E,
- listener: (this: E, event: DocumentEventMap[K]) => void,
- options?: AddEventListenerOptions
- ): void => {
- if (typeof target !== "string") {
- target.addEventListener(type, listener as EventListener, options);
- return;
- }
-
- document.addEventListener(
- type,
- (event: Event) => {
- for (const node of event.composedPath()) {
- if (node instanceof Element && node.matches(target)) {
- try {
- listener.call(node as E, event as DocumentEventMap[K]);
- } catch (error) {
- console.error(error);
- }
- break;
- }
- }
- },
- options
- );
- },
-
- ready: (callback: () => void, options?: ReadyOptions): void => {
- for (const condition of options?.on ?? []) {
- if (!condition) {
- return;
- }
- }
-
- if (document.readyState !== "loading") {
- callback();
- } else {
- searxng.listen("DOMContentLoaded", document, callback, { once: true });
- }
- },
-
- settings: getSettings()
-};
-
-searxng.listen("click", ".close", function (this: Element) {
- (this.parentNode as Element)?.classList.add("invisible");
-});
diff --git a/client/simple/src/js/main/autocomplete.ts b/client/simple/src/js/main/autocomplete.ts
new file mode 100644
index 000000000..c7ed2056b
--- /dev/null
+++ b/client/simple/src/js/main/autocomplete.ts
@@ -0,0 +1,129 @@
+import { assertElement, http, listen, settings } from "../core/toolkit.ts";
+
+const fetchResults = async (qInput: HTMLInputElement, query: string): Promise<void> => {
+ try {
+ let res: Response;
+
+ if (settings.method === "GET") {
+ res = await http("GET", `./autocompleter?q=${query}`);
+ } else {
+ res = await http("POST", "./autocompleter", { body: 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 === 0) {
+ const noItemFoundMessage = Object.assign(document.createElement("li"), {
+ className: "no-item-found",
+ textContent: 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 });
+
+ 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);
+ }
+};
+
+const qInput = document.getElementById("q") as HTMLInputElement | null;
+assertElement(qInput);
+
+let timeoutId: number;
+
+listen("input", qInput, () => {
+ clearTimeout(timeoutId);
+
+ const query = qInput.value;
+ const minLength = settings.autocomplete_min ?? 2;
+
+ if (query.length < minLength) return;
+
+ timeoutId = window.setTimeout(async () => {
+ if (query === qInput.value) {
+ await fetchResults(qInput, query);
+ }
+ }, 300);
+});
+
+const autocomplete: HTMLElement | null = document.querySelector<HTMLElement>(".autocomplete");
+const autocompleteList: HTMLUListElement | null = document.querySelector<HTMLUListElement>(".autocomplete ul");
+if (autocompleteList) {
+ 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;
+ default:
+ 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 ?? "";
+ }
+ }
+ }
+ }
+ });
+}
diff --git a/client/simple/src/js/main/index.ts b/client/simple/src/js/main/index.ts
deleted file mode 100644
index 4dc86b63b..000000000
--- a/client/simple/src/js/main/index.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-/**
- * @preserve (C) Copyright Contributors to the SearXNG project.
- * @preserve (C) Copyright Contributors to the searx project (2014 - 2021).
- * @license AGPL-3.0-or-later
- */
-
-import "./00_toolkit.ts";
-import "./infinite_scroll.ts";
-import "./keyboard.ts";
-import "./mapresult.ts";
-import "./preferences.ts";
-import "./results.ts";
-import "./search.ts";
diff --git a/client/simple/src/js/main/infinite_scroll.ts b/client/simple/src/js/main/infinite_scroll.ts
index e9f931e51..5c3350266 100644
--- a/client/simple/src/js/main/infinite_scroll.ts
+++ b/client/simple/src/js/main/infinite_scroll.ts
@@ -1,4 +1,4 @@
-import { assertElement, searxng } from "./00_toolkit";
+import { assertElement, http, settings } from "../core/toolkit.ts";
const newLoadSpinner = (): HTMLDivElement => {
return Object.assign(document.createElement("div"), {
@@ -13,12 +13,9 @@ const loadNextPage = async (onlyImages: boolean, callback: () => void): Promise<
const form = document.querySelector<HTMLFormElement>("#pagination form.next_page");
assertElement(form);
- const formData = new FormData(form);
-
const action = searchForm.getAttribute("action");
if (!action) {
- console.error("Form action not found");
- return;
+ throw new Error("Form action not defined");
}
const paginationElement = document.querySelector<HTMLElement>("#pagination");
@@ -27,7 +24,7 @@ const loadNextPage = async (onlyImages: boolean, callback: () => void): Promise<
paginationElement.replaceChildren(newLoadSpinner());
try {
- const res = await searxng.http("POST", action, formData);
+ const res = await http("POST", action, { body: new FormData(form) });
const nextPage = await res.text();
if (!nextPage) return;
@@ -39,8 +36,7 @@ const loadNextPage = async (onlyImages: boolean, callback: () => void): Promise<
const urlsElement = document.querySelector<HTMLElement>("#urls");
if (!urlsElement) {
- console.error("URLs element not found");
- return;
+ throw new Error("URLs element not found");
}
if (articleList.length > 0 && !onlyImages) {
@@ -59,7 +55,7 @@ const loadNextPage = async (onlyImages: boolean, callback: () => void): Promise<
console.error("Error loading next page:", error);
const errorElement = Object.assign(document.createElement("div"), {
- textContent: searxng.settings.translations?.error_loading_next_page ?? "Error loading next page",
+ textContent: settings.translations?.error_loading_next_page ?? "Error loading next page",
className: "dialog-error"
});
errorElement.setAttribute("role", "alert");
@@ -67,42 +63,36 @@ const loadNextPage = async (onlyImages: boolean, callback: () => void): Promise<
}
};
-searxng.ready(
- () => {
- const resultsElement = document.getElementById("results");
- if (!resultsElement) {
- console.error("Results element not found");
- return;
- }
+const resultsElement: HTMLElement | null = document.getElementById("results");
+if (!resultsElement) {
+ throw new Error("Results element not found");
+}
- const onlyImages = resultsElement.classList.contains("only_template_images");
- const observedSelector = "article.result:last-child";
+const onlyImages: boolean = resultsElement.classList.contains("only_template_images");
+const observedSelector = "article.result:last-child";
- const intersectionObserveOptions: IntersectionObserverInit = {
- rootMargin: "320px"
- };
+const intersectionObserveOptions: IntersectionObserverInit = {
+ rootMargin: "320px"
+};
- const observer = new IntersectionObserver(async (entries: IntersectionObserverEntry[]) => {
- const [paginationEntry] = entries;
+const observer: IntersectionObserver = new IntersectionObserver((entries: IntersectionObserverEntry[]) => {
+ const [paginationEntry] = entries;
- if (paginationEntry?.isIntersecting) {
- observer.unobserve(paginationEntry.target);
+ if (paginationEntry?.isIntersecting) {
+ observer.unobserve(paginationEntry.target);
- await loadNextPage(onlyImages, () => {
- const nextObservedElement = document.querySelector<HTMLElement>(observedSelector);
- if (nextObservedElement) {
- observer.observe(nextObservedElement);
- }
- });
+ loadNextPage(onlyImages, () => {
+ const nextObservedElement = document.querySelector<HTMLElement>(observedSelector);
+ if (nextObservedElement) {
+ observer.observe(nextObservedElement);
}
- }, intersectionObserveOptions);
-
- const initialObservedElement = document.querySelector<HTMLElement>(observedSelector);
- if (initialObservedElement) {
- observer.observe(initialObservedElement);
- }
- },
- {
- on: [searxng.endpoint === "results", searxng.settings.infinite_scroll]
+ }).then(() => {
+ // wait until promise is resolved
+ });
}
-);
+}, intersectionObserveOptions);
+
+const initialObservedElement: HTMLElement | null = document.querySelector<HTMLElement>(observedSelector);
+if (initialObservedElement) {
+ observer.observe(initialObservedElement);
+}
diff --git a/client/simple/src/js/main/keyboard.ts b/client/simple/src/js/main/keyboard.ts
index 3c6417bbc..46b9bcc20 100644
--- a/client/simple/src/js/main/keyboard.ts
+++ b/client/simple/src/js/main/keyboard.ts
@@ -1,4 +1,4 @@
-import { assertElement, searxng } from "./00_toolkit.ts";
+import { assertElement, listen, mutable, settings } from "../core/toolkit.ts";
export type KeyBindingLayout = "default" | "vim";
@@ -9,11 +9,13 @@ type KeyBinding = {
cat: string;
};
+type HighlightResultElement = "down" | "up" | "visible" | "bottom" | "top";
+
/* common base for layouts */
const baseKeyBinding: Record<string, KeyBinding> = {
Escape: {
key: "ESC",
- fun: (event) => removeFocus(event),
+ fun: (event: KeyboardEvent) => removeFocus(event),
des: "remove focus from the focused input",
cat: "Control"
},
@@ -145,12 +147,12 @@ const keyBindingLayouts: Record<KeyBindingLayout, Record<string, KeyBinding>> =
}
};
-const keyBindings =
- searxng.settings.hotkeys && searxng.settings.hotkeys in keyBindingLayouts
- ? keyBindingLayouts[searxng.settings.hotkeys]
+const keyBindings: Record<string, KeyBinding> =
+ settings.hotkeys && settings.hotkeys in keyBindingLayouts
+ ? keyBindingLayouts[settings.hotkeys]
: keyBindingLayouts.default;
-const isElementInDetail = (element?: Element): boolean => {
+const isElementInDetail = (element?: HTMLElement): boolean => {
const ancestor = element?.closest(".detail, .result");
return ancestor?.classList.contains("detail") ?? false;
};
@@ -159,12 +161,12 @@ const getResultElement = (element?: HTMLElement): HTMLElement | undefined => {
return element?.closest(".result") ?? undefined;
};
-const isImageResult = (resultElement?: Element): boolean => {
+const isImageResult = (resultElement?: HTMLElement): boolean => {
return resultElement?.classList.contains("result-images") ?? false;
};
const highlightResult =
- (which: string | HTMLElement) =>
+ (which: HighlightResultElement | HTMLElement) =>
(noScroll?: boolean, keepFocus?: boolean): void => {
let effectiveWhich = which;
let current = document.querySelector<HTMLElement>(".result[data-vim-selected]");
@@ -210,7 +212,7 @@ const highlightResult =
next = results[results.indexOf(current) - 1] || current;
break;
case "bottom":
- next = results[results.length - 1];
+ next = results.at(-1);
break;
// biome-ignore lint/complexity/noUselessSwitchCase: fallthrough is intended
case "top":
@@ -229,7 +231,7 @@ const highlightResult =
}
if (!noScroll) {
- scrollPageToSelected();
+ mutable.scrollPageToSelected?.();
}
}
};
@@ -245,7 +247,7 @@ const removeFocus = (event: KeyboardEvent): void => {
if (document.activeElement && (tagName === "input" || tagName === "select" || tagName === "textarea")) {
(document.activeElement as HTMLElement).blur();
} else {
- searxng.closeDetail?.();
+ mutable.closeDetail?.();
}
};
@@ -256,23 +258,23 @@ const pageButtonClick = (css_selector: string): void => {
}
};
-const GoToNextPage = () => {
+const GoToNextPage = (): void => {
pageButtonClick('nav#pagination .next_page button[type="submit"]');
};
-const GoToPreviousPage = () => {
+const GoToPreviousPage = (): void => {
pageButtonClick('nav#pagination .previous_page button[type="submit"]');
};
-const scrollPageToSelected = (): void => {
+mutable.scrollPageToSelected = (): void => {
const sel = document.querySelector<HTMLElement>(".result[data-vim-selected]");
if (!sel) return;
- const wtop = document.documentElement.scrollTop || document.body.scrollTop,
- height = document.documentElement.clientHeight,
- etop = sel.offsetTop,
- ebot = etop + sel.clientHeight,
- offset = 120;
+ const wtop = document.documentElement.scrollTop || document.body.scrollTop;
+ const height = document.documentElement.clientHeight;
+ const etop = sel.offsetTop;
+ const ebot = etop + sel.clientHeight;
+ const offset = 120;
// first element ?
if (!sel.previousElementSibling && ebot < height) {
@@ -297,7 +299,7 @@ const scrollPage = (amount: number): void => {
highlightResult("visible")();
};
-const scrollPageTo = (position: number, nav: string): void => {
+const scrollPageTo = (position: number, nav: HighlightResultElement): void => {
window.scrollTo(0, position);
highlightResult(nav)();
};
@@ -385,7 +387,10 @@ const initHelpContent = (divElement: HTMLElement, keyBindings: typeof baseKeyBin
const toggleHelp = (keyBindings: typeof baseKeyBinding): void => {
let helpPanel = document.querySelector<HTMLElement>("#vim-hotkeys-help");
- if (!helpPanel) {
+ if (helpPanel) {
+ // toggle hidden
+ helpPanel.classList.toggle("invisible");
+ } else {
// first call
helpPanel = Object.assign(document.createElement("div"), {
id: "vim-hotkeys-help",
@@ -396,9 +401,6 @@ const toggleHelp = (keyBindings: typeof baseKeyBinding): void => {
if (body) {
body.appendChild(helpPanel);
}
- } else {
- // toggle hidden
- helpPanel.classList.toggle("invisible");
}
};
@@ -412,56 +414,53 @@ const copyURLToClipboard = async (): Promise<void> => {
}
};
-searxng.ready(() => {
- searxng.listen("click", ".result", function (this: HTMLElement, event: Event) {
- if (!isElementInDetail(event.target as HTMLElement)) {
- highlightResult(this)(true, true);
+listen("click", ".result", function (this: HTMLElement, event: PointerEvent) {
+ if (!isElementInDetail(event.target as HTMLElement)) {
+ highlightResult(this)(true, true);
- const resultElement = getResultElement(event.target as HTMLElement);
+ const resultElement = getResultElement(event.target as HTMLElement);
- if (resultElement && isImageResult(resultElement)) {
- event.preventDefault();
- searxng.selectImage?.(resultElement);
- }
+ if (resultElement && isImageResult(resultElement)) {
+ event.preventDefault();
+ mutable.selectImage?.(resultElement);
}
- });
-
- searxng.listen(
- "focus",
- ".result a",
- (event: Event) => {
- if (!isElementInDetail(event.target as HTMLElement)) {
- const resultElement = getResultElement(event.target as HTMLElement);
+ }
+});
- if (resultElement && !resultElement.getAttribute("data-vim-selected")) {
- highlightResult(resultElement)(true);
- }
+// FIXME: Focus might also trigger Pointer event ^^^
+listen(
+ "focus",
+ ".result a",
+ (event: FocusEvent) => {
+ if (!isElementInDetail(event.target as HTMLElement)) {
+ const resultElement = getResultElement(event.target as HTMLElement);
- if (resultElement && isImageResult(resultElement)) {
- searxng.selectImage?.(resultElement);
- }
+ if (resultElement && !resultElement.hasAttribute("data-vim-selected")) {
+ highlightResult(resultElement)(true);
}
- },
- { capture: true }
- );
- searxng.listen("keydown", document, (event: KeyboardEvent) => {
- // check for modifiers so we don't break browser's hotkeys
- if (Object.hasOwn(keyBindings, event.key) && !event.ctrlKey && !event.altKey && !event.shiftKey && !event.metaKey) {
- const tagName = (event.target as Element)?.tagName?.toLowerCase();
-
- if (event.key === "Escape") {
- keyBindings[event.key]?.fun(event);
- } else {
- if (event.target === document.body || tagName === "a" || tagName === "button") {
- event.preventDefault();
- keyBindings[event.key]?.fun(event);
- }
+ if (resultElement && isImageResult(resultElement)) {
+ event.preventDefault();
+ mutable.selectImage?.(resultElement);
}
}
- });
-
- searxng.scrollPageToSelected = scrollPageToSelected;
- searxng.selectNext = highlightResult("down");
- searxng.selectPrevious = highlightResult("up");
+ },
+ { capture: true }
+);
+
+listen("keydown", document, (event: KeyboardEvent) => {
+ // check for modifiers so we don't break browser's hotkeys
+ if (Object.hasOwn(keyBindings, event.key) && !event.ctrlKey && !event.altKey && !event.shiftKey && !event.metaKey) {
+ const tagName = (event.target as HTMLElement)?.tagName?.toLowerCase();
+
+ if (event.key === "Escape") {
+ keyBindings[event.key]?.fun(event);
+ } else if (event.target === document.body || tagName === "a" || tagName === "button") {
+ event.preventDefault();
+ keyBindings[event.key]?.fun(event);
+ }
+ }
});
+
+mutable.selectNext = highlightResult("down");
+mutable.selectPrevious = highlightResult("up");
diff --git a/client/simple/src/js/main/mapresult.ts b/client/simple/src/js/main/mapresult.ts
index 421b41f77..378e1e54f 100644
--- a/client/simple/src/js/main/mapresult.ts
+++ b/client/simple/src/js/main/mapresult.ts
@@ -1,89 +1,84 @@
-import { searxng } from "./00_toolkit.ts";
+import { listen } from "../core/toolkit.ts";
-searxng.ready(
- () => {
- searxng.listen("click", ".searxng_init_map", async function (this: HTMLElement, event: Event) {
- event.preventDefault();
- this.classList.remove("searxng_init_map");
+listen("click", ".searxng_init_map", async function (this: HTMLElement, event: Event) {
+ event.preventDefault();
+ this.classList.remove("searxng_init_map");
- const {
- View,
- OlMap,
- TileLayer,
- VectorLayer,
- OSM,
- VectorSource,
- Style,
- Stroke,
- Fill,
- Circle,
- fromLonLat,
- GeoJSON,
- Feature,
- Point
- } = await import("../pkg/ol.ts");
- import("ol/ol.css");
+ const {
+ View,
+ OlMap,
+ TileLayer,
+ VectorLayer,
+ OSM,
+ VectorSource,
+ Style,
+ Stroke,
+ Fill,
+ Circle,
+ fromLonLat,
+ GeoJSON,
+ Feature,
+ Point
+ } = await import("../pkg/ol.ts");
+ import("ol/ol.css");
- const { leafletTarget: target, mapLon, mapLat, mapGeojson } = this.dataset;
+ const { leafletTarget: target, mapLon, mapLat, mapGeojson } = this.dataset;
- const lon = parseFloat(mapLon || "0");
- const lat = parseFloat(mapLat || "0");
- const view = new View({ maxZoom: 16, enableRotation: false });
- const map = new OlMap({
- target,
- layers: [new TileLayer({ source: new OSM({ maxZoom: 16 }) })],
- view
- });
+ const lon = Number.parseFloat(mapLon || "0");
+ const lat = Number.parseFloat(mapLat || "0");
+ const view = new View({ maxZoom: 16, enableRotation: false });
+ const map = new OlMap({
+ target: target,
+ layers: [new TileLayer({ source: new OSM({ maxZoom: 16 }) })],
+ view: view
+ });
- try {
- const markerSource = new VectorSource({
- features: [
- new Feature({
- geometry: new Point(fromLonLat([lon, lat]))
- })
- ]
- });
+ try {
+ const markerSource = new VectorSource({
+ features: [
+ new Feature({
+ geometry: new Point(fromLonLat([lon, lat]))
+ })
+ ]
+ });
- const markerLayer = new VectorLayer({
- source: markerSource,
- style: new Style({
- image: new Circle({
- radius: 6,
- fill: new Fill({ color: "#3050ff" })
- })
- })
- });
+ const markerLayer = new VectorLayer({
+ source: markerSource,
+ style: new Style({
+ image: new Circle({
+ radius: 6,
+ fill: new Fill({ color: "#3050ff" })
+ })
+ })
+ });
- map.addLayer(markerLayer);
- } catch (error) {
- console.error("Failed to create marker layer:", error);
- }
+ map.addLayer(markerLayer);
+ } catch (error) {
+ console.error("Failed to create marker layer:", error);
+ }
- if (mapGeojson) {
- try {
- const geoSource = new VectorSource({
- features: new GeoJSON().readFeatures(JSON.parse(mapGeojson), {
- dataProjection: "EPSG:4326",
- featureProjection: "EPSG:3857"
- })
- });
+ if (mapGeojson) {
+ try {
+ const geoSource = new VectorSource({
+ features: new GeoJSON().readFeatures(JSON.parse(mapGeojson), {
+ dataProjection: "EPSG:4326",
+ featureProjection: "EPSG:3857"
+ })
+ });
- const geoLayer = new VectorLayer({
- source: geoSource,
- style: new Style({
- stroke: new Stroke({ color: "#3050ff", width: 2 }),
- fill: new Fill({ color: "#3050ff33" })
- })
- });
+ const geoLayer = new VectorLayer({
+ source: geoSource,
+ style: new Style({
+ stroke: new Stroke({ color: "#3050ff", width: 2 }),
+ fill: new Fill({ color: "#3050ff33" })
+ })
+ });
- map.addLayer(geoLayer);
+ map.addLayer(geoLayer);
- view.fit(geoSource.getExtent(), { padding: [20, 20, 20, 20] });
- } catch (error) {
- console.error("Failed to create GeoJSON layer:", error);
- }
- }
- });
- },
- { on: [searxng.endpoint === "results"] }
-);
+ view.fit(geoSource.getExtent(), { padding: [20, 20, 20, 20] });
+ } catch (error) {
+ console.error("Failed to create GeoJSON layer:", error);
+ }
+ }
+});
diff --git a/client/simple/src/js/main/preferences.ts b/client/simple/src/js/main/preferences.ts
index 6c66018a6..fb81e6558 100644
--- a/client/simple/src/js/main/preferences.ts
+++ b/client/simple/src/js/main/preferences.ts
@@ -1,9 +1,11 @@
-import { searxng } from "./00_toolkit.ts";
+import { http, listen, settings } from "../core/toolkit.ts";
+
+let engineDescriptions: Record<string, [string, string]> | undefined;
const loadEngineDescriptions = async (): Promise<void> => {
- let engineDescriptions: Record<string, [string, string]> | null = null;
+ if (engineDescriptions) return;
try {
- const res = await searxng.http("GET", "engine_descriptions.json");
+ const res = await http("GET", "engine_descriptions.json");
engineDescriptions = await res.json();
} catch (error) {
console.error("Error fetching engineDescriptions:", error);
@@ -12,7 +14,7 @@ const loadEngineDescriptions = async (): Promise<void> => {
for (const [engine_name, [description, source]] of Object.entries(engineDescriptions)) {
const elements = document.querySelectorAll<HTMLElement>(`[data-engine-name="${engine_name}"] .engine-description`);
- const sourceText = ` (<i>${searxng.settings.translations?.Source}:&nbsp;${source}</i>)`;
+ const sourceText = ` (<i>${settings.translations?.Source}:&nbsp;${source}</i>)`;
for (const element of elements) {
element.innerHTML = description + sourceText;
@@ -29,43 +31,38 @@ const toggleEngines = (enable: boolean, engineToggles: NodeListOf<HTMLInputEleme
}
};
-searxng.ready(
- () => {
- const engineElements = document.querySelectorAll<HTMLElement>("[data-engine-name]");
- for (const engineElement of engineElements) {
- searxng.listen("mouseenter", engineElement, loadEngineDescriptions);
- }
+const engineElements: NodeListOf<HTMLElement> = document.querySelectorAll<HTMLElement>("[data-engine-name]");
+for (const engineElement of engineElements) {
+ listen("mouseenter", engineElement, loadEngineDescriptions);
+}
- const engineToggles = document.querySelectorAll<HTMLInputElement>(
- "tbody input[type=checkbox][class~=checkbox-onoff]"
- );
+const engineToggles: NodeListOf<HTMLInputElement> = document.querySelectorAll<HTMLInputElement>(
+ "tbody input[type=checkbox][class~=checkbox-onoff]"
+);
- const enableAllEngines = document.querySelectorAll<HTMLElement>(".enable-all-engines");
- for (const engine of enableAllEngines) {
- searxng.listen("click", engine, () => toggleEngines(true, engineToggles));
- }
+const enableAllEngines: NodeListOf<HTMLElement> = document.querySelectorAll<HTMLElement>(".enable-all-engines");
+for (const engine of enableAllEngines) {
+ listen("click", engine, () => toggleEngines(true, engineToggles));
+}
- const disableAllEngines = document.querySelectorAll<HTMLElement>(".disable-all-engines");
- for (const engine of disableAllEngines) {
- searxng.listen("click", engine, () => toggleEngines(false, engineToggles));
- }
+const disableAllEngines: NodeListOf<HTMLElement> = document.querySelectorAll<HTMLElement>(".disable-all-engines");
+for (const engine of disableAllEngines) {
+ listen("click", engine, () => toggleEngines(false, engineToggles));
+}
- const copyHashButton = document.querySelector<HTMLElement>("#copy-hash");
- if (copyHashButton) {
- searxng.listen("click", copyHashButton, async (event: Event) => {
- event.preventDefault();
+const copyHashButton: HTMLElement | null = document.querySelector<HTMLElement>("#copy-hash");
+if (copyHashButton) {
+ listen("click", copyHashButton, async (event: Event) => {
+ event.preventDefault();
- const { copiedText, hash } = copyHashButton.dataset;
- if (!copiedText || !hash) return;
+ const { copiedText, hash } = copyHashButton.dataset;
+ if (!(copiedText && hash)) return;
- try {
- await navigator.clipboard.writeText(hash);
- copyHashButton.innerText = copiedText;
- } catch (error) {
- console.error("Failed to copy hash:", error);
- }
- });
+ try {
+ await navigator.clipboard.writeText(hash);
+ copyHashButton.innerText = copiedText;
+ } catch (error) {
+ console.error("Failed to copy hash:", error);
}
- },
- { on: [searxng.endpoint === "preferences"] }
-);
+ });
+}
diff --git a/client/simple/src/js/main/results.ts b/client/simple/src/js/main/results.ts
index e278c894a..494f38cbc 100644
--- a/client/simple/src/js/main/results.ts
+++ b/client/simple/src/js/main/results.ts
@@ -1,181 +1,175 @@
import "../../../node_modules/swiped-events/src/swiped-events.js";
-import { assertElement, searxng } from "./00_toolkit.ts";
+import { assertElement, listen, mutable, settings } from "../core/toolkit.ts";
-const loadImage = (imgSrc: string, onSuccess: () => void): void => {
- // singleton image object, which is used for all loading processes of a detailed image
- const imgLoader = new Image();
+let imgTimeoutID: number;
- // set handlers in the on-properties
- imgLoader.onload = () => {
- onSuccess();
+const imageLoader = (resultElement: HTMLElement): void => {
+ if (imgTimeoutID) clearTimeout(imgTimeoutID);
+
+ const imgElement = resultElement.querySelector<HTMLImageElement>(".result-images-source img");
+ if (!imgElement) return;
+
+ // use thumbnail until full image loads
+ const thumbnail = resultElement.querySelector<HTMLImageElement>(".image_thumbnail");
+ if (thumbnail) {
+ if (thumbnail.src === `${settings.theme_static_path}/img/img_load_error.svg`) return;
+
+ imgElement.onerror = (): void => {
+ imgElement.src = thumbnail.src;
+ };
+
+ imgElement.src = thumbnail.src;
+ }
+
+ const imgSource = imgElement.getAttribute("data-src");
+ if (!imgSource) return;
+
+ // unsafe nodejs specific, cast to https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout#return_value
+ // https://github.com/searxng/searxng/pull/5073#discussion_r2265767231
+ imgTimeoutID = setTimeout(() => {
+ imgElement.src = imgSource;
+ imgElement.removeAttribute("data-src");
+ }, 1000) as unknown as number;
+};
+
+const imageThumbnails: NodeListOf<HTMLImageElement> =
+ document.querySelectorAll<HTMLImageElement>("#urls img.image_thumbnail");
+for (const thumbnail of imageThumbnails) {
+ if (thumbnail.complete && thumbnail.naturalWidth === 0) {
+ thumbnail.src = `${settings.theme_static_path}/img/img_load_error.svg`;
+ }
+
+ thumbnail.onerror = (): void => {
+ thumbnail.src = `${settings.theme_static_path}/img/img_load_error.svg`;
};
+}
+
+const copyUrlButton: HTMLButtonElement | null =
+ document.querySelector<HTMLButtonElement>("#search_url button#copy_url");
+copyUrlButton?.style.setProperty("display", "block");
+
+mutable.selectImage = (resultElement: HTMLElement): void => {
+ // add a class that can be evaluated in the CSS and indicates that the
+ // detail view is open
+ const resultsElement = document.getElementById("results");
+ resultsElement?.classList.add("image-detail-open");
+
+ // add a hash to the browser history so that pressing back doesn't return
+ // to the previous page this allows us to dismiss the image details on
+ // pressing the back button on mobile devices
+ window.location.hash = "#image-viewer";
+
+ mutable.scrollPageToSelected?.();
- imgLoader.src = imgSrc;
+ // if there is no element given by the caller, stop here
+ if (!resultElement) return;
+
+ imageLoader(resultElement);
};
-searxng.ready(
- () => {
- const imageThumbnails = document.querySelectorAll<HTMLImageElement>("#urls img.image_thumbnail");
- for (const thumbnail of imageThumbnails) {
- if (thumbnail.complete && thumbnail.naturalWidth === 0) {
- thumbnail.src = `${searxng.settings.theme_static_path}/img/img_load_error.svg`;
- }
-
- thumbnail.onerror = () => {
- thumbnail.src = `${searxng.settings.theme_static_path}/img/img_load_error.svg`;
- };
- }
+mutable.closeDetail = (): void => {
+ const resultsElement = document.getElementById("results");
+ resultsElement?.classList.remove("image-detail-open");
- const copyUrlButton = document.querySelector<HTMLButtonElement>("#search_url button#copy_url");
- copyUrlButton?.style.setProperty("display", "block");
-
- searxng.listen("click", ".btn-collapse", function (this: HTMLElement) {
- const btnLabelCollapsed = this.getAttribute("data-btn-text-collapsed");
- const btnLabelNotCollapsed = this.getAttribute("data-btn-text-not-collapsed");
- const target = this.getAttribute("data-target");
-
- if (!target || !btnLabelCollapsed || !btnLabelNotCollapsed) return;
-
- const targetElement = document.querySelector<HTMLElement>(target);
- assertElement(targetElement);
-
- const isCollapsed = this.classList.contains("collapsed");
- const newLabel = isCollapsed ? btnLabelNotCollapsed : btnLabelCollapsed;
- const oldLabel = isCollapsed ? btnLabelCollapsed : btnLabelNotCollapsed;
-
- this.innerHTML = this.innerHTML.replace(oldLabel, newLabel);
- this.classList.toggle("collapsed");
-
- targetElement.classList.toggle("invisible");
- });
-
- searxng.listen("click", ".media-loader", function (this: HTMLElement) {
- const target = this.getAttribute("data-target");
- if (!target) return;
-
- const iframeLoad = document.querySelector<HTMLIFrameElement>(`${target} > iframe`);
- assertElement(iframeLoad);
-
- const srctest = iframeLoad.getAttribute("src");
- if (!srctest) {
- const dataSrc = iframeLoad.getAttribute("data-src");
- if (dataSrc) {
- iframeLoad.setAttribute("src", dataSrc);
- }
- }
- });
-
- searxng.listen("click", "#copy_url", async function (this: HTMLElement) {
- const target = this.parentElement?.querySelector<HTMLPreElement>("pre");
- assertElement(target);
-
- await navigator.clipboard.writeText(target.innerText);
- const copiedText = this.dataset.copiedText;
- if (copiedText) {
- this.innerText = copiedText;
- }
- });
-
- searxng.selectImage = (resultElement: Element): void => {
- // add a class that can be evaluated in the CSS and indicates that the
- // detail view is open
- const resultsElement = document.getElementById("results");
- resultsElement?.classList.add("image-detail-open");
-
- // add a hash to the browser history so that pressing back doesn't return
- // to the previous page this allows us to dismiss the image details on
- // pressing the back button on mobile devices
- window.location.hash = "#image-viewer";
-
- searxng.scrollPageToSelected?.();
-
- // if there is no element given by the caller, stop here
- if (!resultElement) return;
-
- // find image element, if there is none, stop here
- const img = resultElement.querySelector<HTMLImageElement>(".result-images-source img");
- if (!img) return;
-
- // <img src="" data-src="http://example.org/image.jpg">
- const src = img.getAttribute("data-src");
- if (!src) return;
-
- // use thumbnail until full image loads
- const thumbnail = resultElement.querySelector<HTMLImageElement>(".image_thumbnail");
- if (thumbnail) {
- img.src = thumbnail.src;
- }
-
- // load full size image
- loadImage(src, () => {
- img.src = src;
- img.onerror = () => {
- img.src = `${searxng.settings.theme_static_path}/img/img_load_error.svg`;
- };
-
- img.removeAttribute("data-src");
- });
- };
+ // remove #image-viewer hash from url by navigating back
+ if (window.location.hash === "#image-viewer") {
+ window.history.back();
+ }
- searxng.closeDetail = (): void => {
- const resultsElement = document.getElementById("results");
- resultsElement?.classList.remove("image-detail-open");
+ mutable.scrollPageToSelected?.();
+};
- // remove #image-viewer hash from url by navigating back
- if (window.location.hash === "#image-viewer") {
- window.history.back();
- }
+listen("click", ".btn-collapse", function (this: HTMLElement) {
+ const btnLabelCollapsed = this.getAttribute("data-btn-text-collapsed");
+ const btnLabelNotCollapsed = this.getAttribute("data-btn-text-not-collapsed");
+ const target = this.getAttribute("data-target");
- searxng.scrollPageToSelected?.();
- };
+ if (!(target && btnLabelCollapsed && btnLabelNotCollapsed)) return;
+
+ const targetElement = document.querySelector<HTMLElement>(target);
+ assertElement(targetElement);
- searxng.listen("click", ".result-detail-close", (event: Event) => {
- event.preventDefault();
- searxng.closeDetail?.();
- });
-
- searxng.listen("click", ".result-detail-previous", (event: Event) => {
- event.preventDefault();
- searxng.selectPrevious?.(false);
- });
-
- searxng.listen("click", ".result-detail-next", (event: Event) => {
- event.preventDefault();
- searxng.selectNext?.(false);
- });
-
- // listen for the back button to be pressed and dismiss the image details when called
- window.addEventListener("hashchange", () => {
- if (window.location.hash !== "#image-viewer") {
- searxng.closeDetail?.();
- }
- });
-
- const swipeHorizontal = document.querySelectorAll<HTMLElement>(".swipe-horizontal");
- for (const element of swipeHorizontal) {
- searxng.listen("swiped-left", element, () => {
- searxng.selectNext?.(false);
- });
-
- searxng.listen("swiped-right", element, () => {
- searxng.selectPrevious?.(false);
- });
+ const isCollapsed = this.classList.contains("collapsed");
+ const newLabel = isCollapsed ? btnLabelNotCollapsed : btnLabelCollapsed;
+ const oldLabel = isCollapsed ? btnLabelCollapsed : btnLabelNotCollapsed;
+
+ this.innerHTML = this.innerHTML.replace(oldLabel, newLabel);
+ this.classList.toggle("collapsed");
+
+ targetElement.classList.toggle("invisible");
+});
+
+listen("click", ".media-loader", function (this: HTMLElement) {
+ const target = this.getAttribute("data-target");
+ if (!target) return;
+
+ const iframeLoad = document.querySelector<HTMLIFrameElement>(`${target} > iframe`);
+ assertElement(iframeLoad);
+
+ const srctest = iframeLoad.getAttribute("src");
+ if (!srctest) {
+ const dataSrc = iframeLoad.getAttribute("data-src");
+ if (dataSrc) {
+ iframeLoad.setAttribute("src", dataSrc);
}
+ }
+});
+
+listen("click", "#copy_url", async function (this: HTMLElement) {
+ const target = this.parentElement?.querySelector<HTMLPreElement>("pre");
+ assertElement(target);
+
+ await navigator.clipboard.writeText(target.innerText);
+ const copiedText = this.dataset.copiedText;
+ if (copiedText) {
+ this.innerText = copiedText;
+ }
+});
+
+listen("click", ".result-detail-close", (event: Event) => {
+ event.preventDefault();
+ mutable.closeDetail?.();
+});
+
+listen("click", ".result-detail-previous", (event: Event) => {
+ event.preventDefault();
+ mutable.selectPrevious?.(false);
+});
+
+listen("click", ".result-detail-next", (event: Event) => {
+ event.preventDefault();
+ mutable.selectNext?.(false);
+});
+
+// listen for the back button to be pressed and dismiss the image details when called
+window.addEventListener("hashchange", () => {
+ if (window.location.hash !== "#image-viewer") {
+ mutable.closeDetail?.();
+ }
+});
+
+const swipeHorizontal: NodeListOf<HTMLElement> = document.querySelectorAll<HTMLElement>(".swipe-horizontal");
+for (const element of swipeHorizontal) {
+ listen("swiped-left", element, () => {
+ mutable.selectNext?.(false);
+ });
+
+ listen("swiped-right", element, () => {
+ mutable.selectPrevious?.(false);
+ });
+}
+
+window.addEventListener(
+ "scroll",
+ () => {
+ const backToTopElement = document.getElementById("backToTop");
+ const resultsElement = document.getElementById("results");
- window.addEventListener(
- "scroll",
- () => {
- const backToTopElement = document.getElementById("backToTop");
- const resultsElement = document.getElementById("results");
-
- if (backToTopElement && resultsElement) {
- const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
- const isScrolling = scrollTop >= 100;
- resultsElement.classList.toggle("scrolling", isScrolling);
- }
- },
- true
- );
+ if (backToTopElement && resultsElement) {
+ const scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
+ const isScrolling = scrollTop >= 100;
+ resultsElement.classList.toggle("scrolling", isScrolling);
+ }
},
- { on: [searxng.endpoint === "results"] }
+ true
);
diff --git a/client/simple/src/js/main/search.ts b/client/simple/src/js/main/search.ts
index 5e68965b1..508dc702a 100644
--- a/client/simple/src/js/main/search.ts
+++ b/client/simple/src/js/main/search.ts
@@ -1,4 +1,4 @@
-import { assertElement, searxng } from "./00_toolkit.ts";
+import { assertElement, listen, settings } from "../core/toolkit.ts";
const submitIfQuery = (qInput: HTMLInputElement): void => {
if (qInput.value.length > 0) {
@@ -17,217 +17,88 @@ const createClearButton = (qInput: HTMLInputElement): void => {
updateClearButton(qInput, cs);
- searxng.listen("click", cs, (event: MouseEvent) => {
+ listen("click", cs, (event: MouseEvent) => {
event.preventDefault();
qInput.value = "";
qInput.focus();
updateClearButton(qInput, cs);
});
- searxng.listen("input", qInput, () => updateClearButton(qInput, cs), { passive: true });
+ 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);
+const qInput = document.getElementById("q") as HTMLInputElement | null;
+assertElement(qInput);
+
+const isMobile: boolean = window.matchMedia("(max-width: 50em)").matches;
+const isResultsPage: boolean = document.querySelector("main")?.id === "main_results";
+
+// focus search input on large screens
+if (!(isMobile || isResultsPage)) {
+ qInput.focus();
+}
+
+createClearButton(qInput);
+
+// 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));
}
-};
-
-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 ?? "";
- }
- }
- }
- }
- });
- }
- }
+ const timeRangeElement = document.getElementById("time_range");
+ if (timeRangeElement) {
+ listen("change", timeRangeElement, () => submitIfQuery(qInput));
+ }
- // 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 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) {
+ event.preventDefault();
+ button.classList.toggle("selected");
+ return;
}
- 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);
- }
- });
+ // deselect all other categories
+ for (const categoryButton of categoryButtons) {
+ categoryButton.classList.toggle("selected", categoryButton === button);
}
+ });
+}
- const form = document.querySelector<HTMLFormElement>("#search");
- assertElement(form);
+const form: HTMLFormElement | null = document.querySelector<HTMLFormElement>("#search");
+assertElement(form);
- // override form submit action to update the actually selected categories
- searxng.listen("submit", form, (event: Event) => {
- event.preventDefault();
+// 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_", ""));
+ 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(",");
- }
+ categoryValuesInput.value = categoryValues.join(",");
+ }
- form.submit();
- });
- },
- { on: [searxng.endpoint === "index" || searxng.endpoint === "results"] }
-);
+ form.submit();
+});