diff --git a/frontend/js/search.js b/frontend/js/search.js index 57e78b9..27b1036 100644 --- a/frontend/js/search.js +++ b/frontend/js/search.js @@ -7,7 +7,7 @@ import { safeCreateIcons } from './utils.js'; export const SearchHistory = { _load() { try { - const raw = localStorage.getItem(SEARCH_HISTORY_KEY); + const raw = localStorage.getItem(state.SEARCH_HISTORY_KEY); return raw ? JSON.parse(raw) : []; } catch { return []; @@ -15,7 +15,7 @@ export const SearchHistory = { }, _save(entries) { try { - localStorage.setItem(SEARCH_HISTORY_KEY, JSON.stringify(entries)); + localStorage.setItem(state.SEARCH_HISTORY_KEY, JSON.stringify(entries)); } catch {} }, getAll() { @@ -26,7 +26,7 @@ export const SearchHistory = { const q = query.trim(); let entries = this._load().filter((e) => e !== q); entries.unshift(q); - if (entries.length > MAX_HISTORY_ENTRIES) entries = entries.slice(0, MAX_HISTORY_ENTRIES); + if (entries.length > state.MAX_HISTORY_ENTRIES) entries = entries.slice(0, state.MAX_HISTORY_ENTRIES); this._save(entries); }, remove(query) { @@ -182,7 +182,7 @@ export const AutocompleteDropdown = { async populate(inputValue, cursorPos) { if (this._suppressNext) { this._suppressNext = false; return; } // Cancel previous suggestion request - if (suggestAbortController) { + if (state.suggestAbortController) { state.suggestAbortController.abort(); state.suggestAbortController = null; } @@ -373,23 +373,23 @@ export const AutocompleteDropdown = { navigateDown() { if (!this.isVisible() || state.dropdownItems.length === 0) return; - if (dropdownActiveIndex >= 0) dropdownItems[dropdownActiveIndex].classList.remove("active"); - state.dropdownActiveIndex = (dropdownActiveIndex + 1) % state.dropdownItems.length; - dropdownItems[dropdownActiveIndex].classList.add("active"); - dropdownItems[dropdownActiveIndex].scrollIntoView({ block: "nearest" }); + if (state.dropdownActiveIndex >= 0) state.dropdownItems[state.dropdownActiveIndex].classList.remove("active"); + state.dropdownActiveIndex = (state.dropdownActiveIndex + 1) % state.dropdownItems.length; + state.dropdownItems[state.dropdownActiveIndex].classList.add("active"); + state.dropdownItems[state.dropdownActiveIndex].scrollIntoView({ block: "nearest" }); }, navigateUp() { if (!this.isVisible() || state.dropdownItems.length === 0) return; - if (dropdownActiveIndex >= 0) dropdownItems[dropdownActiveIndex].classList.remove("active"); - state.dropdownActiveIndex = dropdownActiveIndex <= 0 ? state.dropdownItems.length - 1 : dropdownActiveIndex - 1; - dropdownItems[dropdownActiveIndex].classList.add("active"); - dropdownItems[dropdownActiveIndex].scrollIntoView({ block: "nearest" }); + if (state.dropdownActiveIndex >= 0) state.dropdownItems[state.dropdownActiveIndex].classList.remove("active"); + state.dropdownActiveIndex = state.dropdownActiveIndex <= 0 ? state.dropdownItems.length - 1 : state.dropdownActiveIndex - 1; + state.dropdownItems[state.dropdownActiveIndex].classList.add("active"); + state.dropdownItems[state.dropdownActiveIndex].scrollIntoView({ block: "nearest" }); }, selectActive() { - if (dropdownActiveIndex >= 0 && dropdownActiveIndex < state.dropdownItems.length) { - dropdownItems[dropdownActiveIndex].click(); + if (state.dropdownActiveIndex >= 0 && state.dropdownActiveIndex < state.dropdownItems.length) { + state.dropdownItems[state.dropdownActiveIndex].click(); return true; } return false; @@ -496,17 +496,17 @@ export function initSearch() { 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); + caseBtn.classList.toggle("active", state.searchCaseSensitive); + wordBtn.classList.toggle("active", state.searchWholeWord); + regexBtn.classList.toggle("active", state.searchRegex); + filterBtn.classList.toggle("active", state.searchFilterVisible); } // Toggle buttons caseBtn.addEventListener("click", () => { state.searchCaseSensitive = !state.searchCaseSensitive; _updateToggleUI(); _research(); }); if (wordBtn) wordBtn.addEventListener("click", () => { state.searchWholeWord = !state.searchWholeWord; _updateToggleUI(); _research(); }); if (regexBtn) regexBtn.addEventListener("click", () => { state.searchRegex = !state.searchRegex; _updateToggleUI(); _research(); }); - if (filterBtn) filterBtn.addEventListener("click", () => { state.searchFilterVisible = !state.searchFilterVisible; if (filterRow) filterRow.style.display = searchFilterVisible ? "flex" : "none"; _updateToggleUI(); }); + if (filterBtn) filterBtn.addEventListener("click", () => { state.searchFilterVisible = !state.searchFilterVisible; if (filterRow) filterRow.style.display = state.searchFilterVisible ? "flex" : "none"; _updateToggleUI(); }); // ── Result navigation (up/down arrows + Enter) ── let _searchResultIdx = -1; @@ -542,7 +542,7 @@ export function initSearch() { function _research() { const q = input.value.trim(); if (q.length >= _getEffective("min_query_length", 2)) { - clearTimeout(searchTimeout); + clearTimeout(state.searchTimeout); state.searchTimeout = setTimeout(() => { const vault = document.getElementById("vault-filter").value; const tagFilter = state.selectedTags.length > 0 ? state.selectedTags.join(",") : null; @@ -578,14 +578,14 @@ export function initSearch() { AutocompleteDropdown.populate(input.value, input.selectionStart); // Debounced search execution - clearTimeout(searchTimeout); + clearTimeout(state.searchTimeout); state.searchTimeout = setTimeout( () => { const q = input.value.trim(); const vault = document.getElementById("vault-filter").value; const tagFilter = state.selectedTags.length > 0 ? state.selectedTags.join(",") : null; state.advancedSearchOffset = 0; - if (q.length >= _getEffective("min_query_length", MIN_SEARCH_LENGTH) || tagFilter) { + if (q.length >= _getEffective("min_query_length", state.MIN_SEARCH_LENGTH) || tagFilter) { performAdvancedSearch(q, vault, tagFilter); } else if (q.length === 0) { SearchChips.clear(); @@ -636,7 +636,7 @@ export function initSearch() { const q = input.value.trim(); if (q) { SearchHistory.add(q); - clearTimeout(searchTimeout); + clearTimeout(state.searchTimeout); state.advancedSearchOffset = 0; const vault = document.getElementById("vault-filter").value; const tagFilter = state.selectedTags.length > 0 ? state.selectedTags.join(",") : null; @@ -660,7 +660,7 @@ export function initSearch() { const q = input.value.trim(); if (q) { SearchHistory.add(q); - clearTimeout(searchTimeout); + clearTimeout(state.searchTimeout); state.advancedSearchOffset = 0; const vault = document.getElementById("vault-filter").value; const tagFilter = state.selectedTags.length > 0 ? state.selectedTags.join(",") : null; @@ -712,7 +712,7 @@ function _isInputFocused() { // --- Backward-compatible search (existing /api/search endpoint) --- export async function performSearch(query, vaultFilter, tagFilter) { - if (searchAbortController) state.searchAbortController.abort(); + if (state.searchAbortController) state.searchAbortController.abort(); state.searchAbortController = new AbortController(); const searchId = ++state.currentSearchId; showLoading(); @@ -720,21 +720,21 @@ export async function performSearch(query, vaultFilter, tagFilter) { if (tagFilter) url += `&tag=${encodeURIComponent(tagFilter)}`; try { const data = await api(url, { signal: state.searchAbortController.signal }); - if (searchId !== currentSearchId) return; + if (searchId !== state.currentSearchId) return; renderSearchResults(data, query, tagFilter); } catch (err) { if (err.name === "AbortError") return; - if (searchId !== currentSearchId) return; + if (searchId !== state.currentSearchId) return; showWelcome(); } finally { hideProgressBar(); - if (searchId === currentSearchId) state.searchAbortController = null; + if (searchId === state.currentSearchId) state.searchAbortController = null; } } // --- Advanced search with TF-IDF, facets, pagination --- export async function performAdvancedSearch(query, vaultFilter, tagFilter, offset, sort) { - if (searchAbortController) state.searchAbortController.abort(); + if (state.searchAbortController) state.searchAbortController.abort(); state.searchAbortController = new AbortController(); const searchId = ++state.currentSearchId; showLoading(); @@ -747,12 +747,12 @@ export async function performAdvancedSearch(query, vaultFilter, tagFilter, offse const parsed = QueryParser.parse(query); SearchChips.update(parsed); - const effectiveLimit = _getEffective("results_per_page", ADVANCED_SEARCH_LIMIT); + const effectiveLimit = _getEffective("results_per_page", state.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 += "®ex=true"; + if (state.searchCaseSensitive) url += "&case_sensitive=true"; + if (state.searchWholeWord) url += "&whole_word=true"; + if (state.searchRegex) url += "®ex=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())}`; @@ -761,26 +761,26 @@ export async function performAdvancedSearch(query, vaultFilter, tagFilter, offse // Search timeout — abort if server takes too long const timeoutId = setTimeout( () => { - if (searchAbortController) state.searchAbortController.abort(); + if (state.searchAbortController) state.searchAbortController.abort(); }, - _getEffective("search_timeout_ms", SEARCH_TIMEOUT_MS), + _getEffective("search_timeout_ms", state.SEARCH_TIMEOUT_MS), ); try { const data = await api(url, { signal: state.searchAbortController.signal }); clearTimeout(timeoutId); - if (searchId !== currentSearchId) return; + if (searchId !== state.currentSearchId) return; state.advancedSearchTotal = data.total; state.advancedSearchOffset = ofs; renderAdvancedSearchResults(data, query, tagFilter); } catch (err) { clearTimeout(timeoutId); if (err.name === "AbortError") return; - if (searchId !== currentSearchId) return; + if (searchId !== state.currentSearchId) return; showWelcome(); } finally { hideProgressBar(); - if (searchId === currentSearchId) state.searchAbortController = null; + if (searchId === state.currentSearchId) state.searchAbortController = null; } } @@ -803,7 +803,7 @@ export function renderSearchResults(data, query, tagFilter) { const titleDiv = el("div", { class: "search-result-title" }); if (query && query.trim()) { - highlightSearchText(titleDiv, r.title, query, searchCaseSensitive); + highlightSearchText(titleDiv, r.title, query, state.searchCaseSensitive); } else { titleDiv.textContent = r.title; } @@ -811,7 +811,7 @@ export function renderSearchResults(data, query, tagFilter) { if (r.snippet && r.snippet.includes("")) { snippetDiv.innerHTML = r.snippet; } else if (query && query.trim() && r.snippet) { - highlightSearchText(snippetDiv, r.snippet, query, searchCaseSensitive); + highlightSearchText(snippetDiv, r.snippet, query, state.searchCaseSensitive); } else { snippetDiv.textContent = r.snippet || ""; } @@ -875,9 +875,9 @@ export function renderAdvancedSearchResults(data, query, tagFilter) { // Active filter badges const filtersRow = el("div", { class: "search-filters-row" }); - if (searchCaseSensitive) filtersRow.appendChild(el("span", { class: "search-filter-badge" }, [document.createTextNode("Aa")])); - if (searchWholeWord) filtersRow.appendChild(el("span", { class: "search-filter-badge" }, [document.createTextNode("wd")])); - if (searchRegex) filtersRow.appendChild(el("span", { class: "search-filter-badge" }, [document.createTextNode(".*")])); + if (state.searchCaseSensitive) filtersRow.appendChild(el("span", { class: "search-filter-badge" }, [document.createTextNode("Aa")])); + if (state.searchWholeWord) filtersRow.appendChild(el("span", { class: "search-filter-badge" }, [document.createTextNode("wd")])); + if (state.searchRegex) filtersRow.appendChild(el("span", { class: "search-filter-badge" }, [document.createTextNode(".*")])); const inclEl = document.getElementById("search-include-input"); const exclEl = document.getElementById("search-exclude-input"); if (inclEl?.value.trim()) filtersRow.appendChild(el("span", { class: "search-filter-badge path" }, [document.createTextNode("incl: " + inclEl.value.trim())])); @@ -919,9 +919,9 @@ export function renderAdvancedSearchResults(data, query, tagFilter) { body: JSON.stringify({ query: query, vault: document.getElementById("vault-filter")?.value || "all", - case_sensitive: searchCaseSensitive, - whole_word: searchWholeWord, - regex: searchRegex, + case_sensitive: state.searchCaseSensitive, + whole_word: state.searchWholeWord, + regex: state.searchRegex, include_paths: inclEl?.value || "", exclude_paths: exclEl?.value || "", }), @@ -1018,7 +1018,7 @@ export function renderAdvancedSearchResults(data, query, tagFilter) { const titleDiv = el("div", { class: "search-result-title" }); if (freeText) { - highlightSearchText(titleDiv, r.title, freeText, searchCaseSensitive); + highlightSearchText(titleDiv, r.title, freeText, state.searchCaseSensitive); } else { titleDiv.textContent = r.title; } @@ -1028,7 +1028,7 @@ export function renderAdvancedSearchResults(data, query, tagFilter) { if (r.snippet && r.snippet.includes("")) { snippetDiv.innerHTML = r.snippet; } else if (freeText && r.snippet) { - highlightSearchText(snippetDiv, r.snippet, freeText, searchCaseSensitive); + highlightSearchText(snippetDiv, r.snippet, freeText, state.searchCaseSensitive); } else { snippetDiv.textContent = r.snippet || ""; } @@ -1066,30 +1066,30 @@ export function renderAdvancedSearchResults(data, query, tagFilter) { area.appendChild(container); // Pagination - if (data.total > ADVANCED_SEARCH_LIMIT) { + if (data.total > state.ADVANCED_SEARCH_LIMIT) { const paginationDiv = el("div", { class: "search-pagination" }); const prevBtn = el("button", { class: "search-pagination__btn", type: "button" }); prevBtn.textContent = "← Précédent"; prevBtn.disabled = state.advancedSearchOffset === 0; prevBtn.addEventListener("click", () => { - state.advancedSearchOffset = Math.max(0, advancedSearchOffset - ADVANCED_SEARCH_LIMIT); + state.advancedSearchOffset = Math.max(0, state.advancedSearchOffset - state.ADVANCED_SEARCH_LIMIT); const vault = document.getElementById("vault-filter").value; - performAdvancedSearch(query, vault, tagFilter, advancedSearchOffset); + performAdvancedSearch(query, vault, tagFilter, state.advancedSearchOffset); document.getElementById("content-area").scrollTop = 0; }); const info = el("span", { class: "search-pagination__info" }); - const from = advancedSearchOffset + 1; - const to = Math.min(advancedSearchOffset + ADVANCED_SEARCH_LIMIT, data.total); + const from = state.advancedSearchOffset + 1; + const to = Math.min(state.advancedSearchOffset + state.ADVANCED_SEARCH_LIMIT, data.total); info.textContent = `${from}–${to} sur ${data.total}`; const nextBtn = el("button", { class: "search-pagination__btn", type: "button" }); nextBtn.textContent = "Suivant →"; - nextBtn.disabled = advancedSearchOffset + ADVANCED_SEARCH_LIMIT >= data.total; + nextBtn.disabled = state.advancedSearchOffset + state.ADVANCED_SEARCH_LIMIT >= data.total; nextBtn.addEventListener("click", () => { - advancedSearchOffset += state.ADVANCED_SEARCH_LIMIT; + state.advancedSearchOffset += state.ADVANCED_SEARCH_LIMIT; const vault = document.getElementById("vault-filter").value; - performAdvancedSearch(query, vault, tagFilter, advancedSearchOffset); + performAdvancedSearch(query, vault, tagFilter, state.advancedSearchOffset); document.getElementById("content-area").scrollTop = 0; });