diff options
| author | Ivan Gabaldon <igabaldon@inetol.net> | 2025-07-06 12:27:28 +0200 |
|---|---|---|
| committer | Markus Heiser <markus.heiser@darmarIT.de> | 2025-08-18 16:38:32 +0200 |
| commit | 60bd8b90f04d5d825fc8ac279cb7fdfde9fe78ea (patch) | |
| tree | 19b2639638e7845597f9aa839eda39a456188a1c /client/simple/src/js/core | |
| parent | adc4361eb919604889dc0661e75ef6ac8cfc4d23 (diff) | |
[enh] theme/simple: custom router
Lay the foundation for loading scripts granularly depending on the endpoint it's
on.
Remove vendor specific prefixes as there are now managed by browserslist and
LightningCSS.
Enabled quite a few rules in Biome that don't come in recommended to better
catch issues and improve consistency.
Related:
- https://github.com/searxng/searxng/pull/5073#discussion_r2256037965
- https://github.com/searxng/searxng/pull/5073#discussion_r2256057100
Diffstat (limited to 'client/simple/src/js/core')
| -rw-r--r-- | client/simple/src/js/core/index.ts | 9 | ||||
| -rw-r--r-- | client/simple/src/js/core/listener.ts | 5 | ||||
| -rw-r--r-- | client/simple/src/js/core/router.ts | 38 | ||||
| -rw-r--r-- | client/simple/src/js/core/toolkit.ts | 140 |
4 files changed, 192 insertions, 0 deletions
diff --git a/client/simple/src/js/core/index.ts b/client/simple/src/js/core/index.ts new file mode 100644 index 000000000..a4021beb9 --- /dev/null +++ b/client/simple/src/js/core/index.ts @@ -0,0 +1,9 @@ +/** + * @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 "./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(); |