Files
ObsiGate/frontend/js/legacy.js
T
bruno 4836d6f1d0
CI / lint (push) Successful in 10s
CI / security (push) Successful in 8s
CI / build (push) Has been cancelled
CI / test (push) Has been cancelled
refactor: split app.js (8875 lines) into 12 ES modules
frontend/js/ structure:
  state.js      (55 lines)  — Shared mutable state, constants
  utils.js      (510 lines) — EXT_ICONS, getFileIcon, escapeHtml, safeCreateIcons
  auth.js       (547 lines) — api(), AuthManager, initLoginForm, AdminPanel
  search.js     (1106 lines)— SearchHistory, QueryParser, Autocomplete, performSearch
  sidebar.js    (1091 lines)— Vault tree, sidebar filter, TagFilterService, loadTags
  viewer.js     (1554 lines)— openFile, Outline, ScrollSpy, Frontmatter, Editor
  ui.js         (2250 lines)— Theme, Toast, Sidebar, Dropdowns, Tabs, ContextMenu
  dashboard.js  (461 lines) — Dashboard widgets (Recent, Stats, Bookmarks)
  config.js     (999 lines) — Config panel, Hidden files, About, Sidebar tabs
  sync.js       (436 lines) — SSE/IndexUpdateManager, PWA registration
  graph.js      (401 lines) — GraphViewManager (force-directed canvas graph)
  legacy.js     (550 lines) — Remaining bridge functions (goHome, showWelcome, initSearch)
  app.js        (80 lines)  — Thin orchestrator: imports all modules, calls init()

index.html: switched from <script src="app.js"> to <script type="module" src="js/app.js">
Original app.js preserved for backward compatibility.
All 14 modules pass node --check syntax validation.
2026-05-28 14:04:50 -04:00

561 lines
21 KiB
JavaScript

