fix: regenerate corrupted ui.js, search.js, viewer.js from app.js
This commit is contained in:
+484
-325
@@ -1,13 +1,10 @@
|
||||
/* ObsiGate — UI: theme, sidebar, context menus, tabs, toast, find-in-page */
|
||||
/* ObsiGate — Auto-extracted module */
|
||||
import { state } from './state.js';
|
||||
import { openFile } from './viewer.js';
|
||||
import { safeCreateIcons } from './utils.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Right Sidebar Manager
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const RightSidebarManager = {
|
||||
const RightSidebarManager = {
|
||||
init() {
|
||||
this.loadState();
|
||||
this.initToggle();
|
||||
@@ -19,11 +16,11 @@ export const RightSidebarManager = {
|
||||
const savedWidth = localStorage.getItem("obsigate-right-sidebar-width");
|
||||
|
||||
if (savedVisible !== null) {
|
||||
state.rightSidebarVisible = savedVisible === "true";
|
||||
rightSidebarVisible = savedVisible === "true";
|
||||
}
|
||||
|
||||
if (savedWidth) {
|
||||
state.rightSidebarWidth = parseInt(savedWidth) || 280;
|
||||
rightSidebarWidth = parseInt(savedWidth) || 280;
|
||||
}
|
||||
|
||||
this.applyState();
|
||||
@@ -37,9 +34,9 @@ export const RightSidebarManager = {
|
||||
|
||||
if (!sidebar) return;
|
||||
|
||||
if (state.rightSidebarVisible) {
|
||||
if (rightSidebarVisible) {
|
||||
sidebar.classList.remove("hidden");
|
||||
sidebar.style.width = `${state.rightSidebarWidth}px`;
|
||||
sidebar.style.width = `${rightSidebarWidth}px`;
|
||||
if (handle) handle.classList.remove("hidden");
|
||||
if (tocBtn) {
|
||||
tocBtn.classList.add("active");
|
||||
@@ -67,8 +64,8 @@ export const RightSidebarManager = {
|
||||
},
|
||||
|
||||
toggle() {
|
||||
state.rightSidebarVisible = !state.rightSidebarVisible;
|
||||
localStorage.setItem("obsigate-right-sidebar-visible", state.rightSidebarVisible);
|
||||
rightSidebarVisible = !rightSidebarVisible;
|
||||
localStorage.setItem("obsigate-right-sidebar-visible", rightSidebarVisible);
|
||||
this.applyState();
|
||||
},
|
||||
|
||||
@@ -108,7 +105,7 @@ export const RightSidebarManager = {
|
||||
newWidth = Math.max(200, Math.min(400, newWidth));
|
||||
|
||||
sidebar.style.width = `${newWidth}px`;
|
||||
state.rightSidebarWidth = newWidth;
|
||||
rightSidebarWidth = newWidth;
|
||||
};
|
||||
|
||||
const onMouseUp = () => {
|
||||
@@ -119,7 +116,7 @@ export const RightSidebarManager = {
|
||||
document.body.style.cursor = "";
|
||||
document.body.style.userSelect = "";
|
||||
|
||||
localStorage.setItem("obsigate-right-sidebar-width", state.rightSidebarWidth);
|
||||
localStorage.setItem("obsigate-right-sidebar-width", rightSidebarWidth);
|
||||
};
|
||||
|
||||
handle.addEventListener("mousedown", onMouseDown);
|
||||
@@ -131,12 +128,12 @@ export const RightSidebarManager = {
|
||||
// ---------------------------------------------------------------------------
|
||||
// Theme
|
||||
// ---------------------------------------------------------------------------
|
||||
export function initTheme() {
|
||||
function initTheme() {
|
||||
const saved = localStorage.getItem("obsigate-theme") || "dark";
|
||||
applyTheme(saved);
|
||||
}
|
||||
|
||||
export function applyTheme(theme) {
|
||||
function applyTheme(theme) {
|
||||
document.documentElement.setAttribute("data-theme", theme);
|
||||
localStorage.setItem("obsigate-theme", theme);
|
||||
|
||||
@@ -161,12 +158,12 @@ export function applyTheme(theme) {
|
||||
}
|
||||
}
|
||||
|
||||
export function toggleTheme() {
|
||||
function toggleTheme() {
|
||||
const current = document.documentElement.getAttribute("data-theme");
|
||||
applyTheme(current === "dark" ? "light" : "dark");
|
||||
}
|
||||
|
||||
export function initHeaderMenu() {
|
||||
function initHeaderMenu() {
|
||||
const menuBtn = document.getElementById("header-menu-btn");
|
||||
const menuDropdown = document.getElementById("header-menu-dropdown");
|
||||
|
||||
@@ -203,7 +200,7 @@ function closeHeaderMenu() {
|
||||
// ---------------------------------------------------------------------------
|
||||
// Custom Dropdowns
|
||||
// ---------------------------------------------------------------------------
|
||||
export function initCustomDropdowns() {
|
||||
function initCustomDropdowns() {
|
||||
document.querySelectorAll(".custom-dropdown").forEach((dropdown) => {
|
||||
const trigger = dropdown.querySelector(".custom-dropdown-trigger");
|
||||
const options = dropdown.querySelectorAll(".custom-dropdown-option");
|
||||
@@ -335,7 +332,7 @@ function populateCustomDropdown(dropdownId, optionsList, defaultValue) {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Display a brief toast message at the bottom of the viewport. */
|
||||
export function showToast(message, type) {
|
||||
function showToast(message, type) {
|
||||
console.log("showToast called with:", message, type);
|
||||
type = type || "info";
|
||||
let container = document.getElementById("toast-container");
|
||||
@@ -365,7 +362,7 @@ export function showToast(message, type) {
|
||||
// ---------------------------------------------------------------------------
|
||||
// Sidebar toggle (desktop)
|
||||
// ---------------------------------------------------------------------------
|
||||
export function initSidebarToggle() {
|
||||
function initSidebarToggle() {
|
||||
const toggleBtn = document.getElementById("sidebar-toggle-btn");
|
||||
const sidebar = document.getElementById("sidebar");
|
||||
const resizeHandle = document.getElementById("sidebar-resize-handle");
|
||||
@@ -391,7 +388,7 @@ export function initSidebarToggle() {
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mobile sidebar
|
||||
// ---------------------------------------------------------------------------
|
||||
export function initMobile() {
|
||||
function initMobile() {
|
||||
const hamburger = document.getElementById("hamburger-btn");
|
||||
const overlay = document.getElementById("sidebar-overlay");
|
||||
const sidebar = document.getElementById("sidebar");
|
||||
@@ -417,7 +414,7 @@ function closeMobileSidebar() {
|
||||
// ---------------------------------------------------------------------------
|
||||
// Resizable sidebar (horizontal)
|
||||
// ---------------------------------------------------------------------------
|
||||
export function initSidebarResize() {
|
||||
function initSidebarResize() {
|
||||
const handle = document.getElementById("sidebar-resize-handle");
|
||||
const sidebar = document.getElementById("sidebar");
|
||||
if (!handle || !sidebar) return;
|
||||
@@ -458,7 +455,7 @@ export function initSidebarResize() {
|
||||
// ---------------------------------------------------------------------------
|
||||
// Resizable tag section (vertical)
|
||||
// ---------------------------------------------------------------------------
|
||||
export function initTagResize() {
|
||||
function initTagResize() {
|
||||
const handle = document.getElementById("tag-resize-handle");
|
||||
const tagSection = document.getElementById("tag-cloud-section");
|
||||
if (!handle || !tagSection) return;
|
||||
@@ -489,11 +486,374 @@ export function initTagResize() {
|
||||
handle.addEventListener("mousedown", (e) => {
|
||||
e.preventDefault();
|
||||
startY = e.clientY;
|
||||
4
|
||||
startHeight = tagSection.getBoundingClientRect().height;
|
||||
document.body.classList.add("resizing-v");
|
||||
handle.classList.add("active");
|
||||
document.addEventListener("mousemove", onMouseMove);
|
||||
document.addEventListener("mouseup", onMouseUp);
|
||||
});
|
||||
}
|
||||
|
||||
... [OUTPUT TRUNCATED - 30698 chars omitted out of 80698 total] ...
|
||||
// ---------------------------------------------------------------------------
|
||||
// Frontmatter Accent Card Builder
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
<div class="modal-warning">
|
||||
function buildFrontmatterCard(frontmatter) {
|
||||
// Helper: format date
|
||||
function formatDate(iso) {
|
||||
if (!iso) return "—";
|
||||
const d = new Date(iso);
|
||||
const date = d.toISOString().slice(0, 10);
|
||||
const time = d.toTimeString().slice(0, 5);
|
||||
return `${date} · ${time}`;
|
||||
}
|
||||
|
||||
// Extract boolean flags
|
||||
const booleanFlags = ["publish", "favoris", "template", "task", "archive", "draft", "private"].map((key) => ({ key, value: !!frontmatter[key] }));
|
||||
|
||||
// Toggle state
|
||||
let isOpen = true;
|
||||
|
||||
// Build header with chevron
|
||||
const chevron = el("span", { class: "fm-chevron open" });
|
||||
chevron.innerHTML = '<i data-lucide="chevron-down" style="width:14px;height:14px"></i>';
|
||||
|
||||
const fmHeader = el("div", { class: "fm-header" }, [chevron, document.createTextNode("Frontmatter")]);
|
||||
|
||||
// ZONE 1: Top strip
|
||||
const topBadges = [];
|
||||
|
||||
// Title badge
|
||||
const title = frontmatter.titre || frontmatter.title || "";
|
||||
if (title) {
|
||||
topBadges.push(el("span", { class: "ac-title" }, [document.createTextNode(`"${title}"`)]));
|
||||
}
|
||||
|
||||
// Status badge
|
||||
if (frontmatter.statut) {
|
||||
const statusBadge = el("span", { class: "ac-badge green" }, [el("span", { class: "ac-dot" }), document.createTextNode(frontmatter.statut)]);
|
||||
topBadges.push(statusBadge);
|
||||
}
|
||||
|
||||
// Category badge
|
||||
if (frontmatter.catégorie || frontmatter.categorie) {
|
||||
const cat = frontmatter.catégorie || frontmatter.categorie;
|
||||
const catBadge = el("span", { class: "ac-badge blue" }, [document.createTextNode(cat)]);
|
||||
topBadges.push(catBadge);
|
||||
}
|
||||
|
||||
// Publish badge
|
||||
if (frontmatter.publish) {
|
||||
topBadges.push(el("span", { class: "ac-badge purple" }, [document.createTextNode("publié")]));
|
||||
}
|
||||
|
||||
// Favoris badge
|
||||
if (frontmatter.favoris) {
|
||||
topBadges.push(el("span", { class: "ac-badge purple" }, [document.createTextNode("favori")]));
|
||||
}
|
||||
|
||||
const acTop = el("div", { class: "ac-top" }, topBadges);
|
||||
|
||||
// ZONE 2: Body 2 columns
|
||||
const leftCol = el("div", { class: "ac-col" }, [
|
||||
el("div", { class: "ac-row" }, [el("span", { class: "ac-k" }, [document.createTextNode("auteur")]), el("span", { class: "ac-v" }, [document.createTextNode(frontmatter.auteur || "—")])]),
|
||||
el("div", { class: "ac-row" }, [el("span", { class: "ac-k" }, [document.createTextNode("catégorie")]), el("span", { class: "ac-v" }, [document.createTextNode(frontmatter.catégorie || frontmatter.categorie ||
|
||||
|
||||
... [OUTPUT TRUNCATED - 6724 chars omitted out of 56724 total] ...
|
||||
|
||||
'div');
|
||||
modal.className = 'obsigate-modal';
|
||||
modal.innerHTML = `
|
||||
<div class="obsigate-modal-header">
|
||||
<h3 class="obsigate-modal-title">Créer un fichier</h3>
|
||||
</div>
|
||||
<div class="obsigate-modal-body">
|
||||
<div class="modal-form-group">
|
||||
<label class="modal-label">Nom du fichier</label>
|
||||
<input type="text" class="modal-input" id="file-name-input" placeholder="note.md" />
|
||||
<div class="modal-hint">Dans: ${parentPath || '/'}</div>
|
||||
<div class="modal-error" id="file-error" style="display:none;"></div>
|
||||
</div>
|
||||
<div class="modal-form-group">
|
||||
<label class="modal-label">Type de fichier</label>
|
||||
<select class="modal-select" id="file-ext-select">
|
||||
<option value=".md">Markdown (.md)</option>
|
||||
<option value=".txt">Texte (.txt)</option>
|
||||
<option value=".py">Python (.py)</option>
|
||||
<option value=".js">JavaScript (.js)</option>
|
||||
<option value=".json">JSON (.json)</option>
|
||||
<option value=".yaml">YAML (.yaml)</option>
|
||||
<option value=".sh">Shell (.sh)</option>
|
||||
<option value=".ps1">PowerShell (.ps1)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="obsigate-modal-footer">
|
||||
<button class="modal-btn" id="file-cancel-btn">Annuler</button>
|
||||
<button class="modal-btn primary" id="file-create-btn">Créer</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
overlay.appendChild(modal);
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
setTimeout(() => overlay.classList.add('active'), 10);
|
||||
|
||||
const input = modal.querySelector('#file-name-input');
|
||||
const extSelect = modal.querySelector('#file-ext-select');
|
||||
const errorDiv = modal.querySelector('#file-error');
|
||||
const createBtn = modal.querySelector('#file-create-btn');
|
||||
const cancelBtn = modal.querySelector('#file-cancel-btn');
|
||||
|
||||
input.focus();
|
||||
|
||||
const validateName = (name) => {
|
||||
if (!name.trim()) return 'Le nom ne peut pas être vide';
|
||||
if (/[/\\:*?"<>|]/.test(name)) return 'Caractères interdits: / \\ : * ? " < > |';
|
||||
return null;
|
||||
};
|
||||
|
||||
input.addEventListener('input', () => {
|
||||
const error = validateName(input.value);
|
||||
if (error) {
|
||||
errorDiv.textContent = error;
|
||||
errorDiv.style.display = 'block';
|
||||
input.classList.add('error');
|
||||
} else {
|
||||
errorDiv.style.display = 'none';
|
||||
input.classList.remove('error');
|
||||
}
|
||||
});
|
||||
|
||||
const create = async () => {
|
||||
let name = input.value.trim();
|
||||
const error = validateName(name);
|
||||
if (error) {
|
||||
errorDiv.textContent = error;
|
||||
errorDiv.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
const ext = extSelect.value;
|
||||
if (!name.endsWith(ext)) {
|
||||
name += ext;
|
||||
}
|
||||
|
||||
const path = parentPath ? `${parentPath}/${name}` : name;
|
||||
createBtn.disabled = true;
|
||||
createBtn.textContent = 'Création...';
|
||||
|
||||
try {
|
||||
await api(`/api/file/${encodeURIComponent(vault)}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ path, content: '' }),
|
||||
});
|
||||
|
||||
showToast(`Fichier "${name}" créé`, 'success');
|
||||
this._closeModal(overlay);
|
||||
await refreshSidebarTreePreservingState();
|
||||
openFile(vault, path);
|
||||
} catch (err) {
|
||||
showToast(err.message || 'Erreur lors de la création', 'error');
|
||||
createBtn.disabled = false;
|
||||
createBtn.textContent = 'Créer';
|
||||
}
|
||||
};
|
||||
|
||||
createBtn.addEventListener('click', create);
|
||||
cancelBtn.addEventListener('click', () => this._closeModal(overlay));
|
||||
|
||||
input.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') create();
|
||||
if (e.key === 'Escape') this._closeModal(overlay);
|
||||
});
|
||||
},
|
||||
|
||||
async startInlineRename(vault, path, type) {
|
||||
const item = document.querySelector(`.tree-item[data-vault="${CSS.escape(vault)}"][data-path="${CSS.escape(path)}"]`);
|
||||
if (!item) {
|
||||
showToast('Élément introuvable dans l’arborescence', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const textNode = Array.from(item.childNodes).find((node) => node.nodeType === Node.TEXT_NODE && node.textContent.trim());
|
||||
if (!textNode) {
|
||||
showToast('Impossible de renommer cet élément', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const originalText = textNode.textContent;
|
||||
const trimmedOriginal = originalText.trim();
|
||||
const currentName = path.split('/').pop() || trimmedOriginal;
|
||||
const baseName = type === 'file' ? currentName.replace(/(\.[^./\\]+)$/i, '') : currentName;
|
||||
const extension = type === 'file' ? (currentName.match(/(\.[^./\\]+)$/i)?.[1] || '') : '';
|
||||
|
||||
const input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.className = 'sidebar-item-input';
|
||||
input.value = baseName;
|
||||
|
||||
textNode.textContent = ' ';
|
||||
const badge = item.querySelector('.badge-small');
|
||||
if (badge) {
|
||||
item.insertBefore(input, badge);
|
||||
} else {
|
||||
item.appendChild(input);
|
||||
}
|
||||
|
||||
const restore = () => {
|
||||
input.remove();
|
||||
textNode.textContent = originalText;
|
||||
};
|
||||
|
||||
const validateName = (name) => {
|
||||
if (!name.trim()) return 'Le nom ne peut pas être vide';
|
||||
if (/[/\\:*?"<>|]/.test(name)) return 'Caractères interdits: / \\ : * ? " < > |';
|
||||
return null;
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
const name = input.value.trim();
|
||||
const error = validateName(name);
|
||||
if (error) {
|
||||
showToast(error, 'error');
|
||||
input.focus();
|
||||
input.select();
|
||||
return;
|
||||
}
|
||||
|
||||
const newName = `${name}${extension}`;
|
||||
if (newName === currentName) {
|
||||
restore();
|
||||
return;
|
||||
}
|
||||
|
||||
input.disabled = true;
|
||||
try {
|
||||
const endpoint = type === 'directory' ? `/api/directory/${encodeURIComponent(vault)}` : `/api/file/${encodeURIComponent(vault)}`;
|
||||
const result = await api(endpoint, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ path, new_name: newName }),
|
||||
});
|
||||
|
||||
const nextPath = result.new_path;
|
||||
await refreshSidebarTreePreservingState();
|
||||
|
||||
if (type === 'file' && currentVault === vault && currentPath === path) {
|
||||
await openFile(vault, nextPath);
|
||||
} else if (type === 'directory' && currentVault === vault && currentPath && (currentPath === path || currentPath.startsWith(`${path}/`))) {
|
||||
const suffix = currentPath === path ? '' : currentPath.slice(path.length);
|
||||
currentPath = `${nextPath}${suffix}`;
|
||||
await focusPathInSidebar(vault, currentPath, { alignToTop: false });
|
||||
}
|
||||
|
||||
showToast(type === 'directory' ? 'Dossier renommé' : 'Fichier renommé', 'success');
|
||||
} catch (err) {
|
||||
input.disabled = false;
|
||||
showToast(err.message || 'Erreur lors du renommage', 'error');
|
||||
input.focus();
|
||||
input.select();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
input.addEventListener('click', (e) => e.stopPropagation());
|
||||
input.addEventListener('keydown', async (e) => {
|
||||
e.stopPropagation();
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
await submit();
|
||||
}
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
restore();
|
||||
}
|
||||
});
|
||||
input.addEventListener('blur', async () => {
|
||||
if (!input.disabled) {
|
||||
await submit();
|
||||
}
|
||||
});
|
||||
|
||||
input.focus();
|
||||
input.setSelectionRange(0, input.value.length);
|
||||
},
|
||||
|
||||
confirmDeleteDirectory(vault, path) {
|
||||
const overlay = this._createModalOverlay();
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'obsigate-modal';
|
||||
modal.innerHTML = `
|
||||
<div class="obsigate-modal-header">
|
||||
<h3 class="obsigate-modal-title">Supprimer le dossier</h3>
|
||||
</div>
|
||||
<div class="obsigate-modal-body">
|
||||
<div class="modal-warning">
|
||||
<i data-lucide="alert-triangle" class="icon"></i>
|
||||
<div>
|
||||
<strong>Attention !</strong> Cette action est irréversible.
|
||||
<br>Tous les fichiers et sous-dossiers seront supprimés définitivement.
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-form-group">
|
||||
<label class="modal-label">Dossier à supprimer:</label>
|
||||
<div style="font-family: 'JetBrains Mono', monospace; color: var(--text-muted);">${path}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="obsigate-modal-footer">
|
||||
<button class="modal-btn" id="del-cancel-btn">Annuler</button>
|
||||
<button class="modal-btn danger" id="del-confirm-btn">Supprimer définitivement</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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 deleteDir = async () => {
|
||||
confirmBtn.disabled = true;
|
||||
confirmBtn.textContent = 'Suppression...';
|
||||
|
||||
try {
|
||||
const result = await api(`/api/directory/${encodeURIComponent(vault)}?path=${encodeURIComponent(path)}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
showToast(`Dossier supprimé (${result.deleted_count} fichiers)`, 'success');
|
||||
this._closeModal(overlay);
|
||||
await refreshSidebarTreePreservingState();
|
||||
|
||||
if (currentVault === vault && currentPath && currentPath.startsWith(path)) {
|
||||
showWelcome();
|
||||
}
|
||||
} catch (err) {
|
||||
showToast(err.message || 'Erreur lors de la suppression', 'error');
|
||||
confirmBtn.disabled = false;
|
||||
confirmBtn.textContent = 'Supprimer définitivement';
|
||||
}
|
||||
};
|
||||
|
||||
confirmBtn.addEventListener('click', deleteDir);
|
||||
cancelBtn.addEventListener('click', () => this._closeModal(overlay));
|
||||
},
|
||||
|
||||
confirmDeleteFile(vault, path) {
|
||||
const overlay = this._createModalOverlay();
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'obsigate-modal';
|
||||
modal.innerHTML = `
|
||||
<div class="obsigate-modal-header">
|
||||
<h3 class="obsigate-modal-title">Supprimer le fichier</h3>
|
||||
</div>
|
||||
<div class="obsigate-modal-body">
|
||||
<div class="modal-warning">
|
||||
<i data-lucide="alert-triangle" class="icon"></i>
|
||||
<div>
|
||||
<strong>Attention !</strong> Cette action est irréversible.
|
||||
@@ -532,7 +892,7 @@ export function initTagResize() {
|
||||
this._closeModal(overlay);
|
||||
await refreshSidebarTreePreservingState();
|
||||
|
||||
if (state.currentVault === vault && state.currentPath === path) {
|
||||
if (currentVault === vault && currentPath === path) {
|
||||
showWelcome();
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -566,7 +926,7 @@ export function initTagResize() {
|
||||
// ---------------------------------------------------------------------------
|
||||
// Find in Page Manager
|
||||
// ---------------------------------------------------------------------------
|
||||
export const FindInPageManager = {
|
||||
const FindInPageManager = {
|
||||
isOpen: false,
|
||||
searchTerm: "",
|
||||
matches: [],
|
||||
@@ -985,313 +1345,112 @@ export const FindInPageManager = {
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tab Manager
|
||||
// Init
|
||||
// ---------------------------------------------------------------------------
|
||||
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(),
|
||||
async function init() {
|
||||
initTheme();
|
||||
initHeaderMenu();
|
||||
initCustomDropdowns();
|
||||
document.getElementById("theme-toggle").addEventListener("click", toggleTheme);
|
||||
document.getElementById("header-logo").addEventListener("click", goHome);
|
||||
const refreshBtn = document.getElementById("header-refresh-btn");
|
||||
if (refreshBtn) refreshBtn.addEventListener("click", goHome);
|
||||
initSearch();
|
||||
initSidebarToggle();
|
||||
initMobile();
|
||||
initVaultContext();
|
||||
initSidebarTabs();
|
||||
initHelpModal();
|
||||
initConfigModal();
|
||||
initSidebarFilter();
|
||||
initSidebarResize();
|
||||
initEditor();
|
||||
initLoginForm();
|
||||
initRecentTab();
|
||||
RightSidebarManager.init();
|
||||
FindInPageManager.init();
|
||||
ContextMenuManager.init();
|
||||
|
||||
init() {
|
||||
this._tabBar = document.getElementById("tab-bar");
|
||||
this._tabList = document.getElementById("tab-list");
|
||||
},
|
||||
// Check auth status first
|
||||
const authOk = await AuthManager.initAuth();
|
||||
|
||||
/** 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 (authOk) {
|
||||
// Start SSE sync AFTER auth is established (cookie available)
|
||||
initSyncStatus();
|
||||
|
||||
// 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;
|
||||
}
|
||||
try {
|
||||
await Promise.all([loadVaultSettings(), loadVaults(), loadTags()]);
|
||||
|
||||
// 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;
|
||||
// Initialize dashboard widgets now that vaults are loaded
|
||||
if (typeof DashboardRecentWidget !== "undefined") {
|
||||
DashboardRecentWidget.init();
|
||||
}
|
||||
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();
|
||||
// Check for popup mode query parameter
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
if (urlParams.get("popup") === "true") {
|
||||
document.body.classList.add("popup-mode");
|
||||
}
|
||||
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 = '<div style="padding:40px;text-align:center;color:var(--text-muted)">Chargement...</div>';
|
||||
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);
|
||||
// Handle direct deep-link to file via #file=...
|
||||
if (window.location.hash && window.location.hash.startsWith("#file=")) {
|
||||
const hashVal = window.location.hash.substring(6);
|
||||
const sepIndex = hashVal.indexOf(":");
|
||||
if (sepIndex > -1) {
|
||||
const vault = decodeURIComponent(hashVal.substring(0, sepIndex));
|
||||
const path = decodeURIComponent(hashVal.substring(sepIndex + 1));
|
||||
openFile(vault, path);
|
||||
}
|
||||
if (cache.scrollTop) {
|
||||
area.scrollTop = cache.scrollTop;
|
||||
}
|
||||
} catch (err) {
|
||||
area.innerHTML = `<div style="padding:40px;text-align:center;color:var(--text-error)">Erreur: ${escapeHtml(err.message)}</div>`;
|
||||
} else if (urlParams.get("popup") !== "true") {
|
||||
// Default to dashboard if no deep link and not in popup mode
|
||||
showWelcome();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to initialize ObsiGate:", err);
|
||||
showToast("Erreur lors de l'initialisation", "error");
|
||||
}
|
||||
}
|
||||
|
||||
safeCreateIcons();
|
||||
}
|
||||
|
||||
// ---- Modify openFile to use TabManager ----
|
||||
const _originalOpenFile = openFile;
|
||||
openFile = function(vault, path) {
|
||||
TabManager.open(vault, path);
|
||||
};
|
||||
|
||||
// ---- Keyboard shortcuts for tabs ----
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
if (e.key === "w" || e.key === "W") {
|
||||
e.preventDefault();
|
||||
if (TabManager._activeTabId) {
|
||||
TabManager.close(TabManager._activeTabId);
|
||||
}
|
||||
} else if (e.key === "Tab" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
const tabs = TabManager._tabs;
|
||||
const currentIdx = tabs.findIndex(t => t.id === TabManager._activeTabId);
|
||||
if (currentIdx >= 0 && tabs.length > 1) {
|
||||
const nextIdx = (currentIdx + 1) % tabs.length;
|
||||
TabManager.activate(tabs[nextIdx].id);
|
||||
}
|
||||
} else if (e.key === "Tab" && e.shiftKey) {
|
||||
e.preventDefault();
|
||||
const tabs = TabManager._tabs;
|
||||
const currentIdx = tabs.findIndex(t => t.id === TabManager._activeTabId);
|
||||
if (currentIdx >= 0 && tabs.length > 1) {
|
||||
const prevIdx = (currentIdx - 1 + tabs.length) % tabs.length;
|
||||
TabManager.activate(tabs[prevIdx].id);
|
||||
}
|
||||
}
|
||||
|
||||
// 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 */
|
||||
// ---- Modify init to include TabManager ----
|
||||
const _origInit2 = init;
|
||||
init = function() {
|
||||
_origInit2();
|
||||
TabManager.init();
|
||||
};
|
||||
Reference in New Issue
Block a user