Add search toggles, path filters, and find/replace functionality

This commit is contained in:
2026-05-26 13:35:38 -04:00
parent 6c282ac77f
commit 8fdcdaf412
5 changed files with 361 additions and 16 deletions
+98
View File
@@ -4673,10 +4673,97 @@
// ---------------------------------------------------------------------------
// Search (enhanced with autocomplete, keyboard nav, global shortcuts)
// ---------------------------------------------------------------------------
// ── Search toggle state ──
let searchCaseSensitive = false;
let searchWholeWord = false;
let searchRegex = false;
let searchFilterVisible = false;
let searchReplaceVisible = false;
function initSearch() {
const input = document.getElementById("search-input");
const caseBtn = document.getElementById("search-case-btn");
const wordBtn = document.getElementById("search-word-btn");
const regexBtn = document.getElementById("search-regex-btn");
const filterBtn = document.getElementById("search-filter-btn");
const replaceBtn = document.getElementById("search-replace-btn");
const clearBtn = document.getElementById("search-clear-btn");
const replaceInput = document.getElementById("search-replace-input");
const replaceOneBtn = document.getElementById("search-replace-one-btn");
const replaceAllBtn = document.getElementById("search-replace-all-btn");
const filterRow = document.getElementById("search-filter-row");
const replaceRow = document.getElementById("search-replace-row");
const countEl = document.getElementById("search-match-count");
function _updateToggleUI() {
caseBtn.classList.toggle("active", searchCaseSensitive);
wordBtn.classList.toggle("active", searchWholeWord);
regexBtn.classList.toggle("active", searchRegex);
filterBtn.classList.toggle("active", searchFilterVisible);
replaceBtn.classList.toggle("active", searchReplaceVisible);
}
// Toggle buttons
caseBtn.addEventListener("click", () => { searchCaseSensitive = !searchCaseSensitive; _updateToggleUI(); _research(); });
if (wordBtn) wordBtn.addEventListener("click", () => { searchWholeWord = !searchWholeWord; _updateToggleUI(); _research(); });
if (regexBtn) regexBtn.addEventListener("click", () => { searchRegex = !searchRegex; _updateToggleUI(); _research(); });
if (filterBtn) filterBtn.addEventListener("click", () => { searchFilterVisible = !searchFilterVisible; if (filterRow) filterRow.style.display = searchFilterVisible ? "flex" : "none"; searchReplaceVisible = false; if (replaceRow) replaceRow.style.display = "none"; _updateToggleUI(); });
if (replaceBtn) replaceBtn.addEventListener("click", () => { searchReplaceVisible = !searchReplaceVisible; if (replaceRow) replaceRow.style.display = searchReplaceVisible ? "flex" : "none"; searchFilterVisible = false; if (filterRow) filterRow.style.display = "none"; _updateToggleUI(); });
// Replace buttons
if (replaceOneBtn) replaceOneBtn.addEventListener("click", () => _doReplace(false));
if (replaceAllBtn) replaceAllBtn.addEventListener("click", () => _doReplace(true));
async function _doReplace(replaceAll) {
const q = input.value.trim();
const replacement = replaceInput ? replaceInput.value : "";
if (!q) return;
try {
const vault = document.getElementById("vault-filter").value;
const includeEl = document.getElementById("search-include-input");
const excludeEl = document.getElementById("search-exclude-input");
const data = await api("/api/search/replace", {
method: "POST",
body: JSON.stringify({
query: q, replacement, vault,
case_sensitive: searchCaseSensitive, whole_word: searchWholeWord, regex: searchRegex,
include_paths: includeEl?.value || null, exclude_paths: excludeEl?.value || null,
replace_all: replaceAll,
}),
});
if (data.dry_run) {
const total = data.total_matches || 0;
showToast(`${total} occurrence(s) trouvée(s) dans ${data.matches?.length || 0} fichier(s). Cliquez "Tout remplacer" pour appliquer.`, "info");
} else {
showToast(`${data.total_replacements || 0} remplacement(s) effectué(s).`, "success");
_research();
}
} catch (err) { showToast("Erreur: " + err.message, "error"); }
}
function _research() {
const q = input.value.trim();
if (q.length >= _getEffective("min_query_length", 2)) {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
const vault = document.getElementById("vault-filter").value;
const tagFilter = selectedTags.length > 0 ? selectedTags.join(",") : null;
advancedSearchOffset = 0;
performAdvancedSearch(q, vault, tagFilter);
}, _getEffective("debounce_ms", 300));
}
}
// Keyboard shortcuts
document.addEventListener("keydown", (e) => {
if (e.altKey && !e.ctrlKey && !e.metaKey) {
if (e.key === "c" || e.key === "C") { e.preventDefault(); caseBtn.click(); }
else if (e.key === "w" || e.key === "W") { e.preventDefault(); if (wordBtn) wordBtn.click(); }
else if (e.key === "r" || e.key === "R") { e.preventDefault(); if (regexBtn) regexBtn.click(); }
else if (e.key === "f" || e.key === "F") { e.preventDefault(); if (filterBtn) filterBtn.click(); input.focus(); }
else if (e.key === "H" || e.key === "h") { e.preventDefault(); if (replaceBtn) replaceBtn.click(); }
}
});
// Initialize sub-controllers
AutocompleteDropdown.init();
@@ -4849,6 +4936,13 @@
const effectiveLimit = _getEffective("results_per_page", ADVANCED_SEARCH_LIMIT);
let url = `/api/search/advanced?q=${encodeURIComponent(query)}&vault=${encodeURIComponent(vaultFilter)}&limit=${effectiveLimit}&offset=${ofs}&sort=${sortBy}`;
if (tagFilter) url += `&tag=${encodeURIComponent(tagFilter)}`;
if (searchCaseSensitive) url += "&case_sensitive=true";
if (searchWholeWord) url += "&whole_word=true";
if (searchRegex) url += "&regex=true";
const includeEl = document.getElementById("search-include-input");
const excludeEl = document.getElementById("search-exclude-input");
if (includeEl?.value.trim()) url += `&include_paths=${encodeURIComponent(includeEl.value.trim())}`;
if (excludeEl?.value.trim()) url += `&exclude_paths=${encodeURIComponent(excludeEl.value.trim())}`;
// Search timeout — abort if server takes too long
const timeoutId = setTimeout(
@@ -4932,6 +5026,10 @@
const area = document.getElementById("content-area");
area.innerHTML = "";
// Update match counter
const countEl = document.getElementById("search-match-count");
if (countEl) countEl.textContent = `${data.total > 0 ? "1" : "0"}/${data.total}`;
// Header with result count and sort controls
const header = el("div", { class: "search-results-header" });
const summaryText = el("span", { class: "search-results-summary-text" });