/* ObsiGate — Legacy module: remaining functions for the orchestrator
Extracted from the monolithic frontend/app.js IIFE.
Functions already in other modules are re-exported. */
// --- State imports ---
import {
currentVault,
currentPath,
showingSource,
cachedRawSource,
searchTimeout,
searchCaseSensitive,
searchWholeWord,
searchRegex,
searchFilterVisible,
advancedSearchOffset,
selectedTags,
selectedContextVault,
vaultSettings,
allVaults,
MIN_SEARCH_LENGTH,
} from './state.js';
// --- Search imports ---
import {
AutocompleteDropdown,
SearchChips,
SearchHistory,
performAdvancedSearch,
} from './search.js';
// --- Dashboard imports ---
import {
DashboardRecentWidget,
DashboardStatsWidget,
DashboardBookmarkWidget,
DashboardSharedWidget,
} from './dashboard.js';
// --- Re-exports from modules that already have these functions ---
export { initSidebarTabs, initConfigModal, initHelpModal, initRecentTab } from './config.js';
export { initSyncStatus } from './sync.js';
export { DashboardRecentWidget } from './dashboard.js';
// =========================================================================
// Progress bar helpers (used by showWelcome)
// =========================================================================
function showProgressBar() {
const bar = document.getElementById("search-progress-bar");
if (bar) bar.classList.add("active");
}
function hideProgressBar() {
const bar = document.getElementById("search-progress-bar");
if (bar) bar.classList.remove("active");
}
// =========================================================================
// loadVaultSettings
// =========================================================================
async function loadVaultSettings() {
try {
const settings = await api("/api/vaults/settings/all");
vaultSettings = settings;
} catch (err) {
console.error("Failed to load vault settings:", err);
vaultSettings = {};
}
}
// =========================================================================
// Config helpers (needed by initSearch)
// =========================================================================
const _FRONTEND_CONFIG_KEY = "obsigate-perf-config";
function _getFrontendConfig() {
try {
return JSON.parse(localStorage.getItem(_FRONTEND_CONFIG_KEY) || "{}");
} catch {
return {};
}
}
function _getEffective(key, fallback) {
const cfg = _getFrontendConfig();
return cfg[key] !== undefined ? cfg[key] : fallback;
}
/** Check if user is focused on an input/textarea/contenteditable */
function _isInputFocused() {
const tag = document.activeElement?.tagName;
if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
return document.activeElement?.isContentEditable === true;
}
// =========================================================================
// initSearch
// =========================================================================
function initSearch() {
const input = document.getElementById("search-input");
if (!input) return;
const caseBtn = document.getElementById("search-case-btn");
const wordBtn = document.getElementById("search-word-btn");
const regexBtn = document.getElementById("search-regex-btn");
const filterBtn = document.getElementById("search-filter-btn");
const clearBtn = document.getElementById("search-clear-btn");
const filterRow = document.getElementById("search-filter-row");
const prevBtn = document.getElementById("search-prev-btn");
const nextBtn = document.getElementById("search-next-btn");
const countEl = document.getElementById("search-match-count");
function _updateToggleUI() {
caseBtn.classList.toggle("active", searchCaseSensitive);
wordBtn.classList.toggle("active", searchWholeWord);
regexBtn.classList.toggle("active", searchRegex);
filterBtn.classList.toggle("active", searchFilterVisible);
}
// Toggle buttons
caseBtn.addEventListener("click", () => { searchCaseSensitive = !searchCaseSensitive; _updateToggleUI(); _research(); });
if (wordBtn) wordBtn.addEventListener("click", () => { searchWholeWord = !searchWholeWord; _updateToggleUI(); _research(); });
if (regexBtn) regexBtn.addEventListener("click", () => { searchRegex = !searchRegex; _updateToggleUI(); _research(); });
if (filterBtn) filterBtn.addEventListener("click", () => { searchFilterVisible = !searchFilterVisible; if (filterRow) filterRow.style.display = searchFilterVisible ? "flex" : "none"; _updateToggleUI(); });
// Result navigation (up/down arrows + Enter)
let _searchResultIdx = -1;
let _searchResultItems = [];
function _updateResultHighlight() {
_searchResultItems.forEach((el, i) => {
el.classList.toggle("search-result-active", i === _searchResultIdx);
});
if (_searchResultIdx >= 0 && _searchResultIdx < _searchResultItems.length) {
_searchResultItems[_searchResultIdx].scrollIntoView({ block: "nearest", behavior: "smooth" });
}
const countEl = document.getElementById("search-match-count");
if (countEl) countEl.textContent = _searchResultIdx >= 0 ? `${_searchResultIdx + 1}/${_searchResultItems.length}` : `0/${_searchResultItems.length}`;
}
function _refreshResultItems() {
_searchResultItems = Array.from(document.querySelectorAll(".search-result-item"));
_searchResultIdx = _searchResultItems.length > 0 ? 0 : -1;
_updateResultHighlight();
}
window.navigateSearchResults = function(delta) {
_searchResultItems = Array.from(document.querySelectorAll(".search-result-item"));
if (_searchResultItems.length === 0) return;
_searchResultIdx = Math.max(0, Math.min(_searchResultItems.length - 1, _searchResultIdx + delta));
_updateResultHighlight();
};
if (prevBtn) prevBtn.addEventListener("click", () => navigateSearchResults(-1));
if (nextBtn) nextBtn.addEventListener("click", () => navigateSearchResults(1));
function _research() {
const q = input.value.trim();
if (q.length >= _getEffective("min_query_length", 2)) {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
const vault = document.getElementById("vault-filter").value;
const tagFilter = selectedTags.length > 0 ? selectedTags.join(",") : null;
advancedSearchOffset = 0;
performAdvancedSearch(q, vault, tagFilter);
}, _getEffective("debounce_ms", 300));
}
}
// Keyboard shortcuts
document.addEventListener("keydown", (e) => {
if (e.altKey && !e.ctrlKey && !e.metaKey) {
if (e.key === "c" || e.key === "C") { e.preventDefault(); caseBtn.click(); }
else if (e.key === "w" || e.key === "W") { e.preventDefault(); if (wordBtn) wordBtn.click(); }
else if (e.key === "r" || e.key === "R") { e.preventDefault(); if (regexBtn) regexBtn.click(); }
else if (e.key === "f" || e.key === "F") { e.preventDefault(); if (filterBtn) filterBtn.click(); input.focus(); }
}
});
// Initialize sub-controllers
AutocompleteDropdown.init();
SearchChips.init();
// Initially hide clear button
if (clearBtn) clearBtn.style.display = "none";
// Input handler: debounced search + autocomplete dropdown
input.addEventListener("input", () => {
const hasText = input.value.length > 0;
clearBtn.style.display = hasText ? "flex" : "none";
// Show autocomplete dropdown while typing
AutocompleteDropdown.populate(input.value, input.selectionStart);
// Debounced search execution
clearTimeout(searchTimeout);
searchTimeout = setTimeout(
() => {
const q = input.value.trim();
const vault = document.getElementById("vault-filter").value;
const tagFilter = selectedTags.length > 0 ? selectedTags.join(",") : null;
advancedSearchOffset = 0;
if (q.length >= _getEffective("min_query_length", MIN_SEARCH_LENGTH) || tagFilter) {
performAdvancedSearch(q, vault, tagFilter);
} else if (q.length === 0) {
SearchChips.clear();
showWelcome();
}
},
_getEffective("debounce_ms", 300),
);
});
// Focus handler: show history dropdown
input.addEventListener("focus", () => {
if (input.value.length === 0) {
const historyItems = SearchHistory.filter("").slice(0, 5);
if (historyItems.length > 0) {
AutocompleteDropdown.populate("", 0);
}
}
});
// Keyboard navigation in dropdown
input.addEventListener("keydown", (e) => {
if (AutocompleteDropdown.isVisible()) {
if (e.key === "ArrowDown") {
e.preventDefault();
AutocompleteDropdown.navigateDown();
} else if (e.key === "ArrowUp") {
e.preventDefault();
AutocompleteDropdown.navigateUp();
} else if (e.key === "Enter") {
// First: check dropdown suggestions (higher priority than search results)
if (AutocompleteDropdown.isVisible() && AutocompleteDropdown.selectActive()) {
e.preventDefault();
return;
}
// Second: navigate search results if visible
const results = document.querySelectorAll(".search-result-item");
if (results.length > 0 && _searchResultIdx >= 0) {
const el = results[_searchResultIdx];
if (el) {
const vault = el.dataset.vault;
const path = el.dataset.path;
if (vault && path) { TabManager.openPreview(vault, path); e.preventDefault(); return; }
}
}
// Third: execute search
AutocompleteDropdown.hide();
const q = input.value.trim();
if (q) {
SearchHistory.add(q);
clearTimeout(searchTimeout);
advancedSearchOffset = 0;
const vault = document.getElementById("vault-filter").value;
const tagFilter = selectedTags.length > 0 ? selectedTags.join(",") : null;
performAdvancedSearch(q, vault, tagFilter);
}
e.preventDefault();
} else if (e.key === "ArrowDown" && !AutocompleteDropdown.isVisible()) {
// Navigate search results when dropdown is closed
if (window.navigateSearchResults) { window.navigateSearchResults(1); e.preventDefault(); }
} else if (e.key === "ArrowUp" && !AutocompleteDropdown.isVisible()) {
if (window.navigateSearchResults) { window.navigateSearchResults(-1); e.preventDefault(); }
} else if (e.key === "Escape") {
AutocompleteDropdown.hide();
e.stopPropagation();
}
} else if (e.key === "Enter") {
if (AutocompleteDropdown.isVisible() && AutocompleteDropdown.selectActive()) {
e.preventDefault();
return;
}
const q = input.value.trim();
if (q) {
SearchHistory.add(q);
clearTimeout(searchTimeout);
advancedSearchOffset = 0;
const vault = document.getElementById("vault-filter").value;
const tagFilter = selectedTags.length > 0 ? selectedTags.join(",") : null;
performAdvancedSearch(q, vault, tagFilter);
}
e.preventDefault();
}
});
clearBtn.addEventListener("click", () => {
input.value = "";
if (clearBtn) clearBtn.style.display = "none";
searchCaseSensitive = false;
searchWholeWord = false;
searchRegex = false;
_updateToggleUI();
SearchChips.clear();
AutocompleteDropdown.hide();
showWelcome();
});
// Global keyboard shortcuts
document.addEventListener("keydown", (e) => {
// Ctrl+K or Cmd+K: focus search
if ((e.ctrlKey || e.metaKey) && e.key === "k") {
e.preventDefault();
input.focus();
input.select();
}
// "/" key: focus search (when not in an input/textarea)
if (e.key === "/" && !_isInputFocused()) {
e.preventDefault();
input.focus();
}
// Escape: blur search input and close dropdown
if (e.key === "Escape" && document.activeElement === input) {
AutocompleteDropdown.hide();
input.blur();
}
});
}
// =========================================================================
// showWelcome
// =========================================================================
function showWelcome() {
hideProgressBar();
// Restore or rebuild the dashboard with tabbed sections
const area = document.getElementById("content-area");
const home = document.getElementById("dashboard-home");
if (area && !home) {
area.innerHTML = `
<div id="dashboard-home" class="dashboard-home" role="region" aria-label="Tableau de bord">
<!-- Dashboard Tabs -->
<div class="dashboard-tabs">
<button class="dashboard-tab active" data-tab="stats">
<i data-lucide="bar-chart-3" style="width:14px;height:14px"></i> Statistiques
</button>
<button class="dashboard-tab" data-tab="bookmarks">
<i data-lucide="bookmark" style="width:14px;height:14px"></i> Bookmarks
</button>
<button class="dashboard-tab" data-tab="recent">
<i data-lucide="clock" style="width:14px;height:14px"></i> Récents
</button>
<button class="dashboard-tab" data-tab="shared">
<i data-lucide="share-2" style="width:14px;height:14px"></i> Partagés
</button>
</div>
<!-- Stats Panel -->
<div id="dashboard-panel-stats" class="dashboard-panel active">
<div id="dashboard-stats-grid" class="dashboard-stats-grid">
<div class="dashboard-stats-loading">Chargement...</div>
</div>
<div id="dashboard-conflicts-container" style="margin-top:16px"></div>
</div>
<!-- Bookmarks Panel -->
<div id="dashboard-panel-bookmarks" class="dashboard-panel">
<div id="dashboard-bookmarks-grid" class="dashboard-recent-grid"></div>
<div id="dashboard-bookmarks-empty" class="dashboard-recent-empty">
<i data-lucide="pin"></i>
<span>Aucun bookmark</span>
<p>Épinglez des fichiers pour les retrouver ici.</p>
</div>
</div>
<!-- Recent Panel -->
<div id="dashboard-panel-recent" class="dashboard-panel">
<div class="dashboard-header">
<div class="dashboard-title-row">
<span id="dashboard-count" class="dashboard-badge"></span>
</div>
</div>
<div id="dashboard-recent-grid" class="dashboard-recent-grid"></div>
<div id="dashboard-loading" class="dashboard-loading">
<div class="skeleton-card"></div><div class="skeleton-card"></div><div class="skeleton-card"></div>
<div class="skeleton-card"></div><div class="skeleton-card"></div><div class="skeleton-card"></div>
</div>
<div id="dashboard-recent-empty" class="dashboard-recent-empty hidden">
<i data-lucide="inbox"></i>
<span>Aucun fichier récent</span>
<p>Ouvrez un fichier pour le voir apparaître ici</p>
</div>
</div>
<!-- Shared Panel -->
<div id="dashboard-panel-shared" class="dashboard-panel">
<div id="dashboard-shared-grid" class="dashboard-recent-grid"></div>
<div id="dashboard-shared-empty" class="dashboard-recent-empty">
<i data-lucide="share-2"></i>
<span>Aucun document partagé</span>
<p>Partagez un document pour le voir apparaître ici</p>
</div>
</div>
</div>`;
// Re-initialize widgets and dashboard tabs
DashboardRecentWidget.init();
initDashboardTabs();
safeCreateIcons();
} else if (home) {
// Dashboard already exists, show it with default tab
home.style.display = "";
// Reset tabs to default
document.querySelectorAll(".dashboard-tab").forEach(t => t.classList.remove("active"));
document.querySelectorAll(".dashboard-panel").forEach(p => p.classList.remove("active"));
const defaultTab = document.querySelector('.dashboard-tab[data-tab="stats"]');
const defaultPanel = document.getElementById("dashboard-panel-stats");
if (defaultTab) defaultTab.classList.add("active");
if (defaultPanel) defaultPanel.classList.add("active");
}
// Load all widgets (they handle missing elements gracefully)
if (typeof DashboardStatsWidget !== "undefined") {
DashboardStatsWidget.load();
}
if (typeof DashboardConflictsWidget !== "undefined") {
DashboardConflictsWidget.load();
}
DashboardRecentWidget.load(selectedContextVault);
if (typeof DashboardBookmarkWidget !== "undefined") {
DashboardBookmarkWidget.load(selectedContextVault);
}
if (typeof DashboardSharedWidget !== "undefined") {
DashboardSharedWidget.load();
}
// Load saved searches sidebar
loadSavedSearches();
}
// =========================================================================
// goHome
// =========================================================================
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();
}
// =========================================================================
// loadSavedSearches (needed by showWelcome)
// =========================================================================
async function loadSavedSearches() {
const list = document.getElementById("saved-searches-list");
const empty = document.getElementById("saved-searches-empty");
if (!list) return;
try {
const searches = await api("/api/saved-searches");
if (!searches.length) {
list.innerHTML = "";
if (empty) empty.style.display = "";
return;
}
if (empty) empty.style.display = "none";
list.innerHTML = searches.map(s => {
const badges = [];
if (s.case_sensitive) badges.push('<span class="search-filter-badge">Aa</span>');
if (s.whole_word) badges.push('<span class="search-filter-badge">wd</span>');
if (s.regex) badges.push('<span class="search-filter-badge">.*</span>');
const pathFilters = [];
if (s.include_paths) pathFilters.push(`<span class="saved-search-path" title="Inclure: ${escapeHtml(s.include_paths)}">\u{1F4E5} ${escapeHtml(s.include_paths)}</span>`);
if (s.exclude_paths) pathFilters.push(`<span class="saved-search-path" title="Exclure: ${escapeHtml(s.exclude_paths)}">\u{1F4E4} ${escapeHtml(s.exclude_paths)}</span>`);
const vaultStr = s.vault && s.vault !== "all" ? `<span class="saved-search-vault">\u{1F4C1} ${escapeHtml(s.vault)}</span>` : "";
return `
<div class="saved-search-item">
<div class="saved-search-query">${escapeHtml(s.query)}</div>
<div class="saved-search-meta">
${badges.join("")}
${vaultStr}
</div>
${pathFilters.length ? '<div class="saved-search-filters">' + pathFilters.join(" ") + '</div>' : ""}
<button class="saved-search-delete" data-id="${s.id}" title="Supprimer"><E2><9C><95></button>
</div>
`}).join("");
list.querySelectorAll(".saved-search-item").forEach(item => {
item.addEventListener("click", (e) => {
if (e.target.classList.contains("saved-search-delete")) return;
const idx = Array.from(list.children).indexOf(item);
const s = searches[idx];
if (!s) return;
// Apply the saved search
const input = document.getElementById("search-input");
if (input) input.value = s.query;
searchCaseSensitive = s.case_sensitive || false;
searchWholeWord = s.whole_word || false;
searchRegex = s.regex || false;
if (typeof _updateToggleUI === "function") _updateToggleUI();
if (s.include_paths) {
const incl = document.getElementById("search-include-input");
if (incl) incl.value = s.include_paths;
}
if (s.exclude_paths) {
const excl = document.getElementById("search-exclude-input");
if (excl) excl.value = s.exclude_paths;
}
// Execute the search
AutocompleteDropdown.hide();
AutocompleteDropdown._suppressNext = true;
const vault = s.vault || "all";
if (input) { input.dispatchEvent(new Event("input")); }
clearTimeout(searchTimeout);
advancedSearchOffset = 0;
performAdvancedSearch(s.query, vault, null);
});
});
list.querySelectorAll(".saved-search-delete").forEach(b => b.addEventListener("click", async (e) => {
e.stopPropagation();
await api(`/api/saved-searches/${b.dataset.id}`, { method: "DELETE" });
loadSavedSearches();
}));
safeCreateIcons();
} catch (err) { /* silently ignore */ }
}
// =========================================================================
// initDashboardTabs (needed by showWelcome)
// =========================================================================
function initDashboardTabs() {
document.querySelectorAll(".dashboard-tab").forEach(tab => {
// Remove existing listeners by cloning
const newTab = tab.cloneNode(true);
tab.parentNode.replaceChild(newTab, tab);
newTab.addEventListener("click", function() {
document.querySelectorAll(".dashboard-tab").forEach(t => t.classList.remove("active"));
document.querySelectorAll(".dashboard-panel").forEach(p => p.classList.remove("active"));
this.classList.add("active");
const panel = document.getElementById("dashboard-panel-" + this.dataset.tab);
if (panel) panel.classList.add("active");
});
});
}
// =========================================================================
// Exports
// =========================================================================
export {
loadVaultSettings,
initSearch,
showWelcome,
goHome,
};