From 89dd31532165ae91a07ab12aed511807899723a1 Mon Sep 17 00:00:00 2001 From: Mehdi Eloualy <92476321+meel-hd@users.noreply.github.com> Date: Thu, 3 Oct 2024 17:22:40 +0100 Subject: [PATCH] feat(tools): add search preview functionality (#12470) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(tools): add search preview functionality This commit adds the functionality to display a search preview when typing in the search input field. The search preview shows a list of search results that match the user's input. It also highlights the matching text in the search results. The search preview is displayed below the search input field and disappears after a delay when the input field loses focus. The commit includes the following changes: - Added a new file `weblate/static/editor/tools/search.js` that contains the JavaScript code for the search preview functionality. - Added CSS styles in `weblate/static/styles/main.css` to position and style the search preview element. * refactor(tools): extract search result rendering logic into separate function - Improved RegEx while Making the search result url relative. because the regular expression may cause exponential backtracking on strings containing many repetitions of '.' - Extracted the logic for rendering search results into a separate function called "showResults". This improves code readability and maintainability by separating concerns and reducing the complexity of the main function. * Move search preview to filters instead of search string * Refactor search preview - Instead of displaying the total number of search results, it now shows the number of matching strings found. - Instead of getting project path from url get it from the Form data. - Update the `showResults` function to accept the `count` parameter, which represents the number of search results. - Modify the logic in the `showResults` function to use the `count` parameter instead of the length of the `results` array. - Adjust the CSS style for the `#results-num` element to increase the font size to 16px. * Remove console.log statement * docs: changelog entry * Add link to /search/?q= in search preview --------- Co-authored-by: Michal Čihař --- docs/changes.rst | 1 + weblate/static/editor/tools/search.js | 160 ++++++++++++++++++++++++++ weblate/static/styles/main.css | 39 +++++++ weblate/templates/base.html | 1 + 4 files changed, 201 insertions(+) create mode 100644 weblate/static/editor/tools/search.js diff --git a/docs/changes.rst b/docs/changes.rst index 3c8ba5c25dc1..68b057617aa9 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -21,6 +21,7 @@ Not yet released. * :ref:`api` now has a preview OpenAPI specification. * :kbd:`?` now displays available :ref:`keyboard`. * Translation and language view in the project now include basic information about the language and plurals. +* :ref:`bulk-edit` shows a preview of matched strings. **Bug fixes** diff --git a/weblate/static/editor/tools/search.js b/weblate/static/editor/tools/search.js new file mode 100644 index 000000000000..00004720e302 --- /dev/null +++ b/weblate/static/editor/tools/search.js @@ -0,0 +1,160 @@ +// Copyright © Michal Čihař +// +// SPDX-License-Identifier: GPL-3.0-or-later + +$(document).ready(() => { + searchPreview("#replace", "#id_replace_q"); + + /** + * Add preview to the search input search results. + * + * @param {string} searchForm The selector string of the parent element of the search input + * @param {string} searchElment The selector string of the search input or textarea element + * + */ + function searchPreview(searchForm, searchElment) { + const $searchForm = $(searchForm); + const $searchElment = $searchForm.find(searchElment); + + // Create the preview element + const $searchPreview = $('
'); + $searchElment.parent().parent().after($searchPreview); + + let debounceTimeout = null; + + // Update the preview while typing with a debounce of 300ms + $searchElment.on("input", () => { + $searchPreview.show(); + const userSearchInput = $searchElment.val(); + const searchQuery = buildSearchQuery($searchElment); + + // Clear the previous timeout to prevent the previous + // request since the user is still typing + clearTimeout(debounceTimeout); + + // fetch search results but not too often + debounceTimeout = setTimeout(() => { + if (userSearchInput) { + $.ajax({ + url: "/api/units/", + method: "GET", + data: { q: searchQuery }, + success: (response) => { + // Clear previous search results + $searchPreview.html(""); + $("#results-num").remove(); + const results = response.results; + if (!results || results.length === 0) { + $searchPreview.text(gettext("No results found")); + } else { + showResults(results, response.count, searchQuery); + } + }, + }); + } + }, 300); // If the user stops typing for 300ms, the search results will be fetched + }); + + // Show the preview on focus + $searchElment.on("focus", () => { + if ($searchElment.val() !== "" && $searchPreview.html() !== "") { + $searchPreview.show(); + $("#results-num").show(); + } + }); + + // Close the preview on focusout, form submit, form reset, and form clear + $searchElment.on("focusout", () => { + // Hide after a delay to allow click on a result + setTimeout(() => { + $searchPreview.hide(); + $("#results-num").hide(); + }, 700); + }); + $searchForm.on("submit", () => { + $searchPreview.html(""); + $searchPreview.hide(); + $("#results-num").remove(); + }); + $searchForm.on("reset", () => { + $searchPreview.html(""); + $searchPreview.hide(); + $("#results-num").remove(); + }); + $searchForm.on("clear", () => { + $searchPreview.html(""); + $("#results-num").remove(); + $searchPreview.hide(); + }); + + /** + * Handles the search results and displays them in the preview element. + * @param {any} results fetched search results + * @param {number} count The number of search results + * @param {string} searchQuery The user typed search + * @returns void + */ + function showResults(results, count, searchQuery) { + // Show the number of results + if (count > 0) { + const t = interpolate( + ngettext("%s matching string", "%s matching strings", count), + [count], + ); + const searchUrl = `/search/?q=${encodeURI(searchQuery)}`; + const resultsNumber = `${t}`; + $searchPreview.append(resultsNumber); + } else { + $("#results-num").remove(); + } + + for (const result of results) { + const key = result.context; + const source = result.source; + + // Make the URL relative + const url = result.web_url.replace(/^[a-zA-Z]+:\/\/[^/]+\//, "/"); + const resultHtml = ` + + ${key} +
+ ${source.toString()} +
+
+ `; + + $searchPreview.append(resultHtml); + } + } + } + + /** + * Builds a search query string from the user input filters. + * The search query is built by the user input filters. + * The path lookup is also added to the search query. + * Built in the following format: `path:proj/comp filters`. + * + * @param {jQuery} $searchElment - The user input. + * @returns {string} - The built search query string. + * + * */ + function buildSearchQuery($searchElment) { + let builtSearchQuery = ""; + + // Add path lookup to the search query + const projectPath = $searchElment + .closest("form") + .find("input[name=path]") + .val(); + if (projectPath) { + builtSearchQuery = `path:${projectPath}`; + } + + // Add filters to the search query + const filters = $searchElment.val(); + if (filters) { + builtSearchQuery = `${builtSearchQuery} ${filters}`; + } + return builtSearchQuery; + } +}); diff --git a/weblate/static/styles/main.css b/weblate/static/styles/main.css index ed06523aec2a..be4178354eb8 100644 --- a/weblate/static/styles/main.css +++ b/weblate/static/styles/main.css @@ -2127,3 +2127,42 @@ tbody.warning { #shortcuts-table-container kbd { color: unset; } + +#search-preview { + display: none; + width: 100%; + min-height: 40px; + max-height: 200px; + margin-top: -10px; + background: white; + padding: 5px; + border-radius: 4px; + border: 1px solid #ccc; + border-top: none; + overflow-y: auto; + box-shadow: 1px 2px 4px #00000020; +} + +#search-preview > #search-result { + display: block; + padding: 5px; + margin-bottom: 5px; + text-decoration: none; + color: inherit; + border-radius: 4px; +} + +#search-preview > #search-result:hover { + background-color: #cccccc50; +} + +#search-preview > #search-result > div { + margin-left: 10px; +} + +#results-num { + font-size: 16px; + font-weight: bold; + color: #1fa385; + margin: 10px; +} diff --git a/weblate/templates/base.html b/weblate/templates/base.html index 7607bcd798a6..d465117bc7ff 100644 --- a/weblate/templates/base.html +++ b/weblate/templates/base.html @@ -77,6 +77,7 @@ + {% endcompress %} {% block extra_script %}