1|/* ObsiGate — UI: theme, sidebar, context menus, tabs, toast, find-in-page */ import { state } from './state.js'; import { openFile } from './viewer.js'; import { safeCreateIcons } from './utils.js'; // --------------------------------------------------------------------------- // Right Sidebar Manager // --------------------------------------------------------------------------- export const RightSidebarManager = { init() { this.loadState(); this.initToggle(); this.initResize(); }, loadState() { const savedVisible = localStorage.getItem("obsigate-right-sidebar-visible"); const savedWidth = localStorage.getItem("obsigate-right-sidebar-width"); if (savedVisible !== null) { state.rightSidebarVisible = savedVisible === "true"; } if (savedWidth) { state.rightSidebarWidth = parseInt(savedWidth) || 280; } this.applyState(); }, applyState() { const sidebar = document.getElementById("right-sidebar"); const handle = document.getElementById("right-sidebar-resize-handle"); const tocBtn = document.getElementById("toc-toggle-btn"); const headerToggleBtn = document.getElementById("right-sidebar-toggle-btn"); if (!sidebar) return; if (state.rightSidebarVisible) { sidebar.classList.remove("hidden"); sidebar.style.width = `${state.rightSidebarWidth}px`; if (handle) handle.classList.remove("hidden"); if (tocBtn) { tocBtn.classList.add("active"); tocBtn.title = "Masquer le sommaire"; } if (headerToggleBtn) { headerToggleBtn.title = "Masquer le panneau"; headerToggleBtn.setAttribute("aria-label", "Masquer le panneau"); } } else { sidebar.classList.add("hidden"); if (handle) handle.classList.add("hidden"); if (tocBtn) { tocBtn.classList.remove("active"); tocBtn.title = "Afficher le sommaire"; } if (headerToggleBtn) { headerToggleBtn.title = "Afficher le panneau"; headerToggleBtn.setAttribute("aria-label", "Afficher le panneau"); } } // Update icons safeCreateIcons(); }, toggle() { state.rightSidebarVisible = !state.rightSidebarVisible; localStorage.setItem("obsigate-right-sidebar-visible", state.rightSidebarVisible); this.applyState(); }, initToggle() { const toggleBtn = document.getElementById("right-sidebar-toggle-btn"); if (toggleBtn) { toggleBtn.addEventListener("click", () => this.toggle()); } }, initResize() { const handle = document.getElementById("right-sidebar-resize-handle"); const sidebar = document.getElementById("right-sidebar"); if (!handle || !sidebar) return; let isResizing = false; let startX = 0; let startWidth = 0; const onMouseDown = (e) => { isResizing = true; startX = e.clientX; startWidth = sidebar.offsetWidth; handle.classList.add("active"); document.body.style.cursor = "ew-resize"; document.body.style.userSelect = "none"; }; const onMouseMove = (e) => { if (!isResizing) return; const delta = startX - e.clientX; let newWidth = startWidth + delta; // Constrain width newWidth = Math.max(200, Math.min(400, newWidth)); sidebar.style.width = `${newWidth}px`; state.rightSidebarWidth = newWidth; }; const onMouseUp = () => { if (!isResizing) return; isResizing = false; handle.classList.remove("active"); document.body.style.cursor = ""; document.body.style.userSelect = ""; localStorage.setItem("obsigate-right-sidebar-width", state.rightSidebarWidth); }; handle.addEventListener("mousedown", onMouseDown); document.addEventListener("mousemove", onMouseMove); document.addEventListener("mouseup", onMouseUp); }, }; // --------------------------------------------------------------------------- // Theme // --------------------------------------------------------------------------- export function initTheme() { const saved = localStorage.getItem("obsigate-theme") || "dark"; applyTheme(saved); } export function applyTheme(theme) { document.documentElement.setAttribute("data-theme", theme); localStorage.setItem("obsigate-theme", theme); // Update theme button icon and label const themeBtn = document.getElementById("theme-toggle"); const themeLabel = document.getElementById("theme-label"); if (themeBtn && themeLabel) { const icon = themeBtn.querySelector("i"); if (icon) { icon.setAttribute("data-lucide", theme === "dark" ? "moon" : "sun"); } themeLabel.textContent = theme === "dark" ? "Sombre" : "Clair"; safeCreateIcons(); } // Swap highlight.js theme const darkSheet = document.getElementById("hljs-theme-dark"); const lightSheet = document.getElementById("hljs-theme-light"); if (darkSheet && lightSheet) { darkSheet.disabled = theme !== "dark"; lightSheet.disabled = theme !== "light"; } } export function toggleTheme() { const current = document.documentElement.getAttribute("data-theme"); applyTheme(current === "dark" ? "light" : "dark"); } export function initHeaderMenu() { const menuBtn = document.getElementById("header-menu-btn"); const menuDropdown = document.getElementById("header-menu-dropdown"); if (!menuBtn || !menuDropdown) return; menuBtn.addEventListener("click", (e) => { e.stopPropagation(); menuBtn.classList.toggle("active"); menuDropdown.classList.toggle("active"); }); // Close menu when clicking outside document.addEventListener("click", (e) => { if (!menuDropdown.contains(e.target) && e.target !== menuBtn) { menuBtn.classList.remove("active"); menuDropdown.classList.remove("active"); } }); // Prevent menu from closing when clicking inside menuDropdown.addEventListener("click", (e) => { e.stopPropagation(); }); } function closeHeaderMenu() { const menuBtn = document.getElementById("header-menu-btn"); const menuDropdown = document.getElementById("header-menu-dropdown"); if (!menuBtn || !menuDropdown) return; menuBtn.classList.remove("active"); menuDropdown.classList.remove("active"); } // --------------------------------------------------------------------------- // Custom Dropdowns // --------------------------------------------------------------------------- export function initCustomDropdowns() { document.querySelectorAll(".custom-dropdown").forEach((dropdown) => { const trigger = dropdown.querySelector(".custom-dropdown-trigger"); const options = dropdown.querySelectorAll(".custom-dropdown-option"); const hiddenInput = dropdown.querySelector('input[type="hidden"]'); const selectedText = dropdown.querySelector(".custom-dropdown-selected"); const menu = dropdown.querySelector(".custom-dropdown-menu"); if (!trigger) return; // Toggle dropdown trigger.addEventListener("click", (e) => { e.stopPropagation(); const isOpen = dropdown.classList.contains("open"); // Close all other dropdowns document.querySelectorAll(".custom-dropdown.open").forEach((d) => { if (d !== dropdown) d.classList.remove("open"); }); dropdown.classList.toggle("open", !isOpen); trigger.setAttribute("aria-expanded", !isOpen); // Position fixed menu for sidebar dropdowns if (!isOpen && dropdown.classList.contains("sidebar-dropdown") && menu) { const rect = trigger.getBoundingClientRect(); menu.style.top = `${rect.bottom + 4}px`; menu.style.left = `${rect.left}px`; menu.style.width = `${rect.width}px`; } }); // Handle option selection options.forEach((option) => { option.addEventListener("click", (e) => { e.stopPropagation(); const value = option.getAttribute("data-value"); const text = option.textContent; // Update hidden input if (hiddenInput) { hiddenInput.value = value; // Trigger change event hiddenInput.dispatchEvent(new Event("change", { bubbles: true })); } // Update selected text if (selectedText) { selectedText.textContent = text; } // Update visual selection options.forEach((opt) => opt.classList.remove("selected")); option.classList.add("selected"); // Close dropdown dropdown.classList.remove("open"); trigger.setAttribute("aria-expanded", "false"); }); }); }); // Close dropdowns when clicking outside document.addEventListener("click", () => { document.querySelectorAll(".custom-dropdown.open").forEach((dropdown) => { dropdown.classList.remove("open"); const trigger = dropdown.querySelector(".custom-dropdown-trigger"); if (trigger) trigger.setAttribute("aria-expanded", "false"); }); }); } // Helper to populate custom dropdown options function populateCustomDropdown(dropdownId, optionsList, defaultValue) { const dropdown = document.getElementById(dropdownId); if (!dropdown) return; const optionsContainer = dropdown.querySelector(".custom-dropdown-menu"); const hiddenInput = dropdown.querySelector('input[type="hidden"]'); const selectedText = dropdown.querySelector(".custom-dropdown-selected"); if (!optionsContainer) return; // Clear existing options (keep the first one if it's the default) optionsContainer.innerHTML = ""; // Add new options optionsList.forEach((opt) => { const li = document.createElement("li"); li.className = "custom-dropdown-option"; li.setAttribute("role", "option"); li.setAttribute("data-value", opt.value); li.textContent = opt.text; if (opt.value === defaultValue) { li.classList.add("selected"); if (selectedText) selectedText.textContent = opt.text; if (hiddenInput) hiddenInput.value = opt.value; } optionsContainer.appendChild(li); }); // Re-initialize click handlers optionsContainer.querySelectorAll(".custom-dropdown-option").forEach((option) => { option.addEventListener("click", (e) => { e.stopPropagation(); const value = option.getAttribute("data-value"); const text = option.textContent; if (hiddenInput) { hiddenInput.value = value; hiddenInput.dispatchEvent(new Event("change", { bubbles: true })); } if (selectedText) { selectedText.textContent = text; } optionsContainer.querySelectorAll(".custom-dropdown-option").forEach((opt) => opt.classList.remove("selected")); option.classList.add("selected"); dropdown.classList.remove("open"); const trigger = dropdown.querySelector(".custom-dropdown-trigger"); if (trigger) trigger.setAttribute("aria-expanded", "false"); }); }); } // --------------------------------------------------------------------------- // Toast notifications // --------------------------------------------------------------------------- /** Display a brief toast message at the bottom of the viewport. */ export function showToast(message, type) { console.log("showToast called with:", message, type); type = type || "info"; let container = document.getElementById("toast-container"); if (!container) { container = document.createElement("div"); container.id = "toast-container"; container.className = "toast-container"; container.setAttribute("aria-live", "polite"); document.body.appendChild(container); } var toast = document.createElement("div"); toast.className = "toast toast-" + type; toast.textContent = message; container.appendChild(toast); // Trigger entrance animation requestAnimationFrame(function () { toast.classList.add("show"); }); setTimeout(function () { toast.classList.remove("show"); toast.addEventListener("transitionend", function () { toast.remove(); }); }, 3500); } // --------------------------------------------------------------------------- // Sidebar toggle (desktop) // --------------------------------------------------------------------------- export function initSidebarToggle() { const toggleBtn = document.getElementById("sidebar-toggle-btn"); const sidebar = document.getElementById("sidebar"); const resizeHandle = document.getElementById("sidebar-resize-handle"); if (!toggleBtn || !sidebar || !resizeHandle) return; // Restore saved state const savedState = localStorage.getItem("obsigate-sidebar-hidden"); if (savedState === "true") { sidebar.classList.add("hidden"); resizeHandle.classList.add("hidden"); toggleBtn.classList.add("active"); } toggleBtn.addEventListener("click", () => { const isHidden = sidebar.classList.toggle("hidden"); resizeHandle.classList.toggle("hidden", isHidden); toggleBtn.classList.toggle("active", isHidden); localStorage.setItem("obsigate-sidebar-hidden", isHidden ? "true" : "false"); }); } // --------------------------------------------------------------------------- // Mobile sidebar // --------------------------------------------------------------------------- export function initMobile() { const hamburger = document.getElementById("hamburger-btn"); const overlay = document.getElementById("sidebar-overlay"); const sidebar = document.getElementById("sidebar"); hamburger.addEventListener("click", () => { sidebar.classList.toggle("mobile-open"); overlay.classList.toggle("active"); }); overlay.addEventListener("click", () => { sidebar.classList.remove("mobile-open"); overlay.classList.remove("active"); }); } function closeMobileSidebar() { const sidebar = document.getElementById("sidebar"); const overlay = document.getElementById("sidebar-overlay"); if (sidebar) sidebar.classList.remove("mobile-open"); if (overlay) overlay.classList.remove("active"); } // --------------------------------------------------------------------------- // Resizable sidebar (horizontal) // --------------------------------------------------------------------------- export 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) // --------------------------------------------------------------------------- export 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; 4 ... [OUTPUT TRUNCATED - 30698 chars omitted out of 80698 total] ... `; overlay.appendChild(modal); document.body.appendChild(overlay); setTimeout(() => overlay.classList.add('active'), 10); safeCreateIcons(); const confirmBtn = modal.querySelector('#del-confirm-btn'); const cancelBtn = modal.querySelector('#del-cancel-btn'); const deleteFile = async () => { confirmBtn.disabled = true; confirmBtn.textContent = 'Suppression...'; try { await api(`/api/file/${encodeURIComponent(vault)}?path=${encodeURIComponent(path)}`, { method: 'DELETE', }); showToast('Fichier supprimé', 'success'); this._closeModal(overlay); await refreshSidebarTreePreservingState(); if (state.currentVault === vault && state.currentPath === path) { showWelcome(); } } catch (err) { showToast(err.message || 'Erreur lors de la suppression', 'error'); confirmBtn.disabled = false; confirmBtn.textContent = 'Supprimer définitivement'; } }; confirmBtn.addEventListener('click', deleteFile); cancelBtn.addEventListener('click', () => this._closeModal(overlay)); }, _createModalOverlay() { const overlay = document.createElement('div'); overlay.className = 'obsigate-modal-overlay'; overlay.addEventListener('click', (e) => { if (e.target === overlay) { this._closeModal(overlay); } }); return overlay; }, _closeModal(overlay) { overlay.classList.remove('active'); setTimeout(() => overlay.remove(), 200); } }; // --------------------------------------------------------------------------- // Find in Page Manager // --------------------------------------------------------------------------- export const FindInPageManager = { isOpen: false, searchTerm: "", matches: [], currentIndex: -1, options: { caseSensitive: false, wholeWord: false, useRegex: false, }, debounceTimer: null, previousFocus: null, init() { const bar = document.getElementById("find-in-page-bar"); const input = document.getElementById("find-input"); const prevBtn = document.getElementById("find-prev"); const nextBtn = document.getElementById("find-next"); const closeBtn = document.getElementById("find-close"); const caseSensitiveBtn = document.getElementById("find-case-sensitive"); const wholeWordBtn = document.getElementById("find-whole-word"); const regexBtn = document.getElementById("find-regex"); if (!bar || !input) return; // Keyboard shortcuts document.addEventListener("keydown", (e) => { // Ctrl+F or Cmd+F to open if ((e.ctrlKey || e.metaKey) && e.key === "f") { e.preventDefault(); this.open(); } // Escape to close if (e.key === "Escape" && this.isOpen) { e.preventDefault(); this.close(); } // Enter to go to next if (e.key === "Enter" && this.isOpen && document.activeElement === input) { e.preventDefault(); if (e.shiftKey) { this.goToPrevious(); } else { this.goToNext(); } } // F3 for next/previous if (e.key === "F3" && this.isOpen) { e.preventDefault(); if (e.shiftKey) { this.goToPrevious(); } else { this.goToNext(); } } }); // Input event with debounce input.addEventListener("input", (e) => { clearTimeout(this.debounceTimer); this.debounceTimer = setTimeout(() => { this.search(e.target.value); }, 250); }); // Navigation buttons prevBtn.addEventListener("click", () => this.goToPrevious()); nextBtn.addEventListener("click", () => this.goToNext()); // Close button closeBtn.addEventListener("click", () => this.close()); // Option toggles caseSensitiveBtn.addEventListener("click", () => { this.options.caseSensitive = !this.options.caseSensitive; caseSensitiveBtn.setAttribute("aria-pressed", this.options.caseSensitive); this.saveState(); if (this.searchTerm) this.search(this.searchTerm); }); wholeWordBtn.addEventListener("click", () => { this.options.wholeWord = !this.options.wholeWord; wholeWordBtn.setAttribute("aria-pressed", this.options.wholeWord); this.saveState(); if (this.searchTerm) this.search(this.searchTerm); }); regexBtn.addEventListener("click", () => { this.options.useRegex = !this.options.useRegex; regexBtn.setAttribute("aria-pressed", this.options.useRegex); this.saveState(); if (this.searchTerm) this.search(this.searchTerm); }); // Load saved state this.loadState(); }, open() { const bar = document.getElementById("find-in-page-bar"); const input = document.getElementById("find-input"); if (!bar || !input) return; this.previousFocus = document.activeElement; this.isOpen = true; bar.hidden = false; input.focus(); input.select(); safeCreateIcons(); }, close() { const bar = document.getElementById("find-in-page-bar"); if (!bar) return; this.isOpen = false; bar.hidden = true; this.clearHighlights(); this.matches = []; this.currentIndex = -1; this.searchTerm = ""; // Restore previous focus if (this.previousFocus && this.previousFocus.focus) { this.previousFocus.focus(); } }, search(term) { this.searchTerm = term; this.clearHighlights(); this.hideError(); if (!term || term.trim().length === 0) { this.updateCounter(); this.updateNavButtons(); return; } const contentArea = document.querySelector(".md-content"); if (!contentArea) { this.updateCounter(); this.updateNavButtons(); return; } try { const regex = this.createRegex(term); this.matches = []; this.findMatches(contentArea, regex); this.currentIndex = this.matches.length > 0 ? 0 : -1; this.highlightMatches(); this.updateCounter(); this.updateNavButtons(); if (this.matches.length > 0) { this.scrollToMatch(0); } } catch (err) { this.showError(err.message); this.matches = []; this.currentIndex = -1; this.updateCounter(); this.updateNavButtons(); } }, createRegex(term) { let pattern = term; if (!this.options.useRegex) { // Escape special regex characters pattern = term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } if (this.options.wholeWord) { pattern = "\\b" + pattern + "\\b"; } const flags = this.options.caseSensitive ? "g" : "gi"; return new RegExp(pattern, flags); }, findMatches(container, regex) { const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, { acceptNode: (node) => { // Skip code blocks, scripts, styles const parent = node.parentElement; if (!parent) return NodeFilter.FILTER_REJECT; const tagName = parent.tagName.toLowerCase(); if (["code", "pre", "script", "style"].includes(tagName)) { return NodeFilter.FILTER_REJECT; } // Skip empty text nodes if (!node.textContent || node.textContent.trim().length === 0) { return NodeFilter.FILTER_REJECT; } return NodeFilter.FILTER_ACCEPT; }, }); let node; while ((node = walker.nextNode())) { const text = node.textContent; let match; regex.lastIndex = 0; // Reset regex while ((match = regex.exec(text)) !== null) { this.matches.push({ node: node, index: match.index, length: match[0].length, text: match[0], }); // Prevent infinite loop with zero-width matches if (match.index === regex.lastIndex) { regex.lastIndex++; } } } }, highlightMatches() { const matchesByNode = new Map(); this.matches.forEach((match, idx) => { if (!matchesByNode.has(match.node)) { matchesByNode.set(match.node, []); } matchesByNode.get(match.node).push({ match, idx }); }); matchesByNode.forEach((entries, node) => { if (!node || !node.parentNode) return; const text = node.textContent || ""; let cursor = 0; const fragment = document.createDocumentFragment(); entries.sort((a, b) => a.match.index - b.match.index); entries.forEach(({ match, idx }) => { if (match.index > cursor) { fragment.appendChild(document.createTextNode(text.substring(cursor, match.index))); } const matchText = text.substring(match.index, match.index + match.length); const mark = document.createElement("mark"); mark.className = idx === this.currentIndex ? "find-highlight find-highlight-active" : "find-highlight"; mark.textContent = matchText; mark.setAttribute("data-find-index", idx); fragment.appendChild(mark); match.element = mark; cursor = match.index + match.length; }); if (cursor < text.length) { fragment.appendChild(document.createTextNode(text.substring(cursor))); } node.parentNode.replaceChild(fragment, node); }); }, clearHighlights() { const contentArea = document.querySelector(".md-content"); if (!contentArea) return; const marks = contentArea.querySelectorAll("mark.find-highlight"); marks.forEach((mark) => { if (!mark.parentNode) return; const text = mark.textContent; const textNode = document.createTextNode(text); mark.parentNode.replaceChild(textNode, mark); }); // Normalize text nodes to merge adjacent text nodes contentArea.normalize(); }, goToNext() { if (this.matches.length === 0) return; // Remove active class from current if (this.currentIndex >= 0 && this.matches[this.currentIndex].element) { this.matches[this.currentIndex].element.classList.remove("find-highlight-active"); } // Move to next (with wrapping) this.currentIndex = (this.currentIndex + 1) % this.matches.length; // Add active class to new current if (this.matches[this.currentIndex].element) { this.matches[this.currentIndex].element.classList.add("find-highlight-active"); } this.scrollToMatch(this.currentIndex); this.updateCounter(); }, goToPrevious() { if (this.matches.length === 0) return; // Remove active class from current if (this.currentIndex >= 0 && this.matches[this.currentIndex].element) { this.matches[this.currentIndex].element.classList.remove("find-highlight-active"); } // Move to previous (with wrapping) this.currentIndex = this.currentIndex <= 0 ? this.matches.length - 1 : this.currentIndex - 1; // Add active class to new current if (this.matches[this.currentIndex].element) { this.matches[this.currentIndex].element.classList.add("find-highlight-active"); } this.scrollToMatch(this.currentIndex); this.updateCounter(); }, scrollToMatch(index) { if (index < 0 || index >= this.matches.length) return; const match = this.matches[index]; if (!match.element) return; const contentArea = document.getElementById("content-area"); if (!contentArea) { match.element.scrollIntoView({ behavior: "smooth", block: "center" }); return; } // Calculate position with offset for header const elementTop = match.element.offsetTop; const offset = 100; // Offset for header contentArea.scrollTo({ top: elementTop - offset, behavior: "smooth", }); }, updateCounter() { const counter = document.getElementById("find-counter"); if (!counter) return; const count = this.matches.length; if (count === 0) { counter.textContent = "0 occurrence"; } else if (count === 1) { counter.textContent = "1 occurrence"; } else { counter.textContent = `${count} occurrences`; } }, updateNavButtons() { const prevBtn = document.getElementById("find-prev"); const nextBtn = document.getElementById("find-next"); if (!prevBtn || !nextBtn) return; const hasMatches = this.matches.length > 0; prevBtn.disabled = !hasMatches; nextBtn.disabled = !hasMatches; }, showError(message) { const errorEl = document.getElementById("find-error"); if (!errorEl) return; errorEl.textContent = message; errorEl.hidden = false; }, hideError() { const errorEl = document.getElementById("find-error"); if (!errorEl) return; errorEl.hidden = true; }, saveState() { try { const state = { options: this.options, }; localStorage.setItem("obsigate-find-in-page-state", JSON.stringify(state)); } catch (e) { // Ignore localStorage errors } }, loadState() { try { const saved = localStorage.getItem("obsigate-find-in-page-state"); if (saved) { const state = JSON.parse(saved); if (state.options) { this.options = { ...this.options, ...state.options }; // Update button states const caseSensitiveBtn = document.getElementById("find-case-sensitive"); const wholeWordBtn = document.getElementById("find-whole-word"); const regexBtn = document.getElementById("find-regex"); if (caseSensitiveBtn) caseSensitiveBtn.setAttribute("aria-pressed", this.options.caseSensitive); if (wholeWordBtn) wholeWordBtn.setAttribute("aria-pressed", this.options.wholeWord); if (regexBtn) regexBtn.setAttribute("aria-pressed", this.options.useRegex); } } } catch (e) { // Ignore localStorage errors } }, }; // --------------------------------------------------------------------------- // Tab Manager // --------------------------------------------------------------------------- export const TabManager = { _tabs: [], _activeTabId: null, _previewTabId: null, // single-click preview tab (temporary, replaced on next preview) _tabCache: {}, // { tabId: { vault, path, title, data, rawSource, sourceView, scrollTop, icon } } _tabBar: null, _tabList: null, _dirtyTabs: new Set(), init() { this._tabBar = document.getElementById("tab-bar"); this._tabList = document.getElementById("tab-list"); }, /** Open a file as a preview tab (single-click). * Replaces any existing preview tab. If the file is already * open as a persistent tab, just activates it. */ async openPreview(vault, path) { const tabId = `${vault}::${path}`; // If already open as persistent tab, just activate it const existing = this._tabs.find(t => t.id === tabId && !t.preview); if (existing) { this.activate(tabId); return; } // Close existing preview tab if (this._previewTabId && this._previewTabId !== tabId) { this.close(this._previewTabId); } // If already open as preview, just focus it const previewExisting = this._tabs.find(t => t.id === tabId && t.preview); if (previewExisting) { this.activate(tabId); return; } // Create preview tab const name = path.split("/").pop().replace(/\.md$/i, ""); const icon = getFileIcon(name + ".md"); this._tabs.push({ id: tabId, vault, path, name, icon, preview: true }); this._tabCache[tabId] = { vault, path, title: name, data: null, rawSource: null, sourceView: false, scrollTop: 0, icon }; this._previewTabId = tabId; this._renderTabs(); this.activate(tabId); }, /** Convert a preview tab to a persistent tab (double-click). * If already persistent, opens a new duplicate (same file, different tab). */ async openPersistent(vault, path) { const tabId = `${vault}::${path}`; // If it's already a preview tab, convert it to persistent const previewTab = this._tabs.find(t => t.id === tabId && t.preview); if (previewTab) { previewTab.preview = false; if (this._previewTabId === tabId) { this._previewTabId = null; } this._renderTabs(); this.activate(tabId); return; } // If already persistent, just focus it const existing = this._tabs.find(t => t.id === tabId && !t.preview); if (existing) { this.activate(tabId); return; } // Create a new persistent tab this.open(vault, path); }, /** Open a file in a tab (or focus existing) */ async open(vault, path, options = {}) { const tabId = `${vault}::${path}`; // If already open, just focus it const existing = this._tabs.find(t => t.id === tabId); if (existing) { // Convert preview to persistent if needed if (existing.preview) { existing.preview = false; if (this._previewTabId === tabId) this._previewTabId = null; this._renderTabs(); } this.activate(tabId); return; } // Create new tab const name = path.split("/").pop().replace(/\.md$/i, ""); const icon = getFileIcon(name + ".md"); this._tabs.push({ id: tabId, vault, path, name, icon }); this._tabCache[tabId] = { vault, path, title: name, data: null, rawSource: null, sourceView: false, scrollTop: 0, icon }; this._renderTabs(); this.activate(tabId); }, /** Activate a specific tab */ async activate(tabId) { if (this._activeTabId === tabId && this._tabs.length > 0) return; // Save current tab state if (this._activeTabId && this._tabCache[this._activeTabId]) { this._saveCurrentTabState(); } this._activeTabId = tabId; this._renderTabs(); // Load tab content const cache = this._tabCache[tabId]; if (!cache) return; // Update global state state.currentVault = cache.vault; state.currentPath = cache.path; syncActiveFileTreeItem(cache.vault, cache.path); const area = document.getElementById("content-area"); if (cache.data) { // Use cached data this._restoreTabContent(cache, area); } else { // Fetch file content area.innerHTML = '
Chargement...
'; try { const data = await api(`/api/file/${encodeURIComponent(cache.vault)}?path=${encodeURIComponent(cache.path)}`); cache.data = data; cache.title = data.title; renderFile(cache.data); // Restore source view if needed if (cache.sourceView) { await this._toggleSourceView(cache, area); } if (cache.scrollTop) { area.scrollTop = cache.scrollTop; } } catch (err) { area.innerHTML = `
Erreur: ${escapeHtml(err.message)}
`; } } // Update URL hash if (history.pushState) { history.pushState(null, "", `#/file/${encodeURIComponent(cache.vault)}/${encodeURIComponent(cache.path)}`); } // Hide dashboard const dashboard = document.getElementById("dashboard-home"); if (dashboard) dashboard.style.display = "none"; }, /** Close a tab */ close(tabId) { const idx = this._tabs.findIndex(t => t.id === tabId); if (idx === -1) return; this._tabs.splice(idx, 1); delete this._tabCache[tabId]; this._dirtyTabs.delete(tabId); if (this._tabs.length === 0) { this._activeTabId = null; this._showDashboard(); this._tabBar.hidden = true; } else if (this._activeTabId === tabId) { // Activate adjacent tab const newIdx = Math.min(idx, this._tabs.length - 1); this.activate(this._tabs[newIdx].id); } this._renderTabs(); }, /** Close all tabs */ closeAll() { this._tabs = []; this._tabCache = {}; this._dirtyTabs.clear(); this._activeTabId = null; this._showDashboard(); this._tabBar.hidden = true; }, /** Close tabs to the right */ closeRight(tabId) { const idx = this._tabs.findIndex(t => t.id === tabId); if (idx === -1) return; const toClose = this._tabs.slice(idx + 1); for (const tab of toClose) { delete this._tabCache[tab.id]; this._dirtyTabs.delete(tab.id); } this._tabs = this._tabs.slice(0, idx + 1); if (!this._tabs.find(t => t.id === this._activeTabId)) { this.activate(tabId); } this._renderTabs(); }, /** Close other tabs */ closeOthers(tabId) { const tab = this._tabs.find(t => t.id === tabId); if (!tab) return; for (const t of this._tabs) { if (t.id !== tabId) { delete this._tabCache[t.id]; this._dirtyTabs.delete(t.id); } } this._tabs = [tab]; this.activate(tabId); this._renderTabs(); }, /** Reorder tabs by drag and drop */ moveTab(fromIdx, toIdx) { if (fromIdx === toIdx || fromIdx < 0 || toIdx < 0) return; const tab = this._tabs.splice(fromIdx, 1)[0]; this._tabs.splice(toIdx, 0, tab); this._renderTabs(); }, /** Save current tab state before switching */ _saveCurrentTabState() { const cache = this._tabCache[this._activeTabId]; if (!cache) return; const area = document.getElementById("content-area"); const rendered = document.getElementById("file-rendered-content"); cache.scrollTop = area.scrollTop; cache.sourceView = rendered ? rendered.style.display === "none" : false; }, /** Restore tab content from cache */ _restoreTabContent(cache, area) { renderFile(cache.data); if (cache.sourceView) { this._restoreSourceView(cache, area); } if (cache.scrollTop) { area.scrollTop = cache.scrollTop; } }, async _toggleSourceView(cache, area) { const rendered = document.getElementById("file-rendered-content"); const raw = document.getElementById("file-raw-content"); if (!rendered || !raw) return; if (!cache.rawSource) { const rawData = await api(`/api/file/${encodeURIComponent(cache.vault)}/raw?path=${encodeURIComponent(cache.path)}`); cache.rawSource = rawData.raw; } raw.textContent = cache.rawSource; rendered.style.display = "none"; raw.style.display = "block"; }, _restoreSourceView(cache, area) { requestAnimationFrame(() => { const rendered = document.getElementById("file-rendered-content"); const raw = document.getElementById("file-raw-content"); if (rendered && raw && cache.rawSource) { raw.textContent = cache.rawSource; rendered.style.display = "none"; raw.style.display = "block"; } }); }, _showDashboard() { const area = document.getElementById("content-area"); // Save dashboard DOM before clearing (it may have been removed from DOM by renderFile) let dashboard = document.getElementById("dashboard-home"); if (!dashboard) { // Dashboard was destroyed — rebuild via showWelcome area.innerHTML = ""; showWelcome(); return; } area.innerHTML = ""; dashboard.style.display = ""; area.appendChild(dashboard); // Refresh widgets after restoring if (typeof DashboardStatsWidget !== "undefined") DashboardStatsWidget.load(); if (typeof DashboardConflictsWidget !== "undefined") DashboardConflictsWidget.load(); if (typeof DashboardRecentWidget !== "undefined") DashboardRecentWidget.load(state.selectedContextVault); if (typeof DashboardBookmarkWidget !== "undefined") DashboardBookmarkWidget.load(state.selectedContextVault); if (history.pushState) { history.pushState(null, "", "#"); } }, /** Render the tab bar */