Erreur : ' + err.message + "
"; } }, _renderUsers(users) { const container = document.getElementById("admin-users-list"); if (!users.length) { container.innerHTML = 'Aucun utilisateur.
'; return; } let html = '| Utilisateur | Rôle | Vaults | Statut | Dernière connexion | Actions | " + "
|---|---|---|---|---|---|
| " +
u.username +
"" +
(u.display_name && u.display_name !== u.username ? " " + u.display_name + "" : "") + " | " +
'' + u.role + " | " + '' + vaults + " | " + "" + status + " | " + "" + lastLogin + " | " + '' + '' + '' + " |
Impossible de charger le fichier.
Expire le ${new Date(existingShare.expires_at).toLocaleDateString("fr-FR")}
` : 'Sans expiration
'; div.innerHTML = ` `; div.querySelector(".share-copy-btn").addEventListener("click", async () => { try { await navigator.clipboard.writeText(url); } catch (e) { // Fallback for non-HTTPS contexts const ta = document.createElement("textarea"); ta.value = url; ta.style.position = "fixed"; ta.style.left = "-9999px"; document.body.appendChild(ta); ta.select(); document.execCommand("copy"); document.body.removeChild(ta); } showToast("Lien copié !", "success"); div.remove(); }); div.querySelector(".share-revoke-btn").addEventListener("click", async () => { try { await api(`/api/share/${existingShare.id}`, { method: "DELETE" }); showToast("Partage révoqué", "success"); existingShare = null; renderContent(); } catch (err) { showToast("Erreur: " + err.message, "error"); } }); } else { div.innerHTML = ` `; div.querySelector(".share-create-btn").addEventListener("click", async () => { try { const expiry = document.getElementById("share-expiry")?.value; const share = await api(`/api/share/${encodeURIComponent(vault)}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ path, expires_in_hours: expiry ? parseInt(expiry) : null }), }); existingShare = share; renderContent(); showToast("Lien créé !", "success"); } catch (err) { showToast("Erreur: " + err.message, "error"); } }); } div.querySelector(".share-close-btn").addEventListener("click", () => div.remove()); div.addEventListener("click", (e) => { if (e.target === div) div.remove(); }); }; renderContent(); document.body.appendChild(div); } function renderConfigFilters() { const config = TagFilterService.getConfig(); const filters = config.tagFilters || TagFilterService.defaultFilters; const container = document.getElementById("config-filters-list"); container.innerHTML = ""; filters.forEach((filter, index) => { const badge = el("div", { class: `config-filter-badge ${!filter.enabled ? "disabled" : ""}` }, [ el("span", {}, [document.createTextNode(filter.pattern)]), el( "button", { class: "config-filter-toggle", title: filter.enabled ? "Désactiver" : "Activer", type: "button", }, [document.createTextNode(filter.enabled ? "✓" : "○")], ), el( "button", { class: "config-filter-remove", title: "Supprimer", type: "button", }, [document.createTextNode("×")], ), ]); const toggleBtn = badge.querySelector(".config-filter-toggle"); const removeBtn = badge.querySelector(".config-filter-remove"); toggleBtn.addEventListener("click", (e) => { e.stopPropagation(); toggleConfigFilter(index); }); removeBtn.addEventListener("click", (e) => { e.stopPropagation(); removeConfigFilter(index); }); container.appendChild(badge); }); } function toggleConfigFilter(index) { const config = TagFilterService.getConfig(); const filters = config.tagFilters || TagFilterService.defaultFilters; if (filters[index]) { filters[index].enabled = !filters[index].enabled; config.tagFilters = filters; TagFilterService.saveConfig(config); renderConfigFilters(); refreshTagsForContext().catch((err) => console.error("Error refreshing tags:", err)); } } function removeConfigFilter(index) { const config = TagFilterService.getConfig(); let filters = config.tagFilters || TagFilterService.defaultFilters; filters = filters.filter((_, i) => i !== index); config.tagFilters = filters; TagFilterService.saveConfig(config); renderConfigFilters(); refreshTagsForContext().catch((err) => console.error("Error refreshing tags:", err)); } function addConfigFilter() { const input = document.getElementById("config-pattern-input"); const pattern = input.value.trim(); if (!pattern) return; const regex = TagFilterService.patternToRegex(pattern); const config = TagFilterService.getConfig(); const filters = config.tagFilters || TagFilterService.defaultFilters; const newFilter = { pattern, regex, enabled: true }; filters.push(newFilter); config.tagFilters = filters; TagFilterService.saveConfig(config); input.value = ""; renderConfigFilters(); refreshTagsForContext().catch((err) => console.error("Error refreshing tags:", err)); updateRegexPreview(); } function updateRegexPreview() { const input = document.getElementById("config-pattern-input"); const preview = document.getElementById("config-regex-preview"); const code = document.getElementById("config-regex-code"); const pattern = input.value.trim(); if (pattern) { const regex = TagFilterService.patternToRegex(pattern); code.textContent = `^${regex}$`; preview.style.display = "block"; } else { preview.style.display = "none"; } } // --------------------------------------------------------------------------- // Search (enhanced with autocomplete, keyboard nav, global shortcuts) // --------------------------------------------------------------------------- // ── Search toggle state ── function initSearch() { const input = document.getElementById("search-input"); if (!input) return; 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 clearBtn = document.getElementById("search-clear-btn"); const filterRow = document.getElementById("search-filter-row"); const prevBtn = document.getElementById("search-prev-btn"); const nextBtn = document.getElementById("search-next-btn"); 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); } // 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"; _updateToggleUI(); }); // ── Result navigation (up/down arrows + Enter) ── let _searchResultIdx = -1; let _searchResultItems = []; function _updateResultHighlight() { _searchResultItems.forEach((el, i) => { el.classList.toggle("search-result-active", i === _searchResultIdx); }); if (_searchResultIdx >= 0 && _searchResultIdx < _searchResultItems.length) { _searchResultItems[_searchResultIdx].scrollIntoView({ block: "nearest", behavior: "smooth" }); } const countEl = document.getElementById("search-match-count"); if (countEl) countEl.textContent = _searchResultIdx >= 0 ? `${_searchResultIdx + 1}/${_searchResultItems.length}` : `0/${_searchResultItems.length}`; } function _refreshResultItems() { _searchResultItems = Array.from(document.querySelectorAll(".search-result-item")); _searchResultIdx = _searchResultItems.length > 0 ? 0 : -1; _updateResultHighlight(); } window.navigateSearchResults = function(delta) { _searchResultItems = Array.from(document.querySelectorAll(".search-result-item")); if (_searchResultItems.length === 0) return; _searchResultIdx = Math.max(0, Math.min(_searchResultItems.length - 1, _searchResultIdx + delta)); _updateResultHighlight(); }; if (prevBtn) prevBtn.addEventListener("click", () => navigateSearchResults(-1)); if (nextBtn) nextBtn.addEventListener("click", () => navigateSearchResults(1)); 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(); } } }); // Initialize sub-controllers AutocompleteDropdown.init(); SearchChips.init(); // Initially hide clear button if (clearBtn) clearBtn.style.display = "none"; // --- Input handler: debounced search + autocomplete dropdown --- input.addEventListener("input", () => { const hasText = input.value.length > 0; clearBtn.style.display = hasText ? "flex" : "none"; // Show autocomplete dropdown while typing AutocompleteDropdown.populate(input.value, input.selectionStart); // Debounced search execution clearTimeout(searchTimeout); searchTimeout = setTimeout( () => { const q = input.value.trim(); const vault = document.getElementById("vault-filter").value; const tagFilter = selectedTags.length > 0 ? selectedTags.join(",") : null; advancedSearchOffset = 0; if (q.length >= _getEffective("min_query_length", MIN_SEARCH_LENGTH) || tagFilter) { performAdvancedSearch(q, vault, tagFilter); } else if (q.length === 0) { SearchChips.clear(); showWelcome(); } }, _getEffective("debounce_ms", 300), ); }); // --- Focus handler: show history dropdown --- input.addEventListener("focus", () => { if (input.value.length === 0) { const historyItems = SearchHistory.filter(""); if (historyItems.length > 0) { AutocompleteDropdown.populate("", 0); } } }); // --- Keyboard navigation in dropdown --- input.addEventListener("keydown", (e) => { if (AutocompleteDropdown.isVisible()) { if (e.key === "ArrowDown") { e.preventDefault(); AutocompleteDropdown.navigateDown(); } else if (e.key === "ArrowUp") { e.preventDefault(); AutocompleteDropdown.navigateUp(); } else if (e.key === "Enter") { // If search results are visible, open the selected result const results = document.querySelectorAll(".search-result-item"); if (results.length > 0 && _searchResultIdx >= 0) { const el = results[_searchResultIdx]; if (el) { const vault = el.dataset.vault; const path = el.dataset.path; if (vault && path) { TabManager.openPreview(vault, path); e.preventDefault(); return; } } } if (AutocompleteDropdown.selectActive()) { e.preventDefault(); return; } // No active item — execute search normally AutocompleteDropdown.hide(); const q = input.value.trim(); if (q) { SearchHistory.add(q); clearTimeout(searchTimeout); advancedSearchOffset = 0; const vault = document.getElementById("vault-filter").value; const tagFilter = selectedTags.length > 0 ? selectedTags.join(",") : null; performAdvancedSearch(q, vault, tagFilter); } e.preventDefault(); } else if (e.key === "ArrowDown" && !AutocompleteDropdown.isVisible()) { // Navigate search results when dropdown is closed if (window.navigateSearchResults) { window.navigateSearchResults(1); e.preventDefault(); } } else if (e.key === "ArrowUp" && !AutocompleteDropdown.isVisible()) { if (window.navigateSearchResults) { window.navigateSearchResults(-1); e.preventDefault(); } } else if (e.key === "Escape") { AutocompleteDropdown.hide(); e.stopPropagation(); } } else if (e.key === "Enter") { // If search results are visible, open selected result const results = document.querySelectorAll(".search-result-item"); if (results.length > 0 && _searchResultIdx >= 0) { const el = results[_searchResultIdx]; if (el) { const vault = el.dataset.vault; const path = el.dataset.path; if (vault && path) { TabManager.openPreview(vault, path); e.preventDefault(); return; } } } const q = input.value.trim(); if (q) { SearchHistory.add(q); clearTimeout(searchTimeout); advancedSearchOffset = 0; const vault = document.getElementById("vault-filter").value; const tagFilter = selectedTags.length > 0 ? selectedTags.join(",") : null; performAdvancedSearch(q, vault, tagFilter); } e.preventDefault(); } }); caseBtn.addEventListener("click", () => { searchCaseSensitive = !searchCaseSensitive; caseBtn.classList.toggle("active"); }); clearBtn.addEventListener("click", () => { input.value = ""; clearBtn.style.display = "none"; searchCaseSensitive = false; caseBtn.classList.remove("active"); SearchChips.clear(); AutocompleteDropdown.hide(); showWelcome(); }); // --- Global keyboard shortcuts --- document.addEventListener("keydown", (e) => { // Ctrl+K or Cmd+K: focus search if ((e.ctrlKey || e.metaKey) && e.key === "k") { e.preventDefault(); input.focus(); input.select(); } // "/" key: focus search (when not in an input/textarea) if (e.key === "/" && !_isInputFocused()) { e.preventDefault(); input.focus(); } // Escape: blur search input and close dropdown if (e.key === "Escape" && document.activeElement === input) { AutocompleteDropdown.hide(); input.blur(); } }); } /** Check if user is focused on an input/textarea/contenteditable */ function _isInputFocused() { const tag = document.activeElement?.tagName; if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true; return document.activeElement?.isContentEditable === true; } // --- Backward-compatible search (existing /api/search endpoint) --- async function performSearch(query, vaultFilter, tagFilter) { if (searchAbortController) searchAbortController.abort(); searchAbortController = new AbortController(); const searchId = ++currentSearchId; showLoading(); let url = `/api/search?q=${encodeURIComponent(query)}&vault=${encodeURIComponent(vaultFilter)}`; if (tagFilter) url += `&tag=${encodeURIComponent(tagFilter)}`; try { const data = await api(url, { signal: searchAbortController.signal }); if (searchId !== currentSearchId) return; renderSearchResults(data, query, tagFilter); } catch (err) { if (err.name === "AbortError") return; if (searchId !== currentSearchId) return; showWelcome(); } finally { hideProgressBar(); if (searchId === currentSearchId) searchAbortController = null; } } // --- Advanced search with TF-IDF, facets, pagination --- async function performAdvancedSearch(query, vaultFilter, tagFilter, offset, sort) { if (searchAbortController) searchAbortController.abort(); searchAbortController = new AbortController(); const searchId = ++currentSearchId; showLoading(); const ofs = offset !== undefined ? offset : advancedSearchOffset; const sortBy = sort || advancedSearchSort; advancedSearchLastQuery = query; // Update chips from parsed query const parsed = QueryParser.parse(query); SearchChips.update(parsed); 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 += "®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())}`; if (excludeEl?.value.trim()) url += `&exclude_paths=${encodeURIComponent(excludeEl.value.trim())}`; // Search timeout — abort if server takes too long const timeoutId = setTimeout( () => { if (searchAbortController) searchAbortController.abort(); }, _getEffective("search_timeout_ms", SEARCH_TIMEOUT_MS), ); try { const data = await api(url, { signal: searchAbortController.signal }); clearTimeout(timeoutId); if (searchId !== currentSearchId) return; advancedSearchTotal = data.total; advancedSearchOffset = ofs; renderAdvancedSearchResults(data, query, tagFilter); } catch (err) { clearTimeout(timeoutId); if (err.name === "AbortError") return; if (searchId !== currentSearchId) return; showWelcome(); } finally { hideProgressBar(); if (searchId === currentSearchId) searchAbortController = null; } } // --- Legacy search results renderer (kept for backward compat) --- function renderSearchResults(data, query, tagFilter) { const area = document.getElementById("content-area"); area.innerHTML = ""; const header = buildSearchResultsHeader(data, query, tagFilter); area.appendChild(header); if (data.results.length === 0) { area.appendChild(el("p", { style: "color:var(--text-muted);margin-top:20px" }, [document.createTextNode("Aucun résultat trouvé.")])); return; } const container = el("div", { class: "search-results" }); data.results.forEach((r) => { // Apply client-side filtering for hidden files if (!shouldDisplayPath(r.path, r.vault)) { return; // Skip this result } const titleDiv = el("div", { class: "search-result-title" }); if (query && query.trim()) { highlightSearchText(titleDiv, r.title, query, searchCaseSensitive); } else { titleDiv.textContent = r.title; } const snippetDiv = el("div", { class: "search-result-snippet" }); if (query && query.trim() && r.snippet) { highlightSearchText(snippetDiv, r.snippet, query, searchCaseSensitive); } else { snippetDiv.textContent = r.snippet || ""; } const item = el("div", { class: "search-result-item", "data-vault": r.vault, "data-path": r.path }, [titleDiv, el("div", { class: "search-result-vault" }, [document.createTextNode(r.vault + " / " + r.path)]), snippetDiv]); if (r.tags && r.tags.length > 0) { const tagsDiv = el("div", { class: "search-result-tags" }); r.tags.forEach((tag) => { if (!TagFilterService.isTagFiltered(tag)) { const tagEl = el("span", { class: "file-tag" }, [document.createTextNode(`#${tag}`)]); tagEl.addEventListener("click", (e) => { e.stopPropagation(); addTagFilter(tag); }); tagsDiv.appendChild(tagEl); } }); if (tagsDiv.children.length > 0) item.appendChild(tagsDiv); } item.addEventListener("click", () => TabManager.openPreview(r.vault, r.path)); item.addEventListener("dblclick", (e) => { e.preventDefault(); TabManager.openPersistent(r.vault, r.path); }); container.appendChild(item); }); area.appendChild(container); } // --- Advanced search results renderer (facets, highlighted snippets, pagination, sort) --- function renderAdvancedSearchResults(data, query, tagFilter) { 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" }); const parsed = QueryParser.parse(query); const freeText = parsed.freeText; if (freeText && tagFilter) { summaryText.textContent = `${data.total} résultat(s) pour "${freeText}" avec filtres`; } else if (freeText) { summaryText.textContent = `${data.total} résultat(s) pour "${freeText}"`; } else if (parsed.tags.length > 0 || tagFilter) { summaryText.textContent = `${data.total} fichier(s) avec filtres`; } else { summaryText.textContent = `${data.total} résultat(s)`; } if (data.query_time_ms !== undefined && data.query_time_ms > 0) { const timeBadge = el("span", { class: "search-time-badge" }); timeBadge.textContent = `(${data.query_time_ms} ms)`; summaryText.appendChild(timeBadge); } header.appendChild(summaryText); // Sort controls const sortDiv = el("div", { class: "search-sort" }); const btnRelevance = el("button", { class: "search-sort__btn" + (advancedSearchSort === "relevance" ? " active" : ""), type: "button" }); btnRelevance.textContent = "Pertinence"; btnRelevance.addEventListener("click", () => { advancedSearchSort = "relevance"; advancedSearchOffset = 0; const vault = document.getElementById("vault-filter").value; performAdvancedSearch(query, vault, tagFilter, 0, "relevance"); }); const btnDate = el("button", { class: "search-sort__btn" + (advancedSearchSort === "modified" ? " active" : ""), type: "button" }); btnDate.textContent = "Date"; btnDate.addEventListener("click", () => { advancedSearchSort = "modified"; advancedSearchOffset = 0; const vault = document.getElementById("vault-filter").value; performAdvancedSearch(query, vault, tagFilter, 0, "modified"); }); sortDiv.appendChild(btnRelevance); sortDiv.appendChild(btnDate); header.appendChild(sortDiv); area.appendChild(header); // Active sidebar tag chips if (selectedTags.length > 0) { const activeTags = el("div", { class: "search-results-active-tags" }); selectedTags.forEach((tag) => { const removeBtn = el( "button", { class: "search-results-active-tag-remove", title: `Retirer ${tag} du filtre`, }, [document.createTextNode("×")], ); removeBtn.addEventListener("click", (e) => { e.stopPropagation(); removeTagFilter(tag); }); const chip = el("span", { class: "search-results-active-tag" }, [document.createTextNode(`#${tag}`), removeBtn]); activeTags.appendChild(chip); }); area.appendChild(activeTags); } // Facets panel if (data.facets && (Object.keys(data.facets.tags || {}).length > 0 || Object.keys(data.facets.vaults || {}).length > 0)) { const facetsDiv = el("div", { class: "search-facets" }); // Vault facets const vaultFacets = data.facets.vaults || {}; if (Object.keys(vaultFacets).length > 1) { const group = el("div", { class: "search-facets__group" }); const label = el("span", { class: "search-facets__label" }); label.textContent = "Vaults"; group.appendChild(label); for (const [vaultName, count] of Object.entries(vaultFacets)) { const item = el("span", { class: "search-facets__item" }); item.innerHTML = `${vaultName} ${count}`; item.addEventListener("click", () => { const input = document.getElementById("search-input"); // Add vault: operator const current = input.value.replace(/vault:\S+\s*/gi, "").trim(); input.value = current + " vault:" + vaultName; _triggerAdvancedSearch(input.value); }); group.appendChild(item); } facetsDiv.appendChild(group); } // Tag facets const tagFacets = data.facets.tags || {}; if (Object.keys(tagFacets).length > 0) { const group = el("div", { class: "search-facets__group" }); const label = el("span", { class: "search-facets__label" }); label.textContent = "Tags"; group.appendChild(label); const entries = Object.entries(tagFacets).slice(0, 12); for (const [tagName, count] of entries) { const item = el("span", { class: "search-facets__item" }); item.innerHTML = `#${tagName} ${count}`; item.addEventListener("click", () => { addTagFilter(tagName); }); group.appendChild(item); } facetsDiv.appendChild(group); } area.appendChild(facetsDiv); } // Empty state if (data.results.length === 0) { area.appendChild(el("p", { style: "color:var(--text-muted);margin-top:20px" }, [document.createTextNode("Aucun résultat trouvé.")])); return; } // Results list const container = el("div", { class: "search-results" }); data.results.forEach((r) => { // Apply client-side filtering for hidden files if (!shouldDisplayPath(r.path, r.vault)) { return; // Skip this result } const titleDiv = el("div", { class: "search-result-title" }); if (freeText) { highlightSearchText(titleDiv, r.title, freeText, searchCaseSensitive); } else { titleDiv.textContent = r.title; } // Snippet — use HTML from backend (already has tags) const snippetDiv = el("div", { class: "search-result-snippet search-result__snippet" }); if (r.snippet && r.snippet.includes("")) { snippetDiv.innerHTML = r.snippet; } else if (freeText && r.snippet) { highlightSearchText(snippetDiv, r.snippet, freeText, searchCaseSensitive); } else { snippetDiv.textContent = r.snippet || ""; } // Score badge const scoreEl = el("span", { class: "search-result-score", style: "font-size:0.7rem;color:var(--text-muted);margin-left:8px" }); scoreEl.textContent = `score: ${r.score}`; const vaultPath = el("div", { class: "search-result-vault" }, [document.createTextNode(r.vault + " / " + r.path), scoreEl]); const item = el("div", { class: "search-result-item", "data-vault": r.vault, "data-path": r.path }, [titleDiv, vaultPath, snippetDiv]); if (r.tags && r.tags.length > 0) { const tagsDiv = el("div", { class: "search-result-tags" }); r.tags.forEach((tag) => { if (!TagFilterService.isTagFiltered(tag)) { const tagEl = el("span", { class: "file-tag" }, [document.createTextNode(`#${tag}`)]); tagEl.addEventListener("click", (e) => { e.stopPropagation(); addTagFilter(tag); }); tagsDiv.appendChild(tagEl); } }); if (tagsDiv.children.length > 0) item.appendChild(tagsDiv); } item.addEventListener("click", () => TabManager.openPreview(r.vault, r.path)); item.addEventListener("dblclick", (e) => { e.preventDefault(); TabManager.openPersistent(r.vault, r.path); }); container.appendChild(item); }); area.appendChild(container); // Pagination if (data.total > 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 = advancedSearchOffset === 0; prevBtn.addEventListener("click", () => { advancedSearchOffset = Math.max(0, advancedSearchOffset - ADVANCED_SEARCH_LIMIT); const vault = document.getElementById("vault-filter").value; performAdvancedSearch(query, vault, tagFilter, 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); 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.addEventListener("click", () => { advancedSearchOffset += ADVANCED_SEARCH_LIMIT; const vault = document.getElementById("vault-filter").value; performAdvancedSearch(query, vault, tagFilter, advancedSearchOffset); document.getElementById("content-area").scrollTop = 0; }); paginationDiv.appendChild(prevBtn); paginationDiv.appendChild(info); paginationDiv.appendChild(nextBtn); area.appendChild(paginationDiv); } safeCreateIcons(); // Initialize result navigation (select first result) setTimeout(() => { if (window.navigateSearchResults) window.navigateSearchResults(0); }, 50); } // --------------------------------------------------------------------------- // Resizable sidebar (horizontal) // --------------------------------------------------------------------------- function initSidebarResize() { const handle = document.getElementById("sidebar-resize-handle"); const sidebar = document.getElementById("sidebar"); if (!handle || !sidebar) return; // Restore saved width const savedWidth = localStorage.getItem("obsigate-sidebar-width"); if (savedWidth) { sidebar.style.width = savedWidth + "px"; } let startX = 0; let startWidth = 0; function onMouseMove(e) { const newWidth = Math.min(500, Math.max(200, startWidth + (e.clientX - startX))); sidebar.style.width = newWidth + "px"; } function onMouseUp() { document.body.classList.remove("resizing"); handle.classList.remove("active"); document.removeEventListener("mousemove", onMouseMove); document.removeEventListener("mouseup", onMouseUp); localStorage.setItem("obsigate-sidebar-width", parseInt(sidebar.style.width)); } handle.addEventListener("mousedown", (e) => { e.preventDefault(); startX = e.clientX; startWidth = sidebar.getBoundingClientRect().width; document.body.classList.add("resizing"); handle.classList.add("active"); document.addEventListener("mousemove", onMouseMove); document.addEventListener("mouseup", onMouseUp); }); } // --------------------------------------------------------------------------- // Resizable tag section (vertical) // --------------------------------------------------------------------------- function initTagResize() { const handle = document.getElementById("tag-resize-handle"); const tagSection = document.getElementById("tag-cloud-section"); if (!handle || !tagSection) return; // Restore saved height const savedHeight = localStorage.getItem("obsigate-tag-height"); if (savedHeight) { tagSection.style.height = savedHeight + "px"; } let startY = 0; let startHeight = 0; function onMouseMove(e) { // Dragging up increases height, dragging down decreases const newHeight = Math.min(400, Math.max(60, startHeight - (e.clientY - startY))); tagSection.style.height = newHeight + "px"; } function onMouseUp() { document.body.classList.remove("resizing-v"); handle.classList.remove("active"); document.removeEventListener("mousemove", onMouseMove); document.removeEventListener("mouseup", onMouseUp); localStorage.setItem("obsigate-tag-height", parseInt(tagSection.style.height)); } handle.addEventListener("mousedown", (e) => { e.preventDefault(); startY = e.clientY; startHeight = tagSection.getBoundingClientRect().height; document.body.classList.add("resizing-v"); handle.classList.add("active"); document.addEventListener("mousemove", onMouseMove); document.addEventListener("mouseup", onMouseUp); }); } // --------------------------------------------------------------------------- // Frontmatter Accent Card Builder // --------------------------------------------------------------------------- function buildFrontmatterCard(frontmatter) { // Helper: format date function formatDate(iso) { if (!iso) return "—"; const d = new Date(iso); const date = d.toISOString().slice(0, 10); const time = d.toTimeString().slice(0, 5); return `${date} · ${time}`; } // Extract boolean flags const booleanFlags = ["publish", "favoris", "template", "task", "archive", "draft", "private"].map((key) => ({ key, value: !!frontmatter[key] })); // Toggle state let isOpen = true; // Build header with chevron const chevron = el("span", { class: "fm-chevron open" }); chevron.innerHTML = ''; const fmHeader = el("div", { class: "fm-header" }, [chevron, document.createTextNode("Frontmatter")]); // ZONE 1: Top strip const topBadges = []; // Title badge const title = frontmatter.titre || frontmatter.title || ""; if (title) { topBadges.push(el("span", { class: "ac-title" }, [document.createTextNode(`"${title}"`)])); } // Status badge if (frontmatter.statut) { const statusBadge = el("span", { class: "ac-badge green" }, [el("span", { class: "ac-dot" }), document.createTextNode(frontmatter.statut)]); topBadges.push(statusBadge); } // Category badge if (frontmatter.catégorie || frontmatter.categorie) { const cat = frontmatter.catégorie || frontmatter.categorie; const catBadge = el("span", { class: "ac-badge blue" }, [document.createTextNode(cat)]); topBadges.push(catBadge); } // Publish badge if (frontmatter.publish) { topBadges.push(el("span", { class: "ac-badge purple" }, [document.createTextNode("publié")])); } // Favoris badge if (frontmatter.favoris) { topBadges.push(el("span", { class: "ac-badge purple" }, [document.createTextNode("favori")])); } const acTop = el("div", { class: "ac-top" }, topBadges); // ZONE 2: Body 2 columns const leftCol = el("div", { class: "ac-col" }, [ el("div", { class: "ac-row" }, [el("span", { class: "ac-k" }, [document.createTextNode("auteur")]), el("span", { class: "ac-v" }, [document.createTextNode(frontmatter.auteur || "—")])]), el("div", { class: "ac-row" }, [el("span", { class: "ac-k" }, [document.createTextNode("catégorie")]), el("span", { class: "ac-v" }, [document.createTextNode(frontmatter.catégorie || frontmatter.categorie || "—")])]), el("div", { class: "ac-row" }, [el("span", { class: "ac-k" }, [document.createTextNode("statut")]), el("span", { class: "ac-v" }, [document.createTextNode(frontmatter.statut || "—")])]), el("div", { class: "ac-row" }, [el("span", { class: "ac-k" }, [document.createTextNode("aliases")]), el("span", { class: "ac-v muted" }, [document.createTextNode(frontmatter.aliases && frontmatter.aliases.length > 0 ? frontmatter.aliases.join(", ") : "[]")])]), ]); const rightCol = el("div", { class: "ac-col" }, [ el("div", { class: "ac-row" }, [el("span", { class: "ac-k" }, [document.createTextNode("creation_date")]), el("span", { class: "ac-v mono" }, [document.createTextNode(formatDate(frontmatter.creation_date))])]), el("div", { class: "ac-row" }, [el("span", { class: "ac-k" }, [document.createTextNode("modification_date")]), el("span", { class: "ac-v mono" }, [document.createTextNode(formatDate(frontmatter.modification_date))])]), el("div", { class: "ac-row" }, [el("span", { class: "ac-k" }, [document.createTextNode("publish")]), el("span", { class: "ac-v" }, [document.createTextNode(String(frontmatter.publish || false))])]), el("div", { class: "ac-row" }, [el("span", { class: "ac-k" }, [document.createTextNode("favoris")]), el("span", { class: "ac-v" }, [document.createTextNode(String(frontmatter.favoris || false))])]), ]); const acBody = el("div", { class: "ac-body" }, [leftCol, rightCol]); // ZONE 3: Tags row const tagPills = []; if (frontmatter.tags && frontmatter.tags.length > 0) { frontmatter.tags.forEach((tag) => { tagPills.push(el("span", { class: "ac-tag" }, [document.createTextNode(tag)])); }); } const acTagsRow = el("div", { class: "ac-tags-row" }, [el("span", { class: "ac-tags-k" }, [document.createTextNode("tags")]), el("div", { class: "ac-tags-wrap" }, tagPills)]); // ZONE 4: Flags row const flagChips = []; booleanFlags.forEach((flag) => { const chipClass = flag.value ? "flag-chip on" : "flag-chip off"; flagChips.push(el("span", { class: chipClass }, [el("span", { class: "flag-dot" }), document.createTextNode(flag.key)])); }); const acFlagsRow = el("div", { class: "ac-flags-row" }, [el("span", { class: "ac-flags-k" }, [document.createTextNode("flags")]), ...flagChips]); // Assemble the card const acCard = el("div", { class: "ac-card" }, [acTop, acBody, acTagsRow, acFlagsRow]); // Toggle functionality fmHeader.addEventListener("click", () => { isOpen = !isOpen; if (isOpen) { acCard.style.display = "block"; chevron.classList.remove("closed"); chevron.classList.add("open"); } else { acCard.style.display = "none"; chevron.classList.remove("open"); chevron.classList.add("closed"); } safeCreateIcons(); }); // Wrap in section const fmSection = el("div", { class: "fm-section" }, [fmHeader, acCard]); return fmSection; } // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function escapeHtml(str) { if (!str) return ""; return String(str).replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); } function el(tag, attrs, children) { const e = document.createElement(tag); if (attrs) { Object.entries(attrs).forEach(([k, v]) => { // Skip boolean false for standard HTML boolean attributes to avoid setAttribute("checked", "false") bug if (v === false && (k === "checked" || k === "disabled" || k === "hidden" || k === "required" || k === "readonly")) { return; } e.setAttribute(k, v); }); } if (children) { children.forEach((c) => { if (c) e.appendChild(c); }); } return e; } function icon(name, size) { const i = document.createElement("i"); i.setAttribute("data-lucide", name); i.style.width = size + "px"; i.style.height = size + "px"; i.classList.add("icon"); return i; } function smallBadge(count) { const s = document.createElement("span"); s.className = "badge-small"; s.style.cssText = "font-size:0.68rem;color:var(--text-muted);margin-left:4px"; s.textContent = `(${count})`; return s; } function getContextMenuPositionFromElement(target) { const rect = target.getBoundingClientRect(); return { x: Math.min(rect.right - 8, window.innerWidth - 16), y: Math.min(rect.top + rect.height / 2, window.innerHeight - 16), }; } function attachTreeItemActionButton(itemEl, vault, path, type, isReadonly) { const button = document.createElement("button"); button.type = "button"; button.className = "tree-item-action-btn"; button.setAttribute("aria-label", "Afficher le menu d’actions"); button.setAttribute("title", "Actions"); const iconEl = icon("more-vertical", 16); button.appendChild(iconEl); button.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); const pos = getContextMenuPositionFromElement(button); ContextMenuManager.show(pos.x, pos.y, vault, path, type, isReadonly); }); itemEl.appendChild(button); // Ensure Lucide icons are rendered for the button setTimeout(() => { safeCreateIcons(); }, 0); } function attachTreeItemLongPress(itemEl, getMenuData) { let pressTimer = null; let pressHandled = false; let startX = 0; let startY = 0; const longPressDelay = 550; const moveThreshold = 10; const clearPressTimer = () => { if (pressTimer) { clearTimeout(pressTimer); pressTimer = null; } }; itemEl.addEventListener("touchstart", (e) => { if (!e.touches || e.touches.length !== 1) return; pressHandled = false; startX = e.touches[0].clientX; startY = e.touches[0].clientY; clearPressTimer(); pressTimer = setTimeout(() => { const data = getMenuData(); if (!data) return; pressHandled = true; ContextMenuManager.show(startX, startY, data.vault, data.path, data.type, data.isReadonly); }, longPressDelay); }, { passive: true }); itemEl.addEventListener("touchmove", (e) => { if (!e.touches || e.touches.length !== 1) return; const dx = Math.abs(e.touches[0].clientX - startX); const dy = Math.abs(e.touches[0].clientY - startY); if (dx > moveThreshold || dy > moveThreshold) { clearPressTimer(); } }, { passive: true }); itemEl.addEventListener("touchend", () => { clearPressTimer(); }, { passive: true }); itemEl.addEventListener("touchcancel", () => { clearPressTimer(); }, { passive: true }); itemEl.addEventListener("click", (e) => { if (pressHandled) { e.preventDefault(); e.stopPropagation(); setTimeout(() => { pressHandled = false; }, 0); } }, true); } function getVaultIcon(vaultName, size = 16) { const v = allVaults.find((val) => val.name === vaultName); const type = v ? v.type : "VAULT"; if (type === "DIR") { const i = icon("folder", size); i.style.color = "#eab308"; // yellow tint return i; } else { const purple = "#8b5cf6"; const svgNS = "http://www.w3.org/2000/svg"; const svg = document.createElementNS(svgNS, "svg"); svg.setAttribute("xmlns", svgNS); svg.setAttribute("width", size); svg.setAttribute("height", size); svg.setAttribute("viewBox", "0 0 24 24"); svg.setAttribute("fill", "none"); svg.setAttribute("stroke", purple); svg.setAttribute("stroke-width", "2"); svg.setAttribute("stroke-linecap", "round"); svg.setAttribute("stroke-linejoin", "round"); svg.classList.add("icon"); const path1 = document.createElementNS(svgNS, "path"); path1.setAttribute("d", "M6 3h12l4 6-10 12L2 9z"); const path2 = document.createElementNS(svgNS, "path"); path2.setAttribute("d", "M11 3 8 9l4 12"); const path3 = document.createElementNS(svgNS, "path"); path3.setAttribute("d", "M12 21l4-12-3-6"); const path4 = document.createElementNS(svgNS, "path"); path4.setAttribute("d", "M2 9h20"); svg.appendChild(path1); svg.appendChild(path2); svg.appendChild(path3); svg.appendChild(path4); return svg; } } function makeBreadcrumbSpan(text, onClick) { const s = document.createElement("span"); s.textContent = text; if (onClick) { s.addEventListener("click", async (event) => { event.preventDefault(); if (s.dataset.busy === "true") return; s.dataset.busy = "true"; s.style.pointerEvents = "none"; try { await onClick(event); } finally { s.dataset.busy = "false"; s.style.pointerEvents = ""; } }); } return s; } function appendHighlightedText(container, text, query, caseSensitive) { container.textContent = ""; if (!query) { container.appendChild(document.createTextNode(text)); return; } const source = caseSensitive ? text : text.toLowerCase(); const needle = caseSensitive ? query : query.toLowerCase(); let start = 0; let index = source.indexOf(needle, start); if (index === -1) { container.appendChild(document.createTextNode(text)); return; } while (index !== -1) { if (index > start) { container.appendChild(document.createTextNode(text.slice(start, index))); } const mark = el("mark", { class: "filter-highlight" }, [document.createTextNode(text.slice(index, index + query.length))]); container.appendChild(mark); start = index + query.length; index = source.indexOf(needle, start); } if (start < text.length) { container.appendChild(document.createTextNode(text.slice(start))); } } function highlightSearchText(container, text, query, caseSensitive) { container.textContent = ""; if (!query || !text) { container.appendChild(document.createTextNode(text || "")); return; } const source = caseSensitive ? text : text.toLowerCase(); const needle = caseSensitive ? query : query.toLowerCase(); let start = 0; let index = source.indexOf(needle, start); if (index === -1) { container.appendChild(document.createTextNode(text)); return; } while (index !== -1) { if (index > start) { container.appendChild(document.createTextNode(text.slice(start, index))); } const mark = el("mark", { class: "search-highlight" }, [document.createTextNode(text.slice(index, index + query.length))]); container.appendChild(mark); start = index + query.length; index = source.indexOf(needle, start); } if (start < text.length) { container.appendChild(document.createTextNode(text.slice(start))); } } function showWelcome() { hideProgressBar(); // Restore or rebuild the dashboard with tabbed sections const area = document.getElementById("content-area"); const home = document.getElementById("dashboard-home"); if (area && !home) { area.innerHTML = `Épinglez des fichiers pour les retrouver ici.