From 60bd8b90f04d5d825fc8ac279cb7fdfde9fe78ea Mon Sep 17 00:00:00 2001 From: Ivan Gabaldon Date: Sun, 6 Jul 2025 12:27:28 +0200 Subject: [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 --- client/simple/src/js/main/results.ts | 324 +++++++++++++++++------------------ 1 file changed, 159 insertions(+), 165 deletions(-) (limited to 'client/simple/src/js/main/results.ts') 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(".result-images-source img"); + if (!imgElement) return; + + // use thumbnail until full image loads + const thumbnail = resultElement.querySelector(".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 = + document.querySelectorAll("#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("#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("#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("#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(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(`${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("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(".result-images-source img"); - if (!img) return; - - // - const src = img.getAttribute("data-src"); - if (!src) return; - - // use thumbnail until full image loads - const thumbnail = resultElement.querySelector(".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(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(".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(`${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("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 = document.querySelectorAll(".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 ); -- cgit v1.2.3