summaryrefslogtreecommitdiff
path: root/client/simple/src
diff options
context:
space:
mode:
Diffstat (limited to 'client/simple/src')
-rw-r--r--client/simple/src/js/core/index.ts (renamed from client/simple/src/js/main/index.ts)10
-rw-r--r--client/simple/src/js/core/listener.ts5
-rw-r--r--client/simple/src/js/core/router.ts38
-rw-r--r--client/simple/src/js/core/toolkit.ts140
-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/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
-rw-r--r--client/simple/src/less/code.less6
-rw-r--r--client/simple/src/less/mixins.less4
-rw-r--r--client/simple/src/less/rss.less8
-rw-r--r--client/simple/src/less/search.less11
-rw-r--r--client/simple/src/less/style-rtl.less8
-rw-r--r--client/simple/src/less/toolkit.less38
18 files changed, 750 insertions, 779 deletions
diff --git a/client/simple/src/js/main/index.ts b/client/simple/src/js/core/index.ts
index 4dc86b63b..a4021beb9 100644
--- a/client/simple/src/js/main/index.ts
+++ b/client/simple/src/js/core/index.ts
@@ -4,10 +4,6 @@
* @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";
+import "./router.ts";
+import "./toolkit.ts";
+import "./listener.ts";
diff --git a/client/simple/src/js/core/listener.ts b/client/simple/src/js/core/listener.ts
new file mode 100644
index 000000000..fb41cfa88
--- /dev/null
+++ b/client/simple/src/js/core/listener.ts
@@ -0,0 +1,5 @@
+import { listen } from "./toolkit.ts";
+
+listen("click", ".close", function (this: HTMLElement) {
+ (this.parentNode as HTMLElement)?.classList.add("invisible");
+});
diff --git a/client/simple/src/js/core/router.ts b/client/simple/src/js/core/router.ts
new file mode 100644
index 000000000..05c49ed07
--- /dev/null
+++ b/client/simple/src/js/core/router.ts
@@ -0,0 +1,38 @@
+import { Endpoints, endpoint, ready, settings } from "./toolkit.ts";
+
+ready(
+ () => {
+ import("../main/keyboard.ts");
+ import("../main/search.ts");
+
+ if (settings.autocomplete) {
+ import("../main/autocomplete.ts");
+ }
+ },
+ { on: [endpoint === Endpoints.index] }
+);
+
+ready(
+ () => {
+ import("../main/keyboard.ts");
+ import("../main/mapresult.ts");
+ import("../main/results.ts");
+ import("../main/search.ts");
+
+ if (settings.infinite_scroll) {
+ import("../main/infinite_scroll.ts");
+ }
+
+ if (settings.autocomplete) {
+ import("../main/autocomplete.ts");
+ }
+ },
+ { on: [endpoint === Endpoints.results] }
+);
+
+ready(
+ () => {
+ import("../main/preferences.ts");
+ },
+ { on: [endpoint === Endpoints.preferences] }
+);
diff --git a/client/simple/src/js/core/toolkit.ts b/client/simple/src/js/core/toolkit.ts
new file mode 100644
index 000000000..0e95eed14
--- /dev/null
+++ b/client/simple/src/js/core/toolkit.ts
@@ -0,0 +1,140 @@
+import type { KeyBindingLayout } from "../main/keyboard.ts";
+
+// synced with searx/webapp.py get_client_settings
+type Settings = {
+ advanced_search?: boolean;
+ autocomplete?: string;
+ autocomplete_min?: number;
+ doi_resolver?: string;
+ favicon_resolver?: string;
+ hotkeys?: KeyBindingLayout;
+ infinite_scroll?: boolean;
+ method?: "GET" | "POST";
+ query_in_title?: boolean;
+ results_on_new_tab?: boolean;
+ safesearch?: 0 | 1 | 2;
+ search_on_category_select?: boolean;
+ theme?: string;
+ theme_static_path?: string;
+ translations?: Record<string, string>;
+ url_formatting?: "pretty" | "full" | "host";
+};
+
+type HTTPOptions = {
+ body?: BodyInit;
+ timeout?: number;
+};
+
+type ReadyOptions = {
+ // all values must be truthy for the callback to be executed
+ on?: (boolean | undefined)[];
+};
+
+type AssertElement = (element?: HTMLElement | null) => asserts element is HTMLElement;
+
+export type EndpointsKeys = keyof typeof Endpoints;
+
+export const Endpoints = {
+ index: "index",
+ results: "results",
+ preferences: "preferences",
+ unknown: "unknown"
+} as const;
+
+export const mutable = {
+ closeDetail: undefined as (() => void) | undefined,
+ scrollPageToSelected: undefined as (() => void) | undefined,
+ selectImage: undefined as ((resultElement: HTMLElement) => void) | undefined,
+ selectNext: undefined as ((openDetailView?: boolean) => void) | undefined,
+ selectPrevious: undefined as ((openDetailView?: boolean) => void) | undefined
+};
+
+const getEndpoint = (): EndpointsKeys => {
+ const metaEndpoint = document.querySelector('meta[name="endpoint"]')?.getAttribute("content");
+
+ if (metaEndpoint && metaEndpoint in Endpoints) {
+ return metaEndpoint as EndpointsKeys;
+ }
+
+ return Endpoints.unknown;
+};
+
+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 {};
+ }
+};
+
+export const assertElement: AssertElement = (element?: HTMLElement | null): asserts element is HTMLElement => {
+ if (!element) {
+ throw new Error("Bad assertion: DOM element not found");
+ }
+};
+
+export const http = async (method: string, url: string | URL, options?: HTTPOptions): Promise<Response> => {
+ const controller = new AbortController();
+ const timeoutId = setTimeout(() => controller.abort(), options?.timeout ?? 30_000);
+
+ const res = await fetch(url, {
+ body: options?.body,
+ method: method,
+ signal: controller.signal
+ }).finally(() => clearTimeout(timeoutId));
+ if (!res.ok) {
+ throw new Error(res.statusText);
+ }
+
+ return res;
+};
+
+export const listen = <K extends keyof DocumentEventMap, E extends HTMLElement>(
+ type: string | K,
+ target: string | Document | E,
+ listener: (this: E, event: DocumentEventMap[K]) => void | Promise<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 HTMLElement && node.matches(target)) {
+ try {
+ listener.call(node as E, event as DocumentEventMap[K]);
+ } catch (error) {
+ console.error(error);
+ }
+ break;
+ }
+ }
+ },
+ options
+ );
+};
+
+export const ready = (callback: () => void, options?: ReadyOptions): void => {
+ for (const condition of options?.on ?? []) {
+ if (!condition) {
+ return;
+ }
+ }
+
+ if (document.readyState !== "loading") {
+ callback();
+ } else {
+ listen("DOMContentLoaded", document, callback, { once: true });
+ }
+};
+
+export const endpoint: EndpointsKeys = getEndpoint();
+export const settings: Settings = getSettings();
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/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();
+});
diff --git a/client/simple/src/less/code.less b/client/simple/src/less/code.less
index d83bb1f6f..20d8c3d1e 100644
--- a/client/simple/src/less/code.less
+++ b/client/simple/src/less/code.less
@@ -18,11 +18,7 @@
cursor: default;
&::selection {
- background: transparent; /* WebKit/Blink Browsers */
- }
-
- &::-moz-selection {
- background: transparent; /* Gecko Browsers */
+ background: transparent;
}
margin-right: 8px;
diff --git a/client/simple/src/less/mixins.less b/client/simple/src/less/mixins.less
index a4bae7128..b2e0f6b16 100644
--- a/client/simple/src/less/mixins.less
+++ b/client/simple/src/less/mixins.less
@@ -2,9 +2,6 @@
// Mixins
.text-size-adjust (@property: 100%) {
- -webkit-text-size-adjust: @property;
- -ms-text-size-adjust: @property;
- -moz-text-size-adjust: @property;
text-size-adjust: @property;
}
@@ -22,7 +19,6 @@
// disable user selection
.disable-user-select () {
- -webkit-touch-callout: none;
user-select: none;
}
diff --git a/client/simple/src/less/rss.less b/client/simple/src/less/rss.less
index 0bc6622e3..26f960f10 100644
--- a/client/simple/src/less/rss.less
+++ b/client/simple/src/less/rss.less
@@ -1,12 +1,6 @@
@import (inline) "../../node_modules/normalize.css/normalize.css";
@import "definitions.less";
-
-.text-size-adjust (@property: 100%) {
- -webkit-text-size-adjust: @property;
- -ms-text-size-adjust: @property;
- -moz-text-size-adjust: @property;
- text-size-adjust: @property;
-}
+@import "mixins.less";
// Reset padding and margin
html,
diff --git a/client/simple/src/less/search.less b/client/simple/src/less/search.less
index bc49ffadc..07dbf535b 100644
--- a/client/simple/src/less/search.less
+++ b/client/simple/src/less/search.less
@@ -196,11 +196,6 @@ html.no-js #clear_search.hide_if_nojs {
.ltr-rounded-left-corners(0.8rem);
}
-#q::-ms-clear,
-#q::-webkit-search-cancel-button {
- display: none;
-}
-
#send_search {
.ltr-rounded-right-corners(0.8rem);
@@ -271,7 +266,6 @@ html.no-js #clear_search.hide_if_nojs {
width: 100%;
.ltr-text-align-left();
overflow: scroll hidden;
- -webkit-overflow-scrolling: touch;
}
}
}
@@ -374,11 +368,6 @@ html.no-js #clear_search.hide_if_nojs {
#categories {
.disable-user-select;
-
- &::-webkit-scrollbar {
- width: 0;
- height: 0;
- }
}
#categories_container {
diff --git a/client/simple/src/less/style-rtl.less b/client/simple/src/less/style-rtl.less
index 7ac1e6e20..b4b4a946f 100644
--- a/client/simple/src/less/style-rtl.less
+++ b/client/simple/src/less/style-rtl.less
@@ -129,13 +129,7 @@
}
// select HTML element
-@supports (
- (background-position-x: 100%) and
- (
- (appearance: none) or (-webkit-appearance: none) or
- (-moz-appearance: none)
- )
-) {
+@supports ((background-position-x: 100%) and ((appearance: none))) {
select {
border-width: 0 0 0 2rem;
background-position-x: -2rem;
diff --git a/client/simple/src/less/toolkit.less b/client/simple/src/less/toolkit.less
index f9bdbf70c..1782ecdfa 100644
--- a/client/simple/src/less/toolkit.less
+++ b/client/simple/src/less/toolkit.less
@@ -156,9 +156,7 @@ div.selectable_url {
td {
padding: 0 1em 0 0;
- padding-top: 0;
.ltr-padding-right(1rem);
- padding-bottom: 0;
.ltr-padding-left(0);
}
@@ -307,7 +305,7 @@ html body .tabs > input:checked {
}
~ label {
- position: inherited;
+ position: inherit;
background: inherit;
border-bottom: 2px solid transparent;
font-weight: normal;
@@ -347,17 +345,9 @@ select {
}
}
-@supports (
- (background-position-x: 100%) and
- (
- (appearance: none) or (-webkit-appearance: none) or
- (-moz-appearance: none)
- )
-) {
+@supports ((background-position-x: 100%) and ((appearance: none))) {
select {
appearance: none;
- -webkit-appearance: none;
- -moz-appearance: none;
border-width: 0 2rem 0 0;
border-color: transparent;
background: data-uri("image/svg+xml;charset=UTF-8", @select-light-svg-path)
@@ -400,8 +390,6 @@ select {
/* -- checkbox-onoff -- */
input.checkbox-onoff[type="checkbox"] {
- -webkit-appearance: none;
- -moz-appearance: none;
appearance: none;
cursor: pointer;
display: inline-block;
@@ -475,8 +463,6 @@ input.checkbox-onoff.reversed-checkbox[type="checkbox"] {
/* -- checkbox -- */
@supports (transform: rotate(-45deg)) {
input[type="checkbox"]:not(.checkbox-onoff) {
- -webkit-appearance: none;
- -moz-appearance: none;
appearance: none;
width: 20px;
@@ -549,33 +535,16 @@ input.checkbox-onoff.reversed-checkbox[type="checkbox"] {
border-right: 0.5em solid var(--color-toolkit-loader-border);
border-bottom: 0.5em solid var(--color-toolkit-loader-border);
border-left: 0.5em solid var(--color-toolkit-loader-borderleft);
- -webkit-transform: translateZ(0);
- -ms-transform: translateZ(0);
transform: translateZ(0);
- -webkit-animation: load8 1.2s infinite linear;
animation: load8 1.2s infinite linear;
}
-@-webkit-keyframes load8 {
- 0% {
- -webkit-transform: rotate(0deg);
- transform: rotate(0deg);
- }
-
- 100% {
- -webkit-transform: rotate(360deg);
- transform: rotate(360deg);
- }
-}
-
@keyframes load8 {
0% {
- -webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
- -webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@@ -606,9 +575,6 @@ td:hover .engine-tooltip,
margin: 0;
padding: 0 0.125rem 0 4rem;
width: 100%;
- width: -moz-available;
- width: -webkit-fill-available;
- width: fill;
flex-flow: row nowrap;
align-items: center;
display: inline-flex;