From c8dc7a9445c0bc7a618169fcc34c537f1f054e38 Mon Sep 17 00:00:00 2001 From: Bnyro Date: Mon, 17 Mar 2025 20:19:13 +0100 Subject: [PATCH] [refactor] search.js: use custom auto completion implementation --- client/simple/package.json | 3 - client/simple/src/js/main/search.js | 192 ++++++++++------------ client/simple/src/less/autocomplete.less | 3 +- client/simple/src/less/definitions.less | 3 +- client/simple/src/less/search.less | 8 +- searx/templates/simple/search.html | 1 + searx/templates/simple/simple_search.html | 1 + 7 files changed, 99 insertions(+), 112 deletions(-) diff --git a/client/simple/package.json b/client/simple/package.json index 52b793395..19c55fb56 100644 --- a/client/simple/package.json +++ b/client/simple/package.json @@ -34,8 +34,5 @@ "vite-plugin-stylelint": "^6.0.0", "webpack": "^5.99.5", "webpack-cli": "^6.0.1" - }, - "dependencies": { - "autocomplete-js": "^2.7.1" } } diff --git a/client/simple/src/js/main/search.js b/client/simple/src/js/main/search.js index aca146c41..f57dce481 100644 --- a/client/simple/src/js/main/search.js +++ b/client/simple/src/js/main/search.js @@ -1,8 +1,6 @@ /* SPDX-License-Identifier: AGPL-3.0-or-later */ /* exported AutoComplete */ -import AutoComplete from "../../../node_modules/autocomplete-js/dist/autocomplete.js"; - (function (w, d, searxng) { 'use strict'; @@ -38,8 +36,62 @@ import AutoComplete from "../../../node_modules/autocomplete-js/dist/autocomple qinput.addEventListener('input', updateClearButton, false); } + const fetchResults = async (query) => { + let request; + if (searxng.settings.method === 'GET') { + const reqParams = new URLSearchParams(); + reqParams.append("q", query); + request = fetch("./autocompleter?" + reqParams.toString()); + } else { + const formData = new FormData(); + formData.append("q", query); + request = fetch("./autocompleter", { + method: 'POST', + body: formData, + }); + } + + request.then(async function (response) { + const results = await response.json(); + + if (!results) return; + + const autocomplete = d.querySelector(".autocomplete"); + const autocompleteList = d.querySelector(".autocomplete ul"); + autocomplete.classList.add("open"); + autocompleteList.innerHTML = ""; + + // show an error message that no result was found + if (!results[1] || results[1].length == 0) { + const noItemFoundMessage = document.createElement("li"); + noItemFoundMessage.classList.add('no-item-found'); + noItemFoundMessage.innerHTML = searxng.settings.translations.no_item_found; + autocompleteList.appendChild(noItemFoundMessage); + return; + } + + for (let result of results[1]) { + const li = document.createElement("li"); + li.innerText = result; + + searxng.on(li, 'mousedown', () => { + qinput.value = result; + const form = d.querySelector("#search"); + form.submit(); + autocomplete.classList.remove('open'); + }) + autocompleteList.appendChild(li); + } + }); + } + searxng.ready(function () { + // focus search input on large screens + if (!isMobile) document.getElementById("q").focus(); + qinput = d.getElementById(qinput_id); + const autocomplete = d.querySelector(".autocomplete"); + const autocompleteList = d.querySelector(".autocomplete ul"); if (qinput !== null) { // clear button @@ -47,109 +99,45 @@ import AutoComplete from "../../../node_modules/autocomplete-js/dist/autocomple // autocompleter if (searxng.settings.autocomplete) { - searxng.autocomplete = AutoComplete.call(w, { - Url: "./autocompleter", - EmptyMessage: searxng.settings.translations.no_item_found, - HttpMethod: searxng.settings.method, - HttpHeaders: { - "Content-type": "application/x-www-form-urlencoded", - "X-Requested-With": "XMLHttpRequest" - }, - MinChars: searxng.settings.autocomplete_min, - Delay: 300, - _Position: function () {}, - _Open: function () { - var params = this; - Array.prototype.forEach.call(this.DOMResults.getElementsByTagName("li"), function (li) { - if (li.getAttribute("class") != "locked") { - li.onmousedown = function () { - params._Select(li); - }; - } - }); - }, - _Select: function (item) { - AutoComplete.defaults._Select.call(this, item); - var form = item.closest('form'); - if (form) { - form.submit(); + searxng.on(qinput, 'input', () => { + const query = qinput.value; + if (query.length < searxng.settings.autocomplete_min) return; + + setTimeout(() => { + if (query == qinput.value) fetchResults(query); + }, 300); + }); + + searxng.on(qinput, 'keyup', (e) => { + let currentIndex = -1; + const listItems = autocompleteList.children; + for (let i = 0; i < listItems.length; i++) { + if (listItems[i].classList.contains('active')) { + currentIndex = i; + break; } - }, - _MinChars: function () { - if (this.Input.value.indexOf('!') > -1) { - return 0; - } else { - return AutoComplete.defaults._MinChars.call(this); - } - }, - KeyboardMappings: Object.assign({}, AutoComplete.defaults.KeyboardMappings, { - "KeyUpAndDown_up": Object.assign({}, AutoComplete.defaults.KeyboardMappings.KeyUpAndDown_up, { - Callback: function (event) { - AutoComplete.defaults.KeyboardMappings.KeyUpAndDown_up.Callback.call(this, event); - var liActive = this.DOMResults.querySelector("li.active"); - if (liActive) { - AutoComplete.defaults._Select.call(this, liActive); - } - }, - }), - "Tab": Object.assign({}, AutoComplete.defaults.KeyboardMappings.Enter, { - Conditions: [{ - Is: 9, - Not: false - }], - Callback: function (event) { - if (this.DOMResults.getAttribute("class").indexOf("open") != -1) { - var liActive = this.DOMResults.querySelector("li.active"); - if (liActive !== null) { - AutoComplete.defaults._Select.call(this, liActive); - event.preventDefault(); - } - } - }, - }) - }), - }, "#" + qinput_id); - } - - /* - Monkey patch autocomplete.js to fix a bug - With the POST method, the values are not URL encoded: query like "1 + 1" are sent as "1 1" since space are URL encoded as plus. - See HTML specifications: - * HTML5: https://url.spec.whatwg.org/#concept-urlencoded-serializer - * HTML4: https://www.w3.org/TR/html401/interact/forms.html#h-17.13.4.1 - - autocomplete.js does not URL encode the name and values: - https://github.com/autocompletejs/autocomplete.js/blob/87069524f3b95e68f1b54d8976868e0eac1b2c83/src/autocomplete.ts#L665 - - The monkey patch overrides the compiled version of the ajax function. - See https://github.com/autocompletejs/autocomplete.js/blob/87069524f3b95e68f1b54d8976868e0eac1b2c83/dist/autocomplete.js#L143-L158 - The patch changes only the line 156 from - params.Request.send(params._QueryArg() + "=" + params._Pre()); - to - params.Request.send(encodeURIComponent(params._QueryArg()) + "=" + encodeURIComponent(params._Pre())); - - Related to: - * https://github.com/autocompletejs/autocomplete.js/issues/78 - * https://github.com/searxng/searxng/issues/1695 - */ - AutoComplete.prototype.ajax = function (params, request, timeout) { - if (timeout === void 0) { timeout = true; } - if (params.$AjaxTimer) { - window.clearTimeout(params.$AjaxTimer); - } - if (timeout === true) { - params.$AjaxTimer = window.setTimeout(AutoComplete.prototype.ajax.bind(null, params, request, false), params.Delay); - } else { - if (params.Request) { - params.Request.abort(); } - params.Request = request; - params.Request.send(encodeURIComponent(params._QueryArg()) + "=" + encodeURIComponent(params._Pre())); - } - }; - if (!isMobile && document.querySelector('.index_endpoint')) { - qinput.focus(); + let newCurrentIndex = -1; + if (e.key === "ArrowUp") { + if (currentIndex >= 0) listItems[currentIndex].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; + } else if (e.key === "ArrowDown") { + if (currentIndex >= 0) listItems[currentIndex].classList.remove('active'); + newCurrentIndex = (currentIndex + 1) % listItems.length; + } else if (e.key === "Tab" || e.key === "Enter") { + autocomplete.classList.remove('open'); + } + + if (newCurrentIndex != -1) { + const selectedItem = listItems[newCurrentIndex]; + selectedItem.classList.add('active'); + + if (!selectedItem.classList.contains('no-item-found')) qinput.value = selectedItem.innerText; + } + }); } } diff --git a/client/simple/src/less/autocomplete.less b/client/simple/src/less/autocomplete.less index 8285ff2c6..93efb875e 100644 --- a/client/simple/src/less/autocomplete.less +++ b/client/simple/src/less/autocomplete.less @@ -3,6 +3,7 @@ .autocomplete { position: absolute; width: @search-width; + max-width: calc(100% - 2 * @search-padding-horizontal); max-height: 0; overflow-y: hidden; .ltr-text-align-left(); @@ -65,8 +66,6 @@ @media screen and (max-width: @phone) { .autocomplete { - width: 100%; - > ul > li { padding: 1rem; } diff --git a/client/simple/src/less/definitions.less b/client/simple/src/less/definitions.less index 395a02cde..d2d14e35d 100644 --- a/client/simple/src/less/definitions.less +++ b/client/simple/src/less/definitions.less @@ -287,8 +287,9 @@ @results-image-row-height: 12rem; @results-image-row-height-phone: 10rem; @search-width: 44rem; -// heigh of #search, see detail.less +// height of #search, see detail.less @search-height: 13rem; +@search-padding-horizontal: 0.5rem; /// Device Size /// @desktop > @tablet diff --git a/client/simple/src/less/search.less b/client/simple/src/less/search.less index 460ea09fa..bc49ffadc 100644 --- a/client/simple/src/less/search.less +++ b/client/simple/src/less/search.less @@ -131,7 +131,7 @@ button.category_button { } #search_view { - padding: 0.5rem 0.3rem 0 0.5rem; + padding: 0.5rem @search-padding-horizontal 0 @search-padding-horizontal; grid-area: search; body.results_endpoint & { @@ -141,7 +141,8 @@ button.category_button { .search_box { border-radius: 0.8rem; - width: @search-width; + width: 100%; + max-width: @search-width; display: inline-flex; flex-direction: row; white-space: nowrap; @@ -291,8 +292,7 @@ html.no-js #clear_search.hide_if_nojs { } .search_box { - width: 98%; - display: flex; + width: 100%; } #q { diff --git a/searx/templates/simple/search.html b/searx/templates/simple/search.html index b26e27821..180c56160 100644 --- a/searx/templates/simple/search.html +++ b/searx/templates/simple/search.html @@ -9,6 +9,7 @@ +
{% set display_tooltip = true %} diff --git a/searx/templates/simple/simple_search.html b/searx/templates/simple/simple_search.html index f69ba6142..af533e6e0 100644 --- a/searx/templates/simple/simple_search.html +++ b/searx/templates/simple/simple_search.html @@ -5,6 +5,7 @@ +