-
-
Notifications
You must be signed in to change notification settings - Fork 1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(tools): add search preview functionality (#12470)
* 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ř <[email protected]>
- Loading branch information
Showing
4 changed files
with
201 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,160 @@ | ||
// Copyright © Michal Čihař <[email protected]> | ||
// | ||
// 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 = $('<div id="search-preview"></div>'); | ||
$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 = `<a href="${searchUrl}" target="_blank" rel="noopener noreferrer" id="results-num">${t}</a>`; | ||
$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 = ` | ||
<a href="${url}" target="_blank" id="search-result" rel="noopener noreferrer"> | ||
<small>${key}</small> | ||
<div> | ||
${source.toString()} | ||
</div> | ||
</a> | ||
`; | ||
|
||
$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; | ||
} | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters