import { state } from './state.js'; import { safeCreateIcons, getFileIcon, flushIcons } from './utils.js'; import { api } from './auth.js'; import { populateCustomDropdown, TabManager, closeMobileSidebar, ContextMenuManager } from './ui.js'; import { _populateRecentVaultFilter, switchSidebarTab } from './config.js'; import { el, icon, getVaultIcon, smallBadge, attachTreeItemActionButton, attachTreeItemLongPress, showWelcome, appendHighlightedText } from './viewer.js'; import { performAdvancedSearch } from './search.js'; // --------------------------------------------------------------------------- // Vault context switching // --------------------------------------------------------------------------- function initVaultContext() { const filter = document.getElementById("vault-filter"); const quickSelect = document.getElementById("vault-quick-select"); if (!filter || !quickSelect) return; filter.addEventListener("change", async () => { await setSelectedVaultContext(filter.value, { focusVault: filter.value !== "all" }); }); quickSelect.addEventListener("change", async () => { await setSelectedVaultContext(quickSelect.value, { focusVault: quickSelect.value !== "all" }); }); } async function setSelectedVaultContext(vaultName, options) { state.selectedContextVault = vaultName; state.showingSource = false; state.cachedRawSource = null; syncVaultSelectors(); await refreshSidebarForContext(); await refreshTagsForContext(); // Synchroniser le dashboard et les fichiers récents if (typeof DashboardRecentWidget !== "undefined" && DashboardRecentWidget.load) { DashboardRecentWidget.load(vaultName); } if (typeof DashboardBookmarkWidget !== "undefined" && DashboardBookmarkWidget.load) { DashboardBookmarkWidget.load(vaultName); } if (state.activeSidebarTab === "recent") { loadRecentFiles(vaultName === "all" ? null : vaultName); } showWelcome(); if (options && options.focusVault && vaultName !== "all") { await focusVaultInSidebar(vaultName); } } function syncVaultSelectors() { const filter = document.getElementById("vault-filter"); const quickSelect = document.getElementById("vault-quick-select"); const recentFilter = document.getElementById("recent-vault-filter"); const dashboardFilter = document.getElementById("dashboard-vault-filter"); const contextText = document.getElementById("vault-context-text"); if (filter) filter.value = state.selectedContextVault; if (quickSelect) quickSelect.value = state.selectedContextVault; if (recentFilter) recentFilter.value = state.selectedContextVault === "all" ? "" : state.selectedContextVault; if (dashboardFilter) dashboardFilter.value = state.selectedContextVault; // Mise à jour visuelle des dropdowns personnalisés updateCustomDropdownVisual("vault-filter-dropdown", state.selectedContextVault); updateCustomDropdownVisual("vault-quick-select-dropdown", state.selectedContextVault); // Update vault context indicator if (contextText) { contextText.textContent = state.selectedContextVault === "all" ? "All" : state.selectedContextVault; } } /** * Updates the visual state of a custom dropdown based on its current value. */ function updateCustomDropdownVisual(dropdownId, value) { const dropdown = document.getElementById(dropdownId); if (!dropdown) return; const selectedText = dropdown.querySelector(".custom-dropdown-selected"); const options = dropdown.querySelectorAll(".custom-dropdown-option"); options.forEach((opt) => { const optValue = opt.getAttribute("data-value"); if (optValue === value) { opt.classList.add("selected"); if (selectedText) selectedText.textContent = opt.textContent; } else { opt.classList.remove("selected"); } }); } function scrollTreeItemIntoView(element, alignToTop) { if (!element) return; const scrollContainer = document.getElementById("sidebar-panel-vaults"); if (!scrollContainer) return; const containerRect = scrollContainer.getBoundingClientRect(); const elementRect = element.getBoundingClientRect(); const isAbove = elementRect.top < containerRect.top; const isBelow = elementRect.bottom > containerRect.bottom; if (!isAbove && !isBelow && !alignToTop) return; const currentTop = scrollContainer.scrollTop; const offsetTop = element.offsetTop; const shouldCenter = alignToTop === "center"; const centeredTop = Math.max(0, currentTop + (elementRect.top - containerRect.top) - (containerRect.height / 2) + (elementRect.height / 2)); const targetTop = shouldCenter ? centeredTop : alignToTop ? Math.max(0, offsetTop - 60) : Math.max(0, currentTop + (elementRect.top - containerRect.top) - containerRect.height * 0.35); scrollContainer.scrollTo({ top: targetTop, behavior: "smooth", }); } async function refreshSidebarForContext() { const container = document.getElementById("vault-tree"); container.innerHTML = ""; const vaultsToShow = state.selectedContextVault === "all" ? state.allVaults : state.allVaults.filter((v) => v.name === state.selectedContextVault); vaultsToShow.forEach((v) => { const vaultItem = el("div", { class: "tree-item vault-item", "data-vault": v.name }, [icon("chevron-right", 14), getVaultIcon(v.name, 16), el("span", { class: "tree-item-text" }, [document.createTextNode(v.name)]), smallBadge(v.file_count)]); vaultItem.addEventListener("click", () => toggleVault(vaultItem, v.name)); vaultItem.addEventListener("contextmenu", (e) => { e.preventDefault(); const isReadonly = false; ContextMenuManager.show(e.clientX, e.clientY, v.name, '', 'vault', isReadonly); }); attachTreeItemActionButton(vaultItem, v.name, "", "vault", false); attachTreeItemLongPress(vaultItem, () => ({ vault: v.name, path: "", type: "vault", isReadonly: false })); container.appendChild(vaultItem); const childContainer = el("div", { class: "tree-children collapsed", id: `vault-children-${v.name}` }); container.appendChild(childContainer); }); safeCreateIcons(); } async function focusVaultInSidebar(vaultName) { switchSidebarTab("vaults"); const vaultItem = document.querySelector(`.vault-item[data-vault="${CSS.escape(vaultName)}"]`); if (!vaultItem) return; document.querySelectorAll(".vault-item.focused").forEach((el) => el.classList.remove("focused")); vaultItem.classList.add("focused"); const childContainer = document.getElementById(`vault-children-${vaultName}`); if (childContainer && childContainer.classList.contains("collapsed")) { await toggleVault(vaultItem, vaultName, true); } scrollTreeItemIntoView(vaultItem, false); } async function refreshTagsForContext() { const vaultParam = state.selectedContextVault === "all" ? "" : `?vault=${encodeURIComponent(state.selectedContextVault)}`; const data = await api(`/api/tags${vaultParam}`); const filteredTags = TagFilterService.filterTags(data.tags); renderTagCloud(filteredTags); } // --------------------------------------------------------------------------- // Helper: Check if path should be displayed based on hideHiddenFiles setting // --------------------------------------------------------------------------- function shouldDisplayPath(path, vaultName) { // Get hideHiddenFiles setting for this vault (default: false = show all) const settings = state.vaultSettings[vaultName] || { hideHiddenFiles: false }; if (!settings.hideHiddenFiles) { // Show all files return true; } // Check if any segment of the path starts with a dot (hidden) const segments = path.split("/").filter(Boolean); for (const segment of segments) { if (segment.startsWith(".")) { return false; // Hide this path } } return true; // Show this path } async function loadVaultSettings() { try { const settings = await api("/api/vaults/settings/all"); state.vaultSettings = settings; } catch (err) { console.error("Failed to load vault settings:", err); state.vaultSettings = {}; } } // --------------------------------------------------------------------------- // Sidebar — Vault tree // --------------------------------------------------------------------------- async function loadVaults() { const vaults = await api("/api/vaults"); state.allVaults = vaults; const container = document.getElementById("vault-tree"); container.innerHTML = ""; // Prepare dropdown options const dropdownOptions = [{ value: "all", text: "Tous les vaults" }, ...vaults.map((v) => ({ value: v.name, text: v.name }))]; // Populate custom dropdowns populateCustomDropdown("vault-filter-dropdown", dropdownOptions, "all"); populateCustomDropdown("vault-quick-select-dropdown", dropdownOptions, "all"); // Populate standard selects _populateRecentVaultFilter(); if (typeof DashboardRecentWidget !== "undefined" && DashboardRecentWidget.populateVaultFilter) { DashboardRecentWidget.populateVaultFilter(); } vaults.forEach((v) => { // Sidebar tree entry const vaultItem = el("div", { class: "tree-item vault-item", "data-vault": v.name }, [icon("chevron-right", 14), getVaultIcon(v.name, 16), el("span", { class: "tree-item-text" }, [document.createTextNode(v.name)]), smallBadge(v.file_count)]); vaultItem.addEventListener("click", () => toggleVault(vaultItem, v.name)); vaultItem.addEventListener("contextmenu", (e) => { e.preventDefault(); const isReadonly = false; ContextMenuManager.show(e.clientX, e.clientY, v.name, '', 'vault', isReadonly); }); attachTreeItemActionButton(vaultItem, v.name, "", "vault", false); attachTreeItemLongPress(vaultItem, () => ({ vault: v.name, path: "", type: "vault", isReadonly: false })); container.appendChild(vaultItem); const childContainer = el("div", { class: "tree-children collapsed", id: `vault-children-${v.name}` }); container.appendChild(childContainer); }); syncVaultSelectors(); safeCreateIcons(); } /** * Refreshes the sidebar tree while preserving the expanded state of vaults and folders. * Optimized to avoid a full sidebar wipe and minimize visible loading states. */ /** * Incrementally update a directory container without wiping existing DOM. * Only adds new items, removes deleted ones, and updates changed ones. */ async function incrementalLoadDirectory(vaultName, dirPath, container) { let data; try { const url = `/api/browse/${encodeURIComponent(vaultName)}?path=${encodeURIComponent(dirPath)}`; data = await api(url); } catch (err) { // Server unavailable — keep existing content return; } // Build a map of existing DOM elements by path const existingItems = {}; const existingChildren = {}; // path -> child container (for directories) for (let i = 0; i < container.children.length; i++) { const child = container.children[i]; if (child.classList.contains("tree-item") && child.dataset.path) { existingItems[child.dataset.path] = child; // The next sibling should be the tree-children container for this directory if (i + 1 < container.children.length) { const next = container.children[i + 1]; if (next.classList.contains("tree-children")) { existingChildren[child.dataset.path] = next; } } } } const fragment = document.createDocumentFragment(); data.items.forEach((item) => { if (!shouldDisplayPath(item.path, vaultName)) return; const existing = existingItems[item.path]; if (existing) { // Item already exists — reuse it, but update text/badge if needed const textEl = existing.querySelector(".tree-item-text"); const displayName = item.type === "file" && item.name.match(/\.md$/i) ? item.name.replace(/\.md$/i, "") : item.name; if (textEl && textEl.textContent !== displayName) { textEl.textContent = displayName; } // Update badge for directories if (item.type === "directory") { const badge = existing.querySelector(".badge-small"); const newBadge = `(${item.children_count})`; if (badge && badge.textContent !== newBadge) { badge.textContent = newBadge; } else if (!badge) { existing.appendChild(smallBadge(item.children_count)); } } fragment.appendChild(existing); // Also re-add the child container for directories if (item.type === "directory" && existingChildren[item.path]) { fragment.appendChild(existingChildren[item.path]); } else if (item.type === "directory") { // Directory existed but no child container — create one const subContainer = el("div", { class: "tree-children collapsed", id: `dir-${vaultName}-${item.path}` }); fragment.appendChild(subContainer); } } else { // New item — create it if (item.type === "directory") { const dirItem = el("div", { class: "tree-item", "data-vault": vaultName, "data-path": item.path }, [icon("chevron-right", 14), icon("folder", 16), el("span", { class: "tree-item-text" }, [document.createTextNode(item.name)]), smallBadge(item.children_count)]); attachTreeItemActionButton(dirItem, vaultName, item.path, "directory", false); attachTreeItemLongPress(dirItem, () => ({ vault: vaultName, path: item.path, type: "directory", isReadonly: false })); fragment.appendChild(dirItem); const subContainer = el("div", { class: "tree-children collapsed", id: `dir-${vaultName}-${item.path}` }); fragment.appendChild(subContainer); dirItem.addEventListener("click", async () => { scrollTreeItemIntoView(dirItem, false); if (subContainer.classList.contains("collapsed")) { if (subContainer.children.length === 0) { await loadDirectory(vaultName, item.path, subContainer); } subContainer.classList.remove("collapsed"); const chev = dirItem.querySelector("[data-lucide]"); if (chev) chev.setAttribute("data-lucide", "chevron-down"); safeCreateIcons(); } else { subContainer.classList.add("collapsed"); const chev = dirItem.querySelector("[data-lucide]"); if (chev) chev.setAttribute("data-lucide", "chevron-right"); safeCreateIcons(); } }); dirItem.addEventListener("contextmenu", (e) => { e.preventDefault(); ContextMenuManager.show(e.clientX, e.clientY, vaultName, item.path, "directory", false); }); } else { const fileIconName = getFileIcon(item.name); const displayName = item.name.match(/\.md$/i) ? item.name.replace(/\.md$/i, "") : item.name; const fileItem = el("div", { class: "tree-item", "data-vault": vaultName, "data-path": item.path }, [icon(fileIconName, 16), el("span", { class: "tree-item-text" }, [document.createTextNode(displayName)])]); attachTreeItemActionButton(fileItem, vaultName, item.path, "file", false); attachTreeItemLongPress(fileItem, () => ({ vault: vaultName, path: item.path, type: "file", isReadonly: false })); fileItem.addEventListener("click", () => { scrollTreeItemIntoView(fileItem, false); TabManager.openPreview(vaultName, item.path); closeMobileSidebar(); }); fileItem.addEventListener("dblclick", (e) => { e.preventDefault(); TabManager.openPersistent(vaultName, item.path); }); fileItem.addEventListener("contextmenu", (e) => { e.preventDefault(); ContextMenuManager.show(e.clientX, e.clientY, vaultName, item.path, "file", false); }); fragment.appendChild(fileItem); } } }); // Replace container content in a single batch operation to avoid flash container.textContent = ""; container.appendChild(fragment); } export async function refreshSidebarTreePreservingState() { // 1. Capture expanded states const expandedVaults = Array.from(document.querySelectorAll(".vault-item")) .filter((v) => { const children = document.getElementById(`vault-children-${v.dataset.vault}`); return children && !children.classList.contains("collapsed"); }) .map((v) => v.dataset.vault); const expandedDirs = Array.from(document.querySelectorAll(".tree-item[data-path]")) .filter((item) => { const vault = item.dataset.vault; const path = item.dataset.path; const children = document.getElementById(`dir-${vault}-${path}`); return children && !children.classList.contains("collapsed"); }) .map((item) => ({ vault: item.dataset.vault, path: item.dataset.path })); const selectedItem = document.querySelector(".tree-item.path-selected"); const selectedState = selectedItem ? { vault: selectedItem.dataset.vault, path: selectedItem.dataset.path } : null; // 2. Soft update: vault names/counts without wiping the tree try { const vaults = await api("/api/vaults"); state.allVaults = vaults; vaults.forEach((v) => { const vItem = document.querySelector(`.vault-item[data-vault="${CSS.escape(v.name)}"]`); if (vItem) { const badge = vItem.querySelector(".badge-small"); if (badge) badge.textContent = `(${v.file_count})`; } }); } catch (e) { console.warn("Soft vault refresh failed, falling back to full reload", e); await loadVaults(); return; } // 3. Incrementally update expanded vaults (no DOM wipe) for (const vName of expandedVaults) { const container = document.getElementById(`vault-children-${vName}`); if (container) { await incrementalLoadDirectory(vName, "", container); } } // 4. Incrementally update expanded directories (parents first, no DOM wipe) expandedDirs.sort((a, b) => a.path.split("/").length - b.path.split("/").length); for (const dir of expandedDirs) { const container = document.getElementById(`dir-${dir.vault}-${dir.path}`); if (container) { try { await incrementalLoadDirectory(dir.vault, dir.path, container); container.classList.remove("collapsed"); const dItem = document.querySelector(`.tree-item[data-vault="${CSS.escape(dir.vault)}"][data-path="${CSS.escape(dir.path)}"]`); if (dItem) { const chev = dItem.querySelector("[data-lucide]"); if (chev) chev.setAttribute("data-lucide", "chevron-down"); } } catch (e) { console.error(`Failed to refresh directory ${dir.vault}/${dir.path}`, e); } } } // 5. Restore selection if (selectedState) { await focusPathInSidebar(selectedState.vault, selectedState.path, { alignToTop: false }); } safeCreateIcons(); } async function toggleVault(itemEl, vaultName, forceExpand) { const childContainer = document.getElementById(`vault-children-${vaultName}`); if (!childContainer) return; scrollTreeItemIntoView(itemEl, false); const shouldExpand = forceExpand || childContainer.classList.contains("collapsed"); if (shouldExpand) { // Expand — load children if empty if (childContainer.children.length === 0) { await loadDirectory(vaultName, "", childContainer); } childContainer.classList.remove("collapsed"); // Swap chevron const chevron = itemEl.querySelector("[data-lucide]"); if (chevron) chevron.setAttribute("data-lucide", "chevron-down"); safeCreateIcons(); } else { childContainer.classList.add("collapsed"); const chevron = itemEl.querySelector("[data-lucide]"); if (chevron) chevron.setAttribute("data-lucide", "chevron-right"); safeCreateIcons(); } } async function expandDirectoryInSidebar(vaultName, dirPath, dirItem) { const subContainer = document.getElementById(`dir-${vaultName}-${dirPath}`); if (!subContainer) return null; if (subContainer.children.length === 0) { await loadDirectory(vaultName, dirPath, subContainer); } subContainer.classList.remove("collapsed"); if (dirItem) { const chevron = dirItem.querySelector("[data-lucide]"); if (chevron) chevron.setAttribute("data-lucide", "chevron-down"); } safeCreateIcons(); return subContainer; } async function focusPathInSidebar(vaultName, targetPath, options) { switchSidebarTab("vaults"); const vaultItem = document.querySelector(`.vault-item[data-vault="${CSS.escape(vaultName)}"]`); if (!vaultItem) return; document.querySelectorAll(".vault-item.focused").forEach((el) => el.classList.remove("focused")); vaultItem.classList.add("focused"); const vaultContainer = document.getElementById(`vault-children-${vaultName}`); if (!vaultContainer) return; if (vaultContainer.classList.contains("collapsed")) { await toggleVault(vaultItem, vaultName, true); } if (!targetPath) { // Clear any previous path selection document.querySelectorAll(".tree-item.path-selected").forEach((el) => el.classList.remove("path-selected")); scrollTreeItemIntoView(vaultItem, options && options.alignToTop); return; } const segments = targetPath.split("/").filter(Boolean); let currentContainer = vaultContainer; let cumulativePath = ""; let lastTargetItem = null; for (let index = 0; index < segments.length; index++) { cumulativePath += (cumulativePath ? "/" : "") + segments[index]; let targetItem = null; try { targetItem = currentContainer.querySelector(`.tree-item[data-vault="${CSS.escape(vaultName)}"][data-path="${CSS.escape(cumulativePath)}"]`); } catch (e) { targetItem = null; } if (!targetItem) { return; } lastTargetItem = targetItem; const isLastSegment = index === segments.length - 1; if (!isLastSegment) { const nextContainer = await expandDirectoryInSidebar(vaultName, cumulativePath, targetItem); if (nextContainer) { currentContainer = nextContainer; } } } if (lastTargetItem && options && options.expandTarget) { await expandDirectoryInSidebar(vaultName, targetPath, lastTargetItem); } // Clear previous path selections and highlight the final target document.querySelectorAll(".tree-item.path-selected").forEach((el) => el.classList.remove("path-selected")); if (lastTargetItem) { lastTargetItem.classList.add("path-selected"); } scrollTreeItemIntoView(lastTargetItem, options && options.alignToTop); } function getParentDirectoryPath(filePath) { if (!filePath) return ""; const segments = filePath.split("/").filter(Boolean); if (segments.length <= 1) return ""; segments.pop(); return segments.join("/"); } function syncActiveFileTreeItem(vaultName, filePath) { document.querySelectorAll(".tree-item.active").forEach((el) => el.classList.remove("active")); if (!vaultName || !filePath) return; const selector = `.tree-item[data-vault="${CSS.escape(vaultName)}"][data-path="${CSS.escape(filePath)}"]`; try { const active = document.querySelector(selector); if (active) active.classList.add("active"); } catch (e) { /* selector might fail on special chars */ } } async function loadDirectory(vaultName, dirPath, container) { // Only show the loading spinner if the container is currently empty const isEmpty = container.children.length === 0; if (isEmpty) { container.innerHTML = '
'; } var data; try { const url = `/api/browse/${encodeURIComponent(vaultName)}?path=${encodeURIComponent(dirPath)}`; data = await api(url); } catch (err) { container.innerHTML = '
Erreur de chargement
'; return; } container.innerHTML = ""; const fragment = document.createDocumentFragment(); data.items.forEach((item) => { // Apply client-side filtering for hidden files if (!shouldDisplayPath(item.path, vaultName)) { return; // Skip this item } if (item.type === "directory") { const dirItem = el("div", { class: "tree-item", "data-vault": vaultName, "data-path": item.path }, [icon("chevron-right", 14), icon("folder", 16), el("span", { class: "tree-item-text" }, [document.createTextNode(item.name)]), smallBadge(item.children_count)]); attachTreeItemActionButton(dirItem, vaultName, item.path, "directory", false); attachTreeItemLongPress(dirItem, () => ({ vault: vaultName, path: item.path, type: "directory", isReadonly: false })); fragment.appendChild(dirItem); const subContainer = el("div", { class: "tree-children collapsed", id: `dir-${vaultName}-${item.path}` }); fragment.appendChild(subContainer); dirItem.addEventListener("click", async () => { scrollTreeItemIntoView(dirItem, false); if (subContainer.classList.contains("collapsed")) { if (subContainer.children.length === 0) { await loadDirectory(vaultName, item.path, subContainer); } subContainer.classList.remove("collapsed"); const chev = dirItem.querySelector("[data-lucide]"); if (chev) chev.setAttribute("data-lucide", "chevron-down"); safeCreateIcons(); } else { subContainer.classList.add("collapsed"); const chev = dirItem.querySelector("[data-lucide]"); if (chev) chev.setAttribute("data-lucide", "chevron-right"); safeCreateIcons(); } }); dirItem.addEventListener("contextmenu", (e) => { e.preventDefault(); const isReadonly = false; ContextMenuManager.show(e.clientX, e.clientY, vaultName, item.path, 'directory', isReadonly); }); } else { const fileIconName = getFileIcon(item.name); const displayName = item.name.match(/\.md$/i) ? item.name.replace(/\.md$/i, "") : item.name; const fileItem = el("div", { class: "tree-item", "data-vault": vaultName, "data-path": item.path }, [icon(fileIconName, 16), el("span", { class: "tree-item-text" }, [document.createTextNode(displayName)])]); attachTreeItemActionButton(fileItem, vaultName, item.path, "file", false); attachTreeItemLongPress(fileItem, () => ({ vault: vaultName, path: item.path, type: "file", isReadonly: false })); fileItem.addEventListener("click", () => { scrollTreeItemIntoView(fileItem, false); TabManager.openPreview(vaultName, item.path); closeMobileSidebar(); }); fileItem.addEventListener("dblclick", (e) => { e.preventDefault(); TabManager.openPersistent(vaultName, item.path); }); fileItem.addEventListener("contextmenu", (e) => { e.preventDefault(); const isReadonly = false; ContextMenuManager.show(e.clientX, e.clientY, vaultName, item.path, 'file', isReadonly); }); fragment.appendChild(fileItem); } }); container.appendChild(fragment); safeCreateIcons(); } // --------------------------------------------------------------------------- // Sidebar filter // --------------------------------------------------------------------------- function initSidebarFilter() { const input = document.getElementById("sidebar-filter-input"); const caseBtn = document.getElementById("sidebar-filter-case-btn"); const clearBtn = document.getElementById("sidebar-filter-clear-btn"); input.addEventListener("input", () => { const hasText = input.value.length > 0; clearBtn.style.display = hasText ? "flex" : "none"; clearTimeout(state.filterDebounce); state.filterDebounce = setTimeout(async () => { const q = state.sidebarFilterCaseSensitive ? input.value.trim() : input.value.trim().toLowerCase(); if (hasText) { if (state.activeSidebarTab === "vaults") { await performTreeSearch(q); } else { filterTagCloud(q); } } else { if (state.activeSidebarTab === "vaults") { await restoreSidebarTree(); } else { filterTagCloud(""); } } }, 220); }); caseBtn.addEventListener("click", async () => { state.sidebarFilterCaseSensitive = !state.sidebarFilterCaseSensitive; caseBtn.classList.toggle("active"); const q = state.sidebarFilterCaseSensitive ? input.value.trim() : input.value.trim().toLowerCase(); if (input.value.trim()) { if (state.activeSidebarTab === "vaults") { await performTreeSearch(q); } else { filterTagCloud(q); } } }); clearBtn.addEventListener("click", async () => { input.value = ""; clearBtn.style.display = "none"; state.sidebarFilterCaseSensitive = false; caseBtn.classList.remove("active"); clearTimeout(state.filterDebounce); if (state.activeSidebarTab === "vaults") { await restoreSidebarTree(); } else { filterTagCloud(""); } }); clearBtn.style.display = "none"; } async function performTreeSearch(query) { if (!query) { await restoreSidebarTree(); return; } try { const vaultParam = state.selectedContextVault === "all" ? "all" : state.selectedContextVault; const url = `/api/tree-search?q=${encodeURIComponent(query)}&vault=${encodeURIComponent(vaultParam)}`; const data = await api(url); renderFilteredSidebarResults(query, data.results); } catch (err) { console.error("Tree search error:", err); renderFilteredSidebarResults(query, []); } } async function restoreSidebarTree() { await refreshSidebarForContext(); if (state.currentVault) { focusPathInSidebar(state.currentVault, state.currentPath || "", { alignToTop: false }).catch(() => {}); } } function renderFilteredSidebarResults(query, results) { const container = document.getElementById("vault-tree"); container.innerHTML = ""; const grouped = new Map(); results.forEach((result) => { if (!grouped.has(result.vault)) { grouped.set(result.vault, []); } grouped.get(result.vault).push(result); }); if (grouped.size === 0) { container.appendChild(el("div", { class: "sidebar-filter-empty" }, [document.createTextNode("Aucun répertoire ou fichier correspondant.")])); return; } grouped.forEach((entries, vaultName) => { entries.sort((a, b) => a.path.localeCompare(b.path, undefined, { sensitivity: "base" })); const vaultHeader = el("div", { class: "tree-item vault-item filter-results-header", "data-vault": vaultName }, [getVaultIcon(vaultName, 16), document.createTextNode(` ${vaultName} `), smallBadge(entries.length)]); container.appendChild(vaultHeader); const resultsWrapper = el("div", { class: "filter-results-group" }); entries.forEach((entry) => { const resultItem = el( "div", { class: `tree-item filter-result-item filter-result-${entry.type}`, "data-vault": entry.vault, "data-path": entry.path, "data-type": entry.type, }, [icon(entry.type === "directory" ? "folder" : getFileIcon(entry.name), 16)], ); const textWrap = el("div", { class: "filter-result-text" }); const primary = el("div", { class: "filter-result-primary" }); appendHighlightedText(primary, entry.name, query, state.sidebarFilterCaseSensitive); const secondary = el("div", { class: "filter-result-secondary" }); appendHighlightedText(secondary, entry.path, query, state.sidebarFilterCaseSensitive); textWrap.appendChild(primary); textWrap.appendChild(secondary); resultItem.appendChild(textWrap); resultItem.addEventListener("click", async () => { const input = document.getElementById("sidebar-filter-input"); const clearBtn = document.getElementById("sidebar-filter-clear-btn"); if (input) input.value = ""; if (clearBtn) clearBtn.style.display = "none"; await restoreSidebarTree(); if (entry.type === "directory") { await focusPathInSidebar(entry.vault, entry.path, { alignToTop: true, expandTarget: true }); } else { await TabManager.openPreview(entry.vault, entry.path); await focusPathInSidebar(entry.vault, getParentDirectoryPath(entry.path), { alignToTop: true, expandTarget: true }); syncActiveFileTreeItem(entry.vault, entry.path); } closeMobileSidebar(); }); resultsWrapper.appendChild(resultItem); }); container.appendChild(resultsWrapper); }); flushIcons(); } function filterSidebarTree(query) { const tree = document.getElementById("vault-tree"); const items = tree.querySelectorAll(".tree-item"); const containers = tree.querySelectorAll(".tree-children"); if (!query) { items.forEach((item) => item.classList.remove("filtered-out")); containers.forEach((c) => { c.classList.remove("filtered-out"); // Keep current collapsed state when clearing filter }); return; } // First pass: mark all as filtered out items.forEach((item) => item.classList.add("filtered-out")); containers.forEach((c) => c.classList.add("filtered-out")); // Second pass: find matching items and mark them + ancestors + descendants const matchingItems = new Set(); items.forEach((item) => { const text = state.sidebarFilterCaseSensitive ? item.textContent : item.textContent.toLowerCase(); const searchQuery = state.sidebarFilterCaseSensitive ? query : query.toLowerCase(); if (text.includes(searchQuery)) { matchingItems.add(item); item.classList.remove("filtered-out"); // Show all ancestor containers let parent = item.parentElement; while (parent && parent !== tree) { parent.classList.remove("filtered-out"); if (parent.classList.contains("tree-children")) { parent.classList.remove("collapsed"); } parent = parent.parentElement; } // If this is a directory (has a children container after it), show all descendants const nextEl = item.nextElementSibling; if (nextEl && nextEl.classList.contains("tree-children")) { nextEl.classList.remove("filtered-out"); nextEl.classList.remove("collapsed"); // Recursively show all children in this container showAllDescendants(nextEl); } } }); // Third pass: show items that are descendants of matching directories // and ensure their containers are visible matchingItems.forEach((item) => { const nextEl = item.nextElementSibling; if (nextEl && nextEl.classList.contains("tree-children")) { const children = nextEl.querySelectorAll(".tree-item"); children.forEach((child) => child.classList.remove("filtered-out")); } }); } function showAllDescendants(container) { const items = container.querySelectorAll(".tree-item"); items.forEach((item) => { item.classList.remove("filtered-out"); // If this item has children, also show them const nextEl = item.nextElementSibling; if (nextEl && nextEl.classList.contains("tree-children")) { nextEl.classList.remove("filtered-out"); nextEl.classList.remove("collapsed"); } }); // Also ensure all nested containers are visible const nestedContainers = container.querySelectorAll(".tree-children"); nestedContainers.forEach((c) => { c.classList.remove("filtered-out"); c.classList.remove("collapsed"); }); } function filterTagCloud(query) { const tags = document.querySelectorAll("#tag-cloud .tag-item"); tags.forEach((tag) => { const text = state.sidebarFilterCaseSensitive ? tag.textContent : tag.textContent.toLowerCase(); const searchQuery = state.sidebarFilterCaseSensitive ? query : query.toLowerCase(); if (!query || text.includes(searchQuery)) { tag.classList.remove("filtered-out"); } else { tag.classList.add("filtered-out"); } }); } // --------------------------------------------------------------------------- // Tag Filter Service // --------------------------------------------------------------------------- const TagFilterService = { defaultFilters: [ { pattern: "#<% ... %>", regex: "#<%.*%>", enabled: true }, { pattern: "#{{ ... }}", regex: "#\\{\\{.*\\}\\}", enabled: true }, { pattern: "#{ ... }", regex: "#\\{.*\\}", enabled: true }, ], getConfig() { const stored = localStorage.getItem("obsigate-tag-filters"); if (stored) { try { return JSON.parse(stored); } catch (e) { return { tagFilters: this.defaultFilters }; } } return { tagFilters: this.defaultFilters }; }, saveConfig(config) { localStorage.setItem("obsigate-tag-filters", JSON.stringify(config)); }, patternToRegex(pattern) { // 1. Escape ALL special regex characters // We use a broader set including * and . let regex = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // 2. Convert escaped '*' to '.*' (wildcard) regex = regex.replace(/\\\*/g, ".*"); // 3. Convert escaped '...' (or any sequence of 2+ dots like ..) to '.*' // We also handle optional whitespace around it to make it more user-friendly regex = regex.replace(/\s*\\\.{2,}\s*/g, ".*"); return regex; }, isTagFiltered(tag) { const config = this.getConfig(); const filters = config.tagFilters || this.defaultFilters; const tagWithHash = `#${tag}`; for (const filter of filters) { if (!filter.enabled) continue; try { // Robustly handle regex with or without ^/$ let patternStr = filter.regex; if (!patternStr.startsWith("^")) patternStr = "^" + patternStr; if (!patternStr.endsWith("$")) patternStr = patternStr + "$"; const regex = new RegExp(patternStr); if (regex.test(tagWithHash)) { return true; } } catch (e) { console.warn("Invalid regex:", filter.regex, e); } } return false; }, filterTags(tags) { const filtered = {}; Object.entries(tags).forEach(([tag, count]) => { if (!this.isTagFiltered(tag)) { filtered[tag] = count; } }); return filtered; }, }; // --------------------------------------------------------------------------- // Tags // --------------------------------------------------------------------------- async function loadTags() { const data = await api("/api/tags"); const filteredTags = TagFilterService.filterTags(data.tags); renderTagCloud(filteredTags); } function renderTagCloud(tags) { const cloud = document.getElementById("tag-cloud"); cloud.innerHTML = ""; const counts = Object.values(tags); if (counts.length === 0) return; const maxCount = Math.max(...counts); const minSize = 0.7; const maxSize = 1.25; Object.entries(tags).forEach(([tag, count]) => { const ratio = maxCount > 1 ? (count - 1) / (maxCount - 1) : 0; const size = minSize + ratio * (maxSize - minSize); const tagEl = el("span", { class: "tag-item", style: `font-size:${size}rem` }, [document.createTextNode(`#${tag}`)]); tagEl.addEventListener("click", () => searchByTag(tag)); cloud.appendChild(tagEl); }); } function addTagFilter(tag) { if (!state.selectedTags.includes(tag)) { state.selectedTags.push(tag); performTagSearch(); } } function removeTagFilter(tag) { state.selectedTags = state.selectedTags.filter((t) => t !== tag); if (state.selectedTags.length > 0) { performTagSearch(); } else { const input = document.getElementById("search-input"); if (input.value.trim()) { performAdvancedSearch(input.value.trim(), document.getElementById("vault-filter").value, null); } else { showWelcome(); } } } function performTagSearch() { const input = document.getElementById("search-input"); const query = input.value.trim(); const vault = document.getElementById("vault-filter").value; performAdvancedSearch(query, vault, state.selectedTags.length > 0 ? state.selectedTags.join(",") : null); } function buildSearchResultsHeader(data, query, tagFilter) { const header = el("div", { class: "search-results-header" }); const summaryText = el("span", { class: "search-results-summary-text" }); if (query && tagFilter) { summaryText.textContent = `${data.count} résultat(s) pour "${query}" avec les tags`; } else if (query) { summaryText.textContent = `${data.count} résultat(s) pour "${query}"`; } else if (tagFilter) { summaryText.textContent = `${data.count} fichier(s) avec les tags`; } else { summaryText.textContent = `${data.count} résultat(s)`; } header.appendChild(summaryText); if (state.selectedTags.length > 0) { const activeTags = el("div", { class: "search-results-active-tags" }); state.selectedTags.forEach((tag) => { const removeBtn = el( "button", { class: "search-results-active-tag-remove", title: `Retirer ${tag} du filtre`, "aria-label": `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); }); header.appendChild(activeTags); } return header; } function searchByTag(tag) { addTagFilter(tag); } export { initVaultContext, setSelectedVaultContext, syncVaultSelectors, shouldDisplayPath, loadVaults, initSidebarFilter, TagFilterService, loadTags, scrollTreeItemIntoView, refreshSidebarForContext, focusVaultInSidebar, refreshTagsForContext, syncActiveFileTreeItem, searchByTag, addTagFilter, buildSearchResultsHeader, removeTagFilter };