Compare commits

...

4 Commits

Author SHA1 Message Date
Markus Heiser
cfb6649b90 [build] /static 2025-08-21 14:04:13 +02:00
Amit Katyal
5ca70ca17e [feat] client/simple: move cursor to end of search input on mobile
On mobile devices, when the search input is focused, move the cursor
to the end of the existing text. This improves the user experience by
making it easier to edit or append to the current query without
manually moving the cursor first.

Closes: https://github.com/searxng/searxng/issues/5112
2025-08-21 14:04:13 +02:00
Markus Heiser
22c2c93274 [build] /static 2025-08-21 09:07:08 +02:00
Markus Heiser
d2b3c92e81 [fix] move initial "JS is enabled?" (no-js) to client side
To avoid an `unsafe-inline` in the CSP header, the JS code must be moved to the
client side [1].

The `<script>` tag at the end of the HTML originates from the old implementation
of the JS client. Since PR-5073 [2] was merged, the `type` is now `module`, and
the tag must be moved to the beginning of the HTML.

> We need to inline this "JS is enabled?" thing to prevent layout shifts and
> temporary "no JS enabled" visuals as ESM scripts loads and evals everything
> deferred from initial DOM render [3]

That's true in theory, but in practice, this effect is unnoticeable because it's
masked by another effect (which we can't avoid): If we load the page with a
severely throttled connection, the HTML (result list) takes a long time to
load. Then the CSS is loaded, which also takes longer. Until the CSS has loaded,
there's no layout. A layout shift is therefore largely determined by the loading
of the HTML and CSS itself.

The running times of the ESM script can be neglected compared to the loading
times of HTML & CSS.

[1] https://github.com/searxng/searxng-docker/pull/424#issuecomment-3199494256
[2] https://github.com/searxng/searxng/pull/5073
[3] https://github.com/searxng/searxng-docker/pull/424#issuecomment-3199622504
2025-08-21 09:07:08 +02:00
8 changed files with 26 additions and 10 deletions

View File

@ -1,5 +1,6 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import "./nojs.ts";
import "./router.ts";
import "./toolkit.ts";
import "./listener.ts";

View File

@ -0,0 +1,8 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import { ready } from "./toolkit.ts";
ready(() => {
document.documentElement.classList.remove("no-js");
document.documentElement.classList.add("js");
});

View File

@ -40,6 +40,18 @@ if (!(isMobile || isResultsPage)) {
qInput.focus();
}
// On mobile, move cursor to the end of the input on focus
if (isMobile) {
listen("focus", qInput, () => {
// Defer cursor move until the next frame to prevent a visual jump
requestAnimationFrame(() => {
const end = qInput.value.length;
qInput.setSelectionRange(end, end);
qInput.scrollLeft = qInput.scrollWidth;
});
});
}
createClearButton(qInput);
// Additionally to searching when selecting a new category, we also

View File

@ -1,2 +1,2 @@
import{c as e,e as t,g as n}from"./searxng.core.min.js";const r=e=>{if(e.value.length>0){let e=document.getElementById(`search`);e?.submit()}},i=(e,t)=>{t.classList.toggle(`empty`,e.value.length===0)},a=n=>{let r=document.getElementById(`clear_search`);e(r),i(n,r),t(`click`,r,e=>{e.preventDefault(),n.value=``,n.focus(),i(n,r)}),t(`input`,n,()=>i(n,r),{passive:!0})},o=document.getElementById(`q`);e(o);const s=window.matchMedia(`(max-width: 50em)`).matches,c=document.querySelector(`main`)?.id===`main_results`;if(s||c||o.focus(),a(o),n.search_on_category_select&&document.querySelector(`.search_filters`)){let e=document.getElementById(`safesearch`);e&&t(`change`,e,()=>r(o));let n=document.getElementById(`time_range`);n&&t(`change`,n,()=>r(o));let i=document.getElementById(`language`);i&&t(`change`,i,()=>r(o))}const l=[...document.querySelectorAll(`button.category_button`)];for(let e of l)t(`click`,e,t=>{if(t.shiftKey){t.preventDefault(),e.classList.toggle(`selected`);return}for(let t of l)t.classList.toggle(`selected`,t===e)});const u=document.querySelector(`#search`);e(u),t(`submit`,u,e=>{e.preventDefault();let t=document.querySelector(`#selected-categories`);if(t){let e=l.filter(e=>e.classList.contains(`selected`)).map(e=>e.name.replace(`category_`,``));t.value=e.join(`,`)}u.submit()});
import{c as e,e as t,g as n}from"./searxng.core.min.js";const r=e=>{if(e.value.length>0){let e=document.getElementById(`search`);e?.submit()}},i=(e,t)=>{t.classList.toggle(`empty`,e.value.length===0)},a=n=>{let r=document.getElementById(`clear_search`);e(r),i(n,r),t(`click`,r,e=>{e.preventDefault(),n.value=``,n.focus(),i(n,r)}),t(`input`,n,()=>i(n,r),{passive:!0})},o=document.getElementById(`q`);e(o);const s=window.matchMedia(`(max-width: 50em)`).matches,c=document.querySelector(`main`)?.id===`main_results`;if(s||c||o.focus(),s&&t(`focus`,o,()=>{requestAnimationFrame(()=>{let e=o.value.length;o.setSelectionRange(e,e),o.scrollLeft=o.scrollWidth})}),a(o),n.search_on_category_select&&document.querySelector(`.search_filters`)){let e=document.getElementById(`safesearch`);e&&t(`change`,e,()=>r(o));let n=document.getElementById(`time_range`);n&&t(`change`,n,()=>r(o));let i=document.getElementById(`language`);i&&t(`change`,i,()=>r(o))}const l=[...document.querySelectorAll(`button.category_button`)];for(let e of l)t(`click`,e,t=>{if(t.shiftKey){t.preventDefault(),e.classList.toggle(`selected`);return}for(let t of l)t.classList.toggle(`selected`,t===e)});const u=document.querySelector(`#search`);e(u),t(`submit`,u,e=>{e.preventDefault();let t=document.querySelector(`#selected-categories`);if(t){let e=l.filter(e=>e.classList.contains(`selected`)).map(e=>e.name.replace(`category_`,``));t.value=e.join(`,`)}u.submit()});
//# sourceMappingURL=search.min.js.map

File diff suppressed because one or more lines are too long

View File

@ -1,2 +1,2 @@
const e={index:`index`,results:`results`,preferences:`preferences`,unknown:`unknown`},t={closeDetail:void 0,scrollPageToSelected:void 0,selectImage:void 0,selectNext:void 0,selectPrevious:void 0},n=()=>{let t=document.querySelector(`meta[name="endpoint"]`)?.getAttribute(`content`);return t&&t in e?t:e.unknown},r=()=>{let e=document.querySelector(`script[client_settings]`)?.getAttribute(`client_settings`);if(!e)return{};try{return JSON.parse(atob(e))}catch(e){return console.error(`Failed to load client_settings:`,e),{}}},i=e=>{if(!e)throw Error(`Bad assertion: DOM element not found`)},a=async(e,t,n)=>{let r=new AbortController,i=setTimeout(()=>r.abort(),n?.timeout??3e4),a=await fetch(t,{body:n?.body,method:e,signal:r.signal}).finally(()=>clearTimeout(i));if(!a.ok)throw Error(a.statusText);return a},o=(e,t,n,r)=>{if(typeof t!=`string`){t.addEventListener(e,n,r);return}document.addEventListener(e,e=>{for(let r of e.composedPath())if(r instanceof HTMLElement&&r.matches(t)){try{n.call(r,e)}catch(e){console.error(e)}break}},r)},s=(e,t)=>{for(let e of t?.on??[])if(!e)return;document.readyState===`loading`?o(`DOMContentLoaded`,document,e,{once:!0}):e()},c=n(),l=r(),u=function(e){return`/static/themes/simple/`+e},d={},f=function(e,t,n){let r=Promise.resolve();if(t&&t.length>0){let e=document.getElementsByTagName(`link`),i=document.querySelector(`meta[property=csp-nonce]`),a=i?.nonce||i?.getAttribute(`nonce`);function o(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}r=o(t.map(t=>{if(t=u(t,n),t in d)return;d[t]=!0;let r=t.endsWith(`.css`),i=r?`[rel="stylesheet"]`:``,o=!!n;if(o)for(let n=e.length-1;n>=0;n--){let i=e[n];if(i.href===t&&(!r||i.rel===`stylesheet`))return}else if(document.querySelector(`link[href="${t}"]${i}`))return;let s=document.createElement(`link`);if(s.rel=r?`stylesheet`:`modulepreload`,r||(s.as=`script`),s.crossOrigin=``,s.href=t,a&&s.setAttribute(`nonce`,a),document.head.appendChild(s),r)return new Promise((e,n)=>{s.addEventListener(`load`,e),s.addEventListener(`error`,()=>n(Error(`Unable to preload CSS for ${t}`)))})}))}function i(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return r.then(t=>{for(let e of t||[]){if(e.status!==`rejected`)continue;i(e.reason)}return e().catch(i)})};s(()=>{f(()=>import(`./keyboard.min.js`),[]),f(()=>import(`./search.min.js`),[]),l.autocomplete&&f(()=>import(`./autocomplete.min.js`),[])},{on:[c===e.index]}),s(()=>{f(()=>import(`./keyboard.min.js`),[]),f(()=>import(`./mapresult.min.js`),[]),f(()=>import(`./results.min.js`),[]),f(()=>import(`./search.min.js`),[]),l.infinite_scroll&&f(()=>import(`./infinite_scroll.min.js`),[]),l.autocomplete&&f(()=>import(`./autocomplete.min.js`),[])},{on:[c===e.results]}),s(()=>{f(()=>import(`./preferences.min.js`),[])},{on:[c===e.preferences]}),o(`click`,`.close`,function(){this.parentNode?.classList.add(`invisible`)});export{f as b,i as c,a as d,o as e,t as f,l as g};
const e={index:`index`,results:`results`,preferences:`preferences`,unknown:`unknown`},t={closeDetail:void 0,scrollPageToSelected:void 0,selectImage:void 0,selectNext:void 0,selectPrevious:void 0},n=()=>{let t=document.querySelector(`meta[name="endpoint"]`)?.getAttribute(`content`);return t&&t in e?t:e.unknown},r=()=>{let e=document.querySelector(`script[client_settings]`)?.getAttribute(`client_settings`);if(!e)return{};try{return JSON.parse(atob(e))}catch(e){return console.error(`Failed to load client_settings:`,e),{}}},i=e=>{if(!e)throw Error(`Bad assertion: DOM element not found`)},a=async(e,t,n)=>{let r=new AbortController,i=setTimeout(()=>r.abort(),n?.timeout??3e4),a=await fetch(t,{body:n?.body,method:e,signal:r.signal}).finally(()=>clearTimeout(i));if(!a.ok)throw Error(a.statusText);return a},o=(e,t,n,r)=>{if(typeof t!=`string`){t.addEventListener(e,n,r);return}document.addEventListener(e,e=>{for(let r of e.composedPath())if(r instanceof HTMLElement&&r.matches(t)){try{n.call(r,e)}catch(e){console.error(e)}break}},r)},s=(e,t)=>{for(let e of t?.on??[])if(!e)return;document.readyState===`loading`?o(`DOMContentLoaded`,document,e,{once:!0}):e()},c=n(),l=r();s(()=>{document.documentElement.classList.remove(`no-js`),document.documentElement.classList.add(`js`)});const u=function(e){return`/static/themes/simple/`+e},d={},f=function(e,t,n){let r=Promise.resolve();if(t&&t.length>0){let e=document.getElementsByTagName(`link`),i=document.querySelector(`meta[property=csp-nonce]`),a=i?.nonce||i?.getAttribute(`nonce`);function o(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}r=o(t.map(t=>{if(t=u(t,n),t in d)return;d[t]=!0;let r=t.endsWith(`.css`),i=r?`[rel="stylesheet"]`:``,o=!!n;if(o)for(let n=e.length-1;n>=0;n--){let i=e[n];if(i.href===t&&(!r||i.rel===`stylesheet`))return}else if(document.querySelector(`link[href="${t}"]${i}`))return;let s=document.createElement(`link`);if(s.rel=r?`stylesheet`:`modulepreload`,r||(s.as=`script`),s.crossOrigin=``,s.href=t,a&&s.setAttribute(`nonce`,a),document.head.appendChild(s),r)return new Promise((e,n)=>{s.addEventListener(`load`,e),s.addEventListener(`error`,()=>n(Error(`Unable to preload CSS for ${t}`)))})}))}function i(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return r.then(t=>{for(let e of t||[]){if(e.status!==`rejected`)continue;i(e.reason)}return e().catch(i)})};s(()=>{f(()=>import(`./keyboard.min.js`),[]),f(()=>import(`./search.min.js`),[]),l.autocomplete&&f(()=>import(`./autocomplete.min.js`),[])},{on:[c===e.index]}),s(()=>{f(()=>import(`./keyboard.min.js`),[]),f(()=>import(`./mapresult.min.js`),[]),f(()=>import(`./results.min.js`),[]),f(()=>import(`./search.min.js`),[]),l.infinite_scroll&&f(()=>import(`./infinite_scroll.min.js`),[]),l.autocomplete&&f(()=>import(`./autocomplete.min.js`),[])},{on:[c===e.results]}),s(()=>{f(()=>import(`./preferences.min.js`),[])},{on:[c===e.preferences]}),o(`click`,`.close`,function(){this.parentNode?.classList.add(`invisible`)});export{f as b,i as c,a as d,o as e,t as f,l as g};
//# sourceMappingURL=searxng.core.min.js.map

File diff suppressed because one or more lines are too long

View File

@ -10,6 +10,7 @@
<meta name="robots" content="noarchive">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}{% endblock %}{{ instance_name }}</title>
<script type="module" src="{{ url_for('static', filename='js/searxng.core.min.js') }}" client_settings="{{ client_settings }}"></script>
{% block meta %}{% endblock %}
{% if rtl %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/searxng-rtl.min.css') }}" type="text/css" media="screen">
@ -19,11 +20,6 @@
{% if get_setting('server.limiter') or get_setting('server.public_instance') %}
<link rel="stylesheet" href="{{ url_for('client_token', token=link_token) }}" type="text/css">
{% endif %}
<script>
// update the css
document.documentElement.classList.remove('no-js');
document.documentElement.classList.add('js');
</script>
{% block head %}
<link title="{{ instance_name }}" type="application/opensearchdescription+xml" rel="search" href="{{ opensearch_url }}">
{% endblock %}
@ -83,6 +79,5 @@
{% endfor %}
</p>
</footer>
<script type="module" src="{{ url_for('static', filename='js/searxng.core.min.js') }}" client_settings="{{ client_settings }}"></script>
</body>
</html>