[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
This commit is contained in:
Ivan Gabaldon
2025-07-06 12:27:28 +02:00
committed by Markus Heiser
parent adc4361eb9
commit 60bd8b90f0
28 changed files with 1109 additions and 1039 deletions
+65 -66
View File
@@ -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);
if (resultElement && isImageResult(resultElement)) {
event.preventDefault();
mutable.selectImage?.(resultElement);
}
}
});
// 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 && !resultElement.hasAttribute("data-vim-selected")) {
highlightResult(resultElement)(true);
}
if (resultElement && isImageResult(resultElement)) {
event.preventDefault();
searxng.selectImage?.(resultElement);
mutable.selectImage?.(resultElement);
}
}
});
},
{ capture: true }
);
searxng.listen(
"focus",
".result a",
(event: Event) => {
if (!isElementInDetail(event.target as HTMLElement)) {
const resultElement = getResultElement(event.target as HTMLElement);
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 (resultElement && !resultElement.getAttribute("data-vim-selected")) {
highlightResult(resultElement)(true);
}
if (resultElement && isImageResult(resultElement)) {
searxng.selectImage?.(resultElement);
}
}
},
{ 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 (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);
}
});
searxng.scrollPageToSelected = scrollPageToSelected;
searxng.selectNext = highlightResult("down");
searxng.selectPrevious = highlightResult("up");
}
});
mutable.selectNext = highlightResult("down");
mutable.selectPrevious = highlightResult("up");