1741 lines
58 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* ObsiGate — Vanilla JS SPA */
(function () {
"use strict";
// ---------------------------------------------------------------------------
// State
// ---------------------------------------------------------------------------
let currentVault = null;
let currentPath = null;
let searchTimeout = null;
let showingSource = false;
let cachedRawSource = null;
let allVaults = [];
let selectedContextVault = "all";
let selectedTags = [];
let editorView = null;
let editorVault = null;
let editorPath = null;
let fallbackEditorEl = null;
let sidebarFilterCaseSensitive = false;
let searchCaseSensitive = false;
const panelState = {
vault: true,
tag: true,
};
// ---------------------------------------------------------------------------
// File extension → Lucide icon mapping
// ---------------------------------------------------------------------------
const EXT_ICONS = {
".md": "file-text",
".txt": "file-text",
".log": "file-text",
".py": "file-code",
".js": "file-code",
".ts": "file-code",
".jsx": "file-code",
".tsx": "file-code",
".html": "file-code",
".css": "file-code",
".scss": "file-code",
".less": "file-code",
".json": "file-json",
".yaml": "file-cog",
".yml": "file-cog",
".toml": "file-cog",
".xml": "file-code",
".sh": "terminal",
".bash": "terminal",
".zsh": "terminal",
".bat": "terminal",
".cmd": "terminal",
".ps1": "terminal",
".java": "file-code",
".c": "file-code",
".cpp": "file-code",
".h": "file-code",
".hpp": "file-code",
".cs": "file-code",
".go": "file-code",
".rs": "file-code",
".rb": "file-code",
".php": "file-code",
".sql": "database",
".csv": "table",
".ini": "file-cog",
".cfg": "file-cog",
".conf": "file-cog",
".env": "file-cog",
};
function getFileIcon(name) {
const ext = "." + name.split(".").pop().toLowerCase();
return EXT_ICONS[ext] || "file";
}
// ---------------------------------------------------------------------------
// Safe CDN helpers
// ---------------------------------------------------------------------------
function safeCreateIcons() {
if (typeof lucide !== "undefined" && lucide.createIcons) {
try { lucide.createIcons(); } catch (e) { /* CDN not loaded */ }
}
}
function safeHighlight(block) {
if (typeof hljs !== "undefined" && hljs.highlightElement) {
try { hljs.highlightElement(block); } catch (e) { /* CDN not loaded */ }
}
}
// ---------------------------------------------------------------------------
// Theme
// ---------------------------------------------------------------------------
function initTheme() {
const saved = localStorage.getItem("obsigate-theme") || "dark";
applyTheme(saved);
}
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";
}
}
function toggleTheme() {
const current = document.documentElement.getAttribute("data-theme");
applyTheme(current === "dark" ? "light" : "dark");
}
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
// ---------------------------------------------------------------------------
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');
});
});
}
// ---------------------------------------------------------------------------
// API helpers
// ---------------------------------------------------------------------------
async function api(path) {
const res = await fetch(path);
if (!res.ok) throw new Error(`API error: ${res.status}`);
return res.json();
}
// ---------------------------------------------------------------------------
// Mobile sidebar
// ---------------------------------------------------------------------------
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");
}
// ---------------------------------------------------------------------------
// 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) {
selectedContextVault = vaultName;
showingSource = false;
cachedRawSource = null;
syncVaultSelectors();
await refreshSidebarForContext();
await refreshTagsForContext();
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");
if (filter) filter.value = selectedContextVault;
if (quickSelect) quickSelect.value = selectedContextVault;
}
function scrollTreeItemIntoView(element, alignToTop) {
if (!element) return;
const scrollContainer = document.getElementById("vault-panel-content");
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 targetTop = 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 = selectedContextVault === "all"
? allVaults
: allVaults.filter((v) => v.name === selectedContextVault);
vaultsToShow.forEach((v) => {
const vaultItem = el("div", { class: "tree-item vault-item", "data-vault": v.name }, [
icon("chevron-right", 14),
icon("database", 16),
document.createTextNode(` ${v.name} `),
smallBadge(v.file_count),
]);
vaultItem.addEventListener("click", () => toggleVault(vaultItem, v.name));
container.appendChild(vaultItem);
const childContainer = el("div", { class: "tree-children collapsed", id: `vault-children-${v.name}` });
container.appendChild(childContainer);
});
safeCreateIcons();
}
async function focusVaultInSidebar(vaultName) {
setPanelExpanded("vault", true);
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 = selectedContextVault === "all" ? "" : `?vault=${encodeURIComponent(selectedContextVault)}`;
const data = await api(`/api/tags${vaultParam}`);
const filteredTags = TagFilterService.filterTags(data.tags);
renderTagCloud(filteredTags);
}
// ---------------------------------------------------------------------------
// Sidebar — Vault tree
// ---------------------------------------------------------------------------
async function loadVaults() {
const vaults = await api("/api/vaults");
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");
vaults.forEach((v) => {
// Sidebar tree entry
const vaultItem = el("div", { class: "tree-item vault-item", "data-vault": v.name }, [
icon("chevron-right", 14),
icon("database", 16),
document.createTextNode(` ${v.name} `),
smallBadge(v.file_count),
]);
vaultItem.addEventListener("click", () => toggleVault(vaultItem, v.name));
container.appendChild(vaultItem);
const childContainer = el("div", { class: "tree-children collapsed", id: `vault-children-${v.name}` });
container.appendChild(childContainer);
});
syncVaultSelectors();
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 focusPathInSidebar(vaultName, targetPath, options) {
setPanelExpanded("vault", true);
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 = document.getElementById(`dir-${vaultName}-${cumulativePath}`);
if (nextContainer && nextContainer.classList.contains("collapsed")) {
targetItem.click();
await new Promise((resolve) => setTimeout(resolve, 0));
}
if (nextContainer) {
currentContainer = nextContainer;
}
}
}
// 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);
}
async function loadDirectory(vaultName, dirPath, container) {
const url = `/api/browse/${encodeURIComponent(vaultName)}?path=${encodeURIComponent(dirPath)}`;
const data = await api(url);
container.innerHTML = "";
const fragment = document.createDocumentFragment();
data.items.forEach((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),
document.createTextNode(` ${item.name} `),
smallBadge(item.children_count),
]);
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();
}
});
} 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),
document.createTextNode(` ${displayName}`),
]);
fileItem.addEventListener("click", () => {
scrollTreeItemIntoView(fileItem, false);
openFile(vaultName, item.path);
closeMobileSidebar();
});
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 q = sidebarFilterCaseSensitive ? input.value.trim() : input.value.trim().toLowerCase();
filterSidebarTree(q);
filterTagCloud(q);
});
caseBtn.addEventListener("click", () => {
sidebarFilterCaseSensitive = !sidebarFilterCaseSensitive;
caseBtn.classList.toggle("active");
const q = sidebarFilterCaseSensitive ? input.value.trim() : input.value.trim().toLowerCase();
filterSidebarTree(q);
filterTagCloud(q);
});
clearBtn.addEventListener("click", () => {
input.value = "";
sidebarFilterCaseSensitive = false;
caseBtn.classList.remove("active");
filterSidebarTree("");
filterTagCloud("");
});
}
function filterSidebarTree(query) {
const tree = document.getElementById("vault-tree");
const items = tree.querySelectorAll(".tree-item");
if (!query) {
items.forEach((item) => item.classList.remove("filtered-out"));
tree.querySelectorAll(".tree-children").forEach((c) => c.classList.remove("filtered-out"));
return;
}
// First pass: mark all as filtered out
items.forEach((item) => item.classList.add("filtered-out"));
tree.querySelectorAll(".tree-children").forEach((c) => c.classList.add("filtered-out"));
// Second pass: show matching items and their ancestors
items.forEach((item) => {
const text = sidebarFilterCaseSensitive ? item.textContent : item.textContent.toLowerCase();
const searchQuery = sidebarFilterCaseSensitive ? query : query.toLowerCase();
if (text.includes(searchQuery)) {
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;
}
}
});
}
function filterTagCloud(query) {
const tags = document.querySelectorAll("#tag-cloud .tag-item");
tags.forEach((tag) => {
const text = sidebarFilterCaseSensitive ? tag.textContent : tag.textContent.toLowerCase();
const searchQuery = 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) {
let regex = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
regex = regex.replace(/\\\.\\\.\\\./g, '.*');
return regex;
},
isTagFiltered(tag) {
const config = this.getConfig();
const filters = config.tagFilters || this.defaultFilters;
for (const filter of filters) {
if (!filter.enabled) continue;
try {
const regex = new RegExp(`^${filter.regex}$`);
if (regex.test(`#${tag}`)) {
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 (!selectedTags.includes(tag)) {
selectedTags.push(tag);
performTagSearch();
}
}
function removeTagFilter(tag) {
selectedTags = selectedTags.filter(t => t !== tag);
if (selectedTags.length > 0) {
performTagSearch();
} else {
const input = document.getElementById("search-input");
if (input.value.trim()) {
performSearch(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;
performSearch(query, vault, selectedTags.length > 0 ? 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 (selectedTags.length > 0) {
const activeTags = el("div", { class: "search-results-active-tags" });
selectedTags.forEach((tag) => {
const removeBtn = el("button", {
class: "search-results-active-tag-remove",
title: `Retirer ${tag} du filtre`,
"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);
}
// ---------------------------------------------------------------------------
// File viewer
// ---------------------------------------------------------------------------
async function openFile(vaultName, filePath) {
currentVault = vaultName;
currentPath = filePath;
showingSource = false;
cachedRawSource = null;
// Highlight active
document.querySelectorAll(".tree-item.active").forEach((el) => el.classList.remove("active"));
const selector = `.tree-item[data-vault="${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 */ }
const url = `/api/file/${encodeURIComponent(vaultName)}?path=${encodeURIComponent(filePath)}`;
const data = await api(url);
renderFile(data);
}
function renderFile(data) {
const area = document.getElementById("content-area");
// Breadcrumb
const parts = data.path.split("/");
const breadcrumbEls = [];
breadcrumbEls.push(makeBreadcrumbSpan(data.vault, () => {
focusPathInSidebar(data.vault, "", { alignToTop: true });
}));
let accumulated = "";
parts.forEach((part, i) => {
breadcrumbEls.push(el("span", { class: "sep" }, [document.createTextNode(" / ")]));
accumulated += (accumulated ? "/" : "") + part;
const p = accumulated;
if (i < parts.length - 1) {
breadcrumbEls.push(makeBreadcrumbSpan(part, () => {
focusPathInSidebar(data.vault, p, { alignToTop: true });
}));
} else {
breadcrumbEls.push(makeBreadcrumbSpan(part.replace(/\.md$/i, ""), () => {
focusPathInSidebar(data.vault, data.path, { alignToTop: false });
}));
}
});
const breadcrumb = el("div", { class: "breadcrumb" }, breadcrumbEls);
// Tags
const tagsDiv = el("div", { class: "file-tags" });
(data.tags || []).forEach((tag) => {
if (!TagFilterService.isTagFiltered(tag)) {
const t = el("span", { class: "file-tag" }, [document.createTextNode(`#${tag}`)]);
t.addEventListener("click", () => searchByTag(tag));
tagsDiv.appendChild(t);
}
});
// Action buttons
const copyBtn = el("button", { class: "btn-action", title: "Copier le chemin" }, [
icon("copy", 14),
document.createTextNode("Copier"),
]);
copyBtn.addEventListener("click", () => {
navigator.clipboard.writeText(`${data.vault}/${data.path}`).then(() => {
copyBtn.querySelector("span") || (copyBtn.lastChild.textContent = "Copié !");
copyBtn.lastChild.textContent = "Copié !";
setTimeout(() => (copyBtn.lastChild.textContent = "Copier"), 1500);
});
});
const sourceBtn = el("button", { class: "btn-action", title: "Voir la source" }, [
icon("code", 14),
document.createTextNode("Source"),
]);
const downloadBtn = el("button", { class: "btn-action", title: "Télécharger" }, [
icon("download", 14),
document.createTextNode("Télécharger"),
]);
downloadBtn.addEventListener("click", () => {
const dlUrl = `/api/file/${encodeURIComponent(data.vault)}/download?path=${encodeURIComponent(data.path)}`;
const a = document.createElement("a");
a.href = dlUrl;
a.download = data.path.split("/").pop();
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
});
const editBtn = el("button", { class: "btn-action", title: "Éditer" }, [
icon("edit", 14),
document.createTextNode("Éditer"),
]);
editBtn.addEventListener("click", () => {
openEditor(data.vault, data.path);
});
// Frontmatter
let fmSection = null;
if (data.frontmatter && Object.keys(data.frontmatter).length > 0) {
const fmToggle = el("div", { class: "frontmatter-toggle" }, [
document.createTextNode("▶ Frontmatter"),
]);
const fmContent = el("div", { class: "frontmatter-content" }, [
document.createTextNode(JSON.stringify(data.frontmatter, null, 2)),
]);
fmToggle.addEventListener("click", () => {
fmContent.classList.toggle("open");
fmToggle.textContent = fmContent.classList.contains("open") ? "▼ Frontmatter" : "▶ Frontmatter";
});
fmSection = el("div", {}, [fmToggle, fmContent]);
}
// Content container (rendered HTML)
const mdDiv = el("div", { class: "md-content", id: "file-rendered-content" });
mdDiv.innerHTML = data.html;
// Raw source container (hidden initially)
const rawDiv = el("div", { class: "raw-source-view", id: "file-raw-content", style: "display:none" });
// Source button toggle logic
sourceBtn.addEventListener("click", async () => {
const rendered = document.getElementById("file-rendered-content");
const raw = document.getElementById("file-raw-content");
if (!rendered || !raw) return;
showingSource = !showingSource;
if (showingSource) {
sourceBtn.classList.add("active");
if (!cachedRawSource) {
const rawUrl = `/api/file/${encodeURIComponent(data.vault)}/raw?path=${encodeURIComponent(data.path)}`;
const rawData = await api(rawUrl);
cachedRawSource = rawData.raw;
}
raw.textContent = cachedRawSource;
rendered.style.display = "none";
raw.style.display = "block";
} else {
sourceBtn.classList.remove("active");
rendered.style.display = "block";
raw.style.display = "none";
}
});
// Assemble
area.innerHTML = "";
area.appendChild(breadcrumb);
area.appendChild(el("div", { class: "file-header" }, [
el("div", { class: "file-title" }, [document.createTextNode(data.title)]),
tagsDiv,
el("div", { class: "file-actions" }, [copyBtn, sourceBtn, downloadBtn, editBtn]),
]));
if (fmSection) area.appendChild(fmSection);
area.appendChild(mdDiv);
area.appendChild(rawDiv);
// Highlight code blocks
area.querySelectorAll("pre code").forEach((block) => {
safeHighlight(block);
});
// Wire up wikilinks
area.querySelectorAll(".wikilink").forEach((link) => {
link.addEventListener("click", (e) => {
e.preventDefault();
const v = link.getAttribute("data-vault");
const p = link.getAttribute("data-path");
if (v && p) openFile(v, p);
});
});
safeCreateIcons();
area.scrollTop = 0;
}
// ---------------------------------------------------------------------------
// Collapsible panels and help modal
// ---------------------------------------------------------------------------
function initCollapsiblePanels() {
bindPanelToggle("vault", "vault-panel-toggle", "vault-panel-content");
bindPanelToggle("tag", "tag-panel-toggle", "tag-panel-content");
setPanelExpanded("vault", true);
setPanelExpanded("tag", true);
}
function bindPanelToggle(panelKey, toggleId, contentId) {
const toggle = document.getElementById(toggleId);
const content = document.getElementById(contentId);
if (!toggle || !content) return;
toggle.addEventListener("click", () => {
setPanelExpanded(panelKey, !panelState[panelKey]);
});
}
function setPanelExpanded(panelKey, expanded) {
panelState[panelKey] = expanded;
const sidebar = document.getElementById("sidebar");
const toggle = document.getElementById(`${panelKey}-panel-toggle`);
const content = document.getElementById(`${panelKey}-panel-content`);
if (!toggle || !content) return;
toggle.setAttribute("aria-expanded", expanded ? "true" : "false");
content.classList.toggle("collapsed", !expanded);
const iconEl = toggle.querySelector("[data-lucide]");
if (iconEl) {
iconEl.setAttribute("data-lucide", expanded ? "chevron-down" : "chevron-right");
}
if (panelKey === "tag") {
const tagSection = document.getElementById("tag-cloud-section");
const resizeHandle = document.getElementById("tag-resize-handle");
if (tagSection) tagSection.classList.toggle("collapsed-panel", !expanded);
if (resizeHandle) resizeHandle.classList.toggle("hidden", !expanded);
}
if (sidebar) {
sidebar.classList.toggle("vault-collapsed", !panelState.vault);
sidebar.classList.toggle("tag-collapsed", !panelState.tag);
}
safeCreateIcons();
}
function initHelpModal() {
const openBtn = document.getElementById("help-open-btn");
const closeBtn = document.getElementById("help-close");
const modal = document.getElementById("help-modal");
if (!openBtn || !closeBtn || !modal) return;
openBtn.addEventListener("click", () => {
modal.classList.add("active");
closeHeaderMenu();
safeCreateIcons();
});
closeBtn.addEventListener("click", closeHelpModal);
modal.addEventListener("click", (e) => {
if (e.target === modal) {
closeHelpModal();
}
});
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && modal.classList.contains("active")) {
closeHelpModal();
}
});
}
function closeHelpModal() {
const modal = document.getElementById("help-modal");
if (modal) modal.classList.remove("active");
}
function initConfigModal() {
const openBtn = document.getElementById("config-open-btn");
const closeBtn = document.getElementById("config-close");
const modal = document.getElementById("config-modal");
const addBtn = document.getElementById("config-add-btn");
const patternInput = document.getElementById("config-pattern-input");
if (!openBtn || !closeBtn || !modal) return;
openBtn.addEventListener("click", () => {
modal.classList.add("active");
closeHeaderMenu();
renderConfigFilters();
safeCreateIcons();
});
closeBtn.addEventListener("click", closeConfigModal);
modal.addEventListener("click", (e) => {
if (e.target === modal) {
closeConfigModal();
}
});
addBtn.addEventListener("click", addConfigFilter);
patternInput.addEventListener("keypress", (e) => {
if (e.key === "Enter") {
addConfigFilter();
}
});
patternInput.addEventListener("input", updateRegexPreview);
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && modal.classList.contains("active")) {
closeConfigModal();
}
});
}
function closeConfigModal() {
const modal = document.getElementById("config-modal");
if (modal) modal.classList.remove("active");
}
function renderConfigFilters() {
const config = TagFilterService.getConfig();
const filters = config.tagFilters || TagFilterService.defaultFilters;
const container = document.getElementById("config-filters-list");
container.innerHTML = "";
filters.forEach((filter, index) => {
const badge = el("div", { class: `config-filter-badge ${!filter.enabled ? "disabled" : ""}` }, [
el("span", {}, [document.createTextNode(filter.pattern)]),
el("button", {
class: "config-filter-toggle",
title: filter.enabled ? "Désactiver" : "Activer",
type: "button"
}, [document.createTextNode(filter.enabled ? "✓" : "○")]),
el("button", {
class: "config-filter-remove",
title: "Supprimer",
type: "button"
}, [document.createTextNode("×")]),
]);
const toggleBtn = badge.querySelector(".config-filter-toggle");
const removeBtn = badge.querySelector(".config-filter-remove");
toggleBtn.addEventListener("click", (e) => {
e.stopPropagation();
toggleConfigFilter(index);
});
removeBtn.addEventListener("click", (e) => {
e.stopPropagation();
removeConfigFilter(index);
});
container.appendChild(badge);
});
}
function toggleConfigFilter(index) {
const config = TagFilterService.getConfig();
const filters = config.tagFilters || TagFilterService.defaultFilters;
if (filters[index]) {
filters[index].enabled = !filters[index].enabled;
config.tagFilters = filters;
TagFilterService.saveConfig(config);
renderConfigFilters();
refreshTagsForContext().catch(err => console.error("Error refreshing tags:", err));
}
}
function removeConfigFilter(index) {
const config = TagFilterService.getConfig();
let filters = config.tagFilters || TagFilterService.defaultFilters;
filters = filters.filter((_, i) => i !== index);
config.tagFilters = filters;
TagFilterService.saveConfig(config);
renderConfigFilters();
refreshTagsForContext().catch(err => console.error("Error refreshing tags:", err));
}
function addConfigFilter() {
const input = document.getElementById("config-pattern-input");
const pattern = input.value.trim();
if (!pattern) return;
const regex = TagFilterService.patternToRegex(pattern);
const config = TagFilterService.getConfig();
const filters = config.tagFilters || TagFilterService.defaultFilters;
const newFilter = { pattern, regex, enabled: true };
filters.push(newFilter);
config.tagFilters = filters;
TagFilterService.saveConfig(config);
input.value = "";
renderConfigFilters();
refreshTagsForContext().catch(err => console.error("Error refreshing tags:", err));
updateRegexPreview();
}
function updateRegexPreview() {
const input = document.getElementById("config-pattern-input");
const preview = document.getElementById("config-regex-preview");
const code = document.getElementById("config-regex-code");
const pattern = input.value.trim();
if (pattern) {
const regex = TagFilterService.patternToRegex(pattern);
code.textContent = `^${regex}$`;
preview.style.display = "block";
} else {
preview.style.display = "none";
}
}
// ---------------------------------------------------------------------------
// Search
// ---------------------------------------------------------------------------
function initSearch() {
const input = document.getElementById("search-input");
const caseBtn = document.getElementById("search-case-btn");
const clearBtn = document.getElementById("search-clear-btn");
input.addEventListener("input", () => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
const q = input.value.trim();
const vault = document.getElementById("vault-filter").value;
const tagFilter = selectedTags.length > 0 ? selectedTags.join(",") : null;
if (q.length > 0 || tagFilter) {
performSearch(q, vault, tagFilter);
} else {
showWelcome();
}
}, 300);
});
caseBtn.addEventListener("click", () => {
searchCaseSensitive = !searchCaseSensitive;
caseBtn.classList.toggle("active");
});
clearBtn.addEventListener("click", () => {
input.value = "";
searchCaseSensitive = false;
caseBtn.classList.remove("active");
showWelcome();
});
}
async function performSearch(query, vaultFilter, tagFilter) {
showLoading();
let url = `/api/search?q=${encodeURIComponent(query)}&vault=${encodeURIComponent(vaultFilter)}`;
if (tagFilter) url += `&tag=${encodeURIComponent(tagFilter)}`;
const data = await api(url);
renderSearchResults(data, query, tagFilter);
}
function renderSearchResults(data, query, tagFilter) {
const area = document.getElementById("content-area");
area.innerHTML = "";
const header = buildSearchResultsHeader(data, query, tagFilter);
area.appendChild(header);
if (data.results.length === 0) {
area.appendChild(el("p", { style: "color:var(--text-muted);margin-top:20px" }, [
document.createTextNode("Aucun résultat trouvé."),
]));
return;
}
const container = el("div", { class: "search-results" });
data.results.forEach((r) => {
const item = el("div", { class: "search-result-item" }, [
el("div", { class: "search-result-title" }, [document.createTextNode(r.title)]),
el("div", { class: "search-result-vault" }, [document.createTextNode(r.vault + " / " + r.path)]),
el("div", { class: "search-result-snippet" }, [document.createTextNode(r.snippet || "")]),
]);
if (r.tags && r.tags.length > 0) {
const tagsDiv = el("div", { class: "search-result-tags" });
r.tags.forEach((tag) => {
if (!TagFilterService.isTagFiltered(tag)) {
const tagEl = el("span", { class: "file-tag" }, [document.createTextNode(`#${tag}`)]);
tagEl.addEventListener("click", (e) => {
e.stopPropagation();
addTagFilter(tag);
});
tagsDiv.appendChild(tagEl);
}
});
if (tagsDiv.children.length > 0) {
item.appendChild(tagsDiv);
}
}
item.addEventListener("click", () => openFile(r.vault, r.path));
container.appendChild(item);
});
area.appendChild(container);
}
// ---------------------------------------------------------------------------
// Resizable sidebar (horizontal)
// ---------------------------------------------------------------------------
function initSidebarResize() {
const handle = document.getElementById("sidebar-resize-handle");
const sidebar = document.getElementById("sidebar");
if (!handle || !sidebar) return;
// Restore saved width
const savedWidth = localStorage.getItem("obsigate-sidebar-width");
if (savedWidth) {
sidebar.style.width = savedWidth + "px";
}
let startX = 0;
let startWidth = 0;
function onMouseMove(e) {
const newWidth = Math.min(500, Math.max(200, startWidth + (e.clientX - startX)));
sidebar.style.width = newWidth + "px";
}
function onMouseUp() {
document.body.classList.remove("resizing");
handle.classList.remove("active");
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
localStorage.setItem("obsigate-sidebar-width", parseInt(sidebar.style.width));
}
handle.addEventListener("mousedown", (e) => {
e.preventDefault();
startX = e.clientX;
startWidth = sidebar.getBoundingClientRect().width;
document.body.classList.add("resizing");
handle.classList.add("active");
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
});
}
// ---------------------------------------------------------------------------
// Resizable tag section (vertical)
// ---------------------------------------------------------------------------
function initTagResize() {
const handle = document.getElementById("tag-resize-handle");
const tagSection = document.getElementById("tag-cloud-section");
if (!handle || !tagSection) return;
// Restore saved height
const savedHeight = localStorage.getItem("obsigate-tag-height");
if (savedHeight) {
tagSection.style.height = savedHeight + "px";
}
let startY = 0;
let startHeight = 0;
function onMouseMove(e) {
// Dragging up increases height, dragging down decreases
const newHeight = Math.min(400, Math.max(60, startHeight - (e.clientY - startY)));
tagSection.style.height = newHeight + "px";
}
function onMouseUp() {
document.body.classList.remove("resizing-v");
handle.classList.remove("active");
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
localStorage.setItem("obsigate-tag-height", parseInt(tagSection.style.height));
}
handle.addEventListener("mousedown", (e) => {
e.preventDefault();
startY = e.clientY;
startHeight = tagSection.getBoundingClientRect().height;
document.body.classList.add("resizing-v");
handle.classList.add("active");
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
});
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function el(tag, attrs, children) {
const e = document.createElement(tag);
if (attrs) {
Object.entries(attrs).forEach(([k, v]) => e.setAttribute(k, v));
}
if (children) {
children.forEach((c) => { if (c) e.appendChild(c); });
}
return e;
}
function icon(name, size) {
const i = document.createElement("i");
i.setAttribute("data-lucide", name);
i.style.width = size + "px";
i.style.height = size + "px";
i.classList.add("icon");
return i;
}
function smallBadge(count) {
const s = document.createElement("span");
s.style.cssText = "font-size:0.68rem;color:var(--text-muted);margin-left:4px";
s.textContent = `(${count})`;
return s;
}
function makeBreadcrumbSpan(text, onClick) {
const s = document.createElement("span");
s.textContent = text;
if (onClick) s.addEventListener("click", onClick);
return s;
}
function showWelcome() {
const area = document.getElementById("content-area");
area.innerHTML = `
<div class="welcome">
<i data-lucide="library" style="width:48px;height:48px;color:var(--text-muted)"></i>
<h2>ObsiGate</h2>
<p>Sélectionnez un fichier dans la sidebar ou utilisez la recherche pour commencer.</p>
</div>`;
safeCreateIcons();
}
function showLoading() {
const area = document.getElementById("content-area");
area.innerHTML = `
<div class="loading-indicator">
<div class="loading-spinner"></div>
<div>Recherche en cours...</div>
</div>`;
}
function goHome() {
const searchInput = document.getElementById("search-input");
if (searchInput) searchInput.value = "";
document.querySelectorAll(".tree-item.active").forEach((el) => el.classList.remove("active"));
currentVault = null;
currentPath = null;
showingSource = false;
cachedRawSource = null;
closeMobileSidebar();
showWelcome();
}
// ---------------------------------------------------------------------------
// Editor (CodeMirror)
// ---------------------------------------------------------------------------
async function openEditor(vaultName, filePath) {
editorVault = vaultName;
editorPath = filePath;
const modal = document.getElementById("editor-modal");
const titleEl = document.getElementById("editor-title");
const bodyEl = document.getElementById("editor-body");
titleEl.textContent = `Édition: ${filePath.split("/").pop()}`;
// Fetch raw content
const rawUrl = `/api/file/${encodeURIComponent(vaultName)}/raw?path=${encodeURIComponent(filePath)}`;
const rawData = await api(rawUrl);
// Clear previous editor
bodyEl.innerHTML = "";
if (editorView) {
editorView.destroy();
editorView = null;
}
fallbackEditorEl = null;
try {
await waitForCodeMirror();
const { EditorView, EditorState, basicSetup, markdown, oneDark, keymap } = window.CodeMirror;
const currentTheme = document.documentElement.getAttribute("data-theme");
const extensions = [
basicSetup,
markdown(),
keymap.of([{
key: "Mod-s",
run: () => {
saveFile();
return true;
}
}]),
EditorView.lineWrapping,
];
if (currentTheme === "dark") {
extensions.push(oneDark);
}
const state = EditorState.create({
doc: rawData.raw,
extensions: extensions,
});
editorView = new EditorView({
state: state,
parent: bodyEl,
});
} catch (err) {
console.error("CodeMirror init failed, falling back to textarea:", err);
fallbackEditorEl = document.createElement("textarea");
fallbackEditorEl.className = "fallback-editor";
fallbackEditorEl.value = rawData.raw;
bodyEl.appendChild(fallbackEditorEl);
}
modal.classList.add("active");
safeCreateIcons();
}
async function waitForCodeMirror() {
let attempts = 0;
while (!window.CodeMirror && attempts < 50) {
await new Promise(resolve => setTimeout(resolve, 100));
attempts++;
}
if (!window.CodeMirror) {
throw new Error("CodeMirror failed to load");
}
}
function closeEditor() {
const modal = document.getElementById("editor-modal");
modal.classList.remove("active");
if (editorView) {
editorView.destroy();
editorView = null;
}
fallbackEditorEl = null;
editorVault = null;
editorPath = null;
}
async function saveFile() {
if ((!editorView && !fallbackEditorEl) || !editorVault || !editorPath) return;
const content = editorView ? editorView.state.doc.toString() : fallbackEditorEl.value;
const saveBtn = document.getElementById("editor-save");
const originalHTML = saveBtn.innerHTML;
try {
saveBtn.disabled = true;
saveBtn.innerHTML = '<i data-lucide="loader" style="width:16px;height:16px"></i>';
safeCreateIcons();
const response = await fetch(
`/api/file/${encodeURIComponent(editorVault)}/save?path=${encodeURIComponent(editorPath)}`,
{
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content }),
}
);
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || "Erreur de sauvegarde");
}
saveBtn.innerHTML = '<i data-lucide="check" style="width:16px;height:16px"></i>';
safeCreateIcons();
setTimeout(() => {
closeEditor();
if (currentVault === editorVault && currentPath === editorPath) {
openFile(currentVault, currentPath);
}
}, 800);
} catch (err) {
console.error("Save error:", err);
alert(`Erreur: ${err.message}`);
saveBtn.innerHTML = originalHTML;
saveBtn.disabled = false;
safeCreateIcons();
}
}
async function deleteFile() {
if (!editorVault || !editorPath) return;
const deleteBtn = document.getElementById("editor-delete");
const originalHTML = deleteBtn.innerHTML;
try {
deleteBtn.disabled = true;
deleteBtn.innerHTML = '<i data-lucide="loader" style="width:16px;height:16px"></i>';
safeCreateIcons();
const response = await fetch(
`/api/file/${encodeURIComponent(editorVault)}?path=${encodeURIComponent(editorPath)}`,
{ method: "DELETE" }
);
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || "Erreur de suppression");
}
closeEditor();
showWelcome();
await refreshSidebarForContext();
await refreshTagsForContext();
} catch (err) {
console.error("Delete error:", err);
alert(`Erreur: ${err.message}`);
deleteBtn.innerHTML = originalHTML;
deleteBtn.disabled = false;
safeCreateIcons();
}
}
function initEditor() {
const cancelBtn = document.getElementById("editor-cancel");
const deleteBtn = document.getElementById("editor-delete");
const saveBtn = document.getElementById("editor-save");
const modal = document.getElementById("editor-modal");
cancelBtn.addEventListener("click", closeEditor);
deleteBtn.addEventListener("click", deleteFile);
saveBtn.addEventListener("click", saveFile);
// Close on overlay click
modal.addEventListener("click", (e) => {
if (e.target === modal) {
closeEditor();
}
});
// ESC to close
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && modal.classList.contains("active")) {
closeEditor();
}
});
// Fix mouse wheel scrolling in editor
modal.addEventListener("wheel", (e) => {
const editorBody = document.getElementById("editor-body");
if (editorBody && editorBody.contains(e.target)) {
// Let the editor handle the scroll
return;
}
// Prevent modal from scrolling if not in editor area
e.preventDefault();
}, { passive: false });
}
// ---------------------------------------------------------------------------
// Init
// ---------------------------------------------------------------------------
async function init() {
initTheme();
initHeaderMenu();
initCustomDropdowns();
document.getElementById("theme-toggle").addEventListener("click", toggleTheme);
document.getElementById("header-logo").addEventListener("click", goHome);
initSearch();
initMobile();
initVaultContext();
initCollapsiblePanels();
initHelpModal();
initConfigModal();
initSidebarFilter();
initSidebarResize();
initTagResize();
initEditor();
try {
await Promise.all([loadVaults(), loadTags()]);
} catch (err) {
console.error("Failed to initialize ObsiGate:", err);
}
safeCreateIcons();
}
document.addEventListener("DOMContentLoaded", init);
})();