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 = '