fix: regenerate corrupted ui.js, search.js, viewer.js from app.js
CI / lint (push) Successful in 12s
CI / security (push) Successful in 8s
CI / build (push) Has been cancelled
CI / test (push) Has been cancelled

This commit is contained in:
2026-05-28 16:49:29 -04:00
parent a2ff9297ce
commit 6d36b53b3a
3 changed files with 870 additions and 530 deletions
+168 -118
View File
@@ -1,19 +1,12 @@
/* ObsiGate — Search module (extracted from app.js) */ /* ObsiGate — Auto-extracted module */
import { state } from './state.js'; import { state } from './state.js';
import { safeCreateIcons } from './utils.js';
// Re-export constants used internally
const state.SEARCH_HISTORY_KEY = state.SEARCH_HISTORY_KEY;
const state.MAX_HISTORY_ENTRIES = state.MAX_HISTORY_ENTRIES;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Search History Service (localStorage, LIFO, max 50, dedup) // Search History Service (localStorage, LIFO, max 50, dedup)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export const SearchHistory = { const SearchHistory = {
_load() { _load() {
try { try {
const raw = localStorage.getItem(state.SEARCH_HISTORY_KEY); const raw = localStorage.getItem(SEARCH_HISTORY_KEY);
return raw ? JSON.parse(raw) : []; return raw ? JSON.parse(raw) : [];
} catch { } catch {
return []; return [];
@@ -21,7 +14,7 @@ export const SearchHistory = {
}, },
_save(entries) { _save(entries) {
try { try {
localStorage.setItem(state.SEARCH_HISTORY_KEY, JSON.stringify(entries)); localStorage.setItem(SEARCH_HISTORY_KEY, JSON.stringify(entries));
} catch {} } catch {}
}, },
getAll() { getAll() {
@@ -32,7 +25,7 @@ export const SearchHistory = {
const q = query.trim(); const q = query.trim();
let entries = this._load().filter((e) => e !== q); let entries = this._load().filter((e) => e !== q);
entries.unshift(q); entries.unshift(q);
if (entries.length > state.MAX_HISTORY_ENTRIES) entries = entries.slice(0, state.MAX_HISTORY_ENTRIES); if (entries.length > MAX_HISTORY_ENTRIES) entries = entries.slice(0, MAX_HISTORY_ENTRIES);
this._save(entries); this._save(entries);
}, },
remove(query) { remove(query) {
@@ -54,7 +47,7 @@ export const SearchHistory = {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Query Parser — extracts operators (tag:, #, vault:, title:, path:, ext:) // Query Parser — extracts operators (tag:, #, vault:, title:, path:, ext:)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export const QueryParser = { const QueryParser = {
parse(raw) { parse(raw) {
const result = { tags: [], vault: null, title: null, path: null, ext: null, freeText: "" }; const result = { tags: [], vault: null, title: null, path: null, ext: null, freeText: "" };
if (!raw) return result; if (!raw) return result;
@@ -129,7 +122,7 @@ export const QueryParser = {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Autocomplete Dropdown Controller // Autocomplete Dropdown Controller
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export const AutocompleteDropdown = { const AutocompleteDropdown = {
_dropdown: null, _dropdown: null,
_historySection: null, _historySection: null,
_titlesSection: null, _titlesSection: null,
@@ -174,8 +167,8 @@ export const AutocompleteDropdown = {
hide() { hide() {
if (this._dropdown) this._dropdown.hidden = true; if (this._dropdown) this._dropdown.hidden = true;
state.dropdownActiveIndex = -1; dropdownActiveIndex = -1;
state.dropdownItems = []; dropdownItems = [];
}, },
isVisible() { isVisible() {
@@ -186,9 +179,9 @@ export const AutocompleteDropdown = {
async populate(inputValue, cursorPos) { async populate(inputValue, cursorPos) {
if (this._suppressNext) { this._suppressNext = false; return; } if (this._suppressNext) { this._suppressNext = false; return; }
// Cancel previous suggestion request // Cancel previous suggestion request
if (state.suggestAbortController) { if (suggestAbortController) {
state.suggestAbortController.abort(); suggestAbortController.abort();
state.suggestAbortController = null; suggestAbortController = null;
} }
const ctx = QueryParser.getContext(inputValue, cursorPos); const ctx = QueryParser.getContext(inputValue, cursorPos);
@@ -235,10 +228,10 @@ export const AutocompleteDropdown = {
}, },
async _fetchSuggestions(prefix, vault) { async _fetchSuggestions(prefix, vault) {
state.suggestAbortController = new AbortController(); suggestAbortController = new AbortController();
// Fetch titles // Fetch titles
try { try {
const titlesRes = await api(`/api/suggest?q=${encodeURIComponent(prefix)}&vault=${encodeURIComponent(vault)}&limit=5`, { signal: state.suggestAbortController.signal }); const titlesRes = await api(`/api/suggest?q=${encodeURIComponent(prefix)}&vault=${encodeURIComponent(vault)}&limit=5`, { signal: suggestAbortController.signal });
this._renderTitles(titlesRes.suggestions || [], prefix); this._renderTitles(titlesRes.suggestions || [], prefix);
this._titlesSection.hidden = !(titlesRes.suggestions || []).length; this._titlesSection.hidden = !(titlesRes.suggestions || []).length;
if (titlesRes.suggestions?.length) this.show(); if (titlesRes.suggestions?.length) this.show();
@@ -248,7 +241,7 @@ export const AutocompleteDropdown = {
} }
// Fetch tags — keep section always visible to confirm it works // Fetch tags — keep section always visible to confirm it works
try { try {
const tagsRes = await api(`/api/tags/suggest?q=${encodeURIComponent(prefix)}&vault=${encodeURIComponent(vault)}&limit=5`, { signal: state.suggestAbortController.signal }); const tagsRes = await api(`/api/tags/suggest?q=${encodeURIComponent(prefix)}&vault=${encodeURIComponent(vault)}&limit=5`, { signal: suggestAbortController.signal });
const items = tagsRes.suggestions || []; const items = tagsRes.suggestions || [];
if (items.length > 0) { if (items.length > 0) {
this._renderTags(items, prefix); this._renderTags(items, prefix);
@@ -370,30 +363,30 @@ export const AutocompleteDropdown = {
}, },
_collectItems() { _collectItems() {
state.dropdownItems = Array.from(this._dropdown.querySelectorAll(".search-dropdown__item")); dropdownItems = Array.from(this._dropdown.querySelectorAll(".search-dropdown__item"));
state.dropdownActiveIndex = -1; dropdownActiveIndex = -1;
state.dropdownItems.forEach((item) => item.classList.remove("active")); dropdownItems.forEach((item) => item.classList.remove("active"));
}, },
navigateDown() { navigateDown() {
if (!this.isVisible() || state.dropdownItems.length === 0) return; if (!this.isVisible() || dropdownItems.length === 0) return;
if (state.dropdownActiveIndex >= 0) state.dropdownItems[state.dropdownActiveIndex].classList.remove("active"); if (dropdownActiveIndex >= 0) dropdownItems[dropdownActiveIndex].classList.remove("active");
state.dropdownActiveIndex = (state.dropdownActiveIndex + 1) % state.dropdownItems.length; dropdownActiveIndex = (dropdownActiveIndex + 1) % dropdownItems.length;
state.dropdownItems[state.dropdownActiveIndex].classList.add("active"); dropdownItems[dropdownActiveIndex].classList.add("active");
state.dropdownItems[state.dropdownActiveIndex].scrollIntoView({ block: "nearest" }); dropdownItems[dropdownActiveIndex].scrollIntoView({ block: "nearest" });
}, },
navigateUp() { navigateUp() {
if (!this.isVisible() || state.dropdownItems.length === 0) return; if (!this.isVisible() || dropdownItems.length === 0) return;
if (state.dropdownActiveIndex >= 0) state.dropdownItems[state.dropdownActiveIndex].classList.remove("active"); if (dropdownActiveIndex >= 0) dropdownItems[dropdownActiveIndex].classList.remove("active");
state.dropdownActiveIndex = state.dropdownActiveIndex <= 0 ? state.dropdownItems.length - 1 : state.dropdownActiveIndex - 1; dropdownActiveIndex = dropdownActiveIndex <= 0 ? dropdownItems.length - 1 : dropdownActiveIndex - 1;
state.dropdownItems[state.dropdownActiveIndex].classList.add("active"); dropdownItems[dropdownActiveIndex].classList.add("active");
state.dropdownItems[state.dropdownActiveIndex].scrollIntoView({ block: "nearest" }); dropdownItems[dropdownActiveIndex].scrollIntoView({ block: "nearest" });
}, },
selectActive() { selectActive() {
if (state.dropdownActiveIndex >= 0 && state.dropdownActiveIndex < state.dropdownItems.length) { if (dropdownActiveIndex >= 0 && dropdownActiveIndex < dropdownItems.length) {
state.dropdownItems[state.dropdownActiveIndex].click(); dropdownItems[dropdownActiveIndex].click();
return true; return true;
} }
return false; return false;
@@ -403,7 +396,7 @@ export const AutocompleteDropdown = {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Search Chips Controller — renders active filter chips from parsed query // Search Chips Controller — renders active filter chips from parsed query
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export const SearchChips = { const SearchChips = {
_container: null, _container: null,
init() { init() {
this._container = document.getElementById("search-chips"); this._container = document.getElementById("search-chips");
@@ -443,14 +436,71 @@ export const SearchChips = {
const chip = el("span", { class: `search-chip search-chip--${type}` }); const chip = el("span", { class: `search-chip search-chip--${type}` });
const label = el("span", { class: "search-chip__label" }); const label = el("span", { class: "search-chip__label" });
label.textContent = fullOperator; label.textContent = fullOperator;
const removeBtn = el("button", { class: "search-chi const removeBtn = el("button", { class: "search-chip__remove", title: "Retirer ce filtre", type: "button" });
removeBtn.innerHTML = '<i data-lucide="x" style="width:10px;height:10px"></i>';
removeBtn.addEventListener("click", () => {
// Remove this operator from the input
const input = document.getElementById("search-input");
const raw = input.value;
// Remove the operator text from the query
const escaped = fullOperator.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
input.value = raw.replace(new RegExp("\\s*" + escaped + "\\s*", "i"), " ").trim();
_triggerAdvancedSearch(input.value);
});
chip.appendChild(label);
chip.appendChild(removeBtn);
this._container.appendChild(chip);
safeCreateIcons();
},
};
... [OUTPUT TRUNCATED - 3180 chars omitted out of 53180 total] ... // ---------------------------------------------------------------------------
// Helper: trigger advanced search from input value
// ---------------------------------------------------------------------------
function _triggerAdvancedSearch(rawQuery) {
const q = (rawQuery || "").trim();
const vault = document.getElementById("vault-filter").value;
const tagFilter = selectedTags.length > 0 ? selectedTags.join(",") : null;
advancedSearchOffset = 0;
if (q.length > 0 || tagFilter) {
SearchHistory.add(q);
performAdvancedSearch(q, vault, tagFilter);
} else {
SearchChips.clear();
showWelcome();
}
}
search(); }); // ---------------------------------------------------------------------------
if (wordBtn) wordBtn.addEventListener("click", () => { state.searchWholeWord = !state.searchWholeWord; _updateToggleUI(); _research(); }); // Search (enhanced with autocomplete, keyboard nav, global shortcuts)
if (regexBtn) regexBtn.addEventListener("click", () => { state.searchRegex = !state.searchRegex; _updateToggleUI(); _research(); }); // ---------------------------------------------------------------------------
if (filterBtn) filterBtn.addEventListener("click", () => { state.searchFilterVisible = !state.searchFilterVisible; if (filterRow) filterRow.style.display = state.searchFilterVisible ? "flex" : "none"; _updateToggleUI(); }); // ── Search toggle state ──
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) ── // ── Result navigation (up/down arrows + Enter) ──
let _searchResultIdx = -1; let _searchResultIdx = -1;
@@ -486,11 +536,11 @@ search(); });
function _research() { function _research() {
const q = input.value.trim(); const q = input.value.trim();
if (q.length >= _getEffective("min_query_length", 2)) { if (q.length >= _getEffective("min_query_length", 2)) {
clearTimeout(state.searchTimeout); clearTimeout(searchTimeout);
state.searchTimeout = setTimeout(() => { searchTimeout = setTimeout(() => {
const vault = document.getElementById("vault-filter").value; const vault = document.getElementById("vault-filter").value;
const tagFilter = state.selectedTags.length > 0 ? state.selectedTags.join(",") : null; const tagFilter = selectedTags.length > 0 ? selectedTags.join(",") : null;
state.advancedSearchOffset = 0; advancedSearchOffset = 0;
performAdvancedSearch(q, vault, tagFilter); performAdvancedSearch(q, vault, tagFilter);
}, _getEffective("debounce_ms", 300)); }, _getEffective("debounce_ms", 300));
} }
@@ -522,14 +572,14 @@ search(); });
AutocompleteDropdown.populate(input.value, input.selectionStart); AutocompleteDropdown.populate(input.value, input.selectionStart);
// Debounced search execution // Debounced search execution
clearTimeout(state.searchTimeout); clearTimeout(searchTimeout);
state.searchTimeout = setTimeout( searchTimeout = setTimeout(
() => { () => {
const q = input.value.trim(); const q = input.value.trim();
const vault = document.getElementById("vault-filter").value; const vault = document.getElementById("vault-filter").value;
const tagFilter = state.selectedTags.length > 0 ? state.selectedTags.join(",") : null; const tagFilter = selectedTags.length > 0 ? selectedTags.join(",") : null;
state.advancedSearchOffset = 0; advancedSearchOffset = 0;
if (q.length >= _getEffective("min_query_length", state.MIN_SEARCH_LENGTH) || tagFilter) { if (q.length >= _getEffective("min_query_length", MIN_SEARCH_LENGTH) || tagFilter) {
performAdvancedSearch(q, vault, tagFilter); performAdvancedSearch(q, vault, tagFilter);
} else if (q.length === 0) { } else if (q.length === 0) {
SearchChips.clear(); SearchChips.clear();
@@ -580,10 +630,10 @@ search(); });
const q = input.value.trim(); const q = input.value.trim();
if (q) { if (q) {
SearchHistory.add(q); SearchHistory.add(q);
clearTimeout(state.searchTimeout); clearTimeout(searchTimeout);
state.advancedSearchOffset = 0; advancedSearchOffset = 0;
const vault = document.getElementById("vault-filter").value; const vault = document.getElementById("vault-filter").value;
const tagFilter = state.selectedTags.length > 0 ? state.selectedTags.join(",") : null; const tagFilter = selectedTags.length > 0 ? selectedTags.join(",") : null;
performAdvancedSearch(q, vault, tagFilter); performAdvancedSearch(q, vault, tagFilter);
} }
e.preventDefault(); e.preventDefault();
@@ -604,10 +654,10 @@ search(); });
const q = input.value.trim(); const q = input.value.trim();
if (q) { if (q) {
SearchHistory.add(q); SearchHistory.add(q);
clearTimeout(state.searchTimeout); clearTimeout(searchTimeout);
state.advancedSearchOffset = 0; advancedSearchOffset = 0;
const vault = document.getElementById("vault-filter").value; const vault = document.getElementById("vault-filter").value;
const tagFilter = state.selectedTags.length > 0 ? state.selectedTags.join(",") : null; const tagFilter = selectedTags.length > 0 ? selectedTags.join(",") : null;
performAdvancedSearch(q, vault, tagFilter); performAdvancedSearch(q, vault, tagFilter);
} }
e.preventDefault(); e.preventDefault();
@@ -617,9 +667,9 @@ search(); });
clearBtn.addEventListener("click", () => { clearBtn.addEventListener("click", () => {
input.value = ""; input.value = "";
if (clearBtn) clearBtn.style.display = "none"; if (clearBtn) clearBtn.style.display = "none";
state.searchCaseSensitive = false; searchCaseSensitive = false;
state.searchWholeWord = false; searchWholeWord = false;
state.searchRegex = false; searchRegex = false;
_updateToggleUI(); _updateToggleUI();
SearchChips.clear(); SearchChips.clear();
AutocompleteDropdown.hide(); AutocompleteDropdown.hide();
@@ -655,48 +705,48 @@ function _isInputFocused() {
} }
// --- Backward-compatible search (existing /api/search endpoint) --- // --- Backward-compatible search (existing /api/search endpoint) ---
export async function performSearch(query, vaultFilter, tagFilter) { async function performSearch(query, vaultFilter, tagFilter) {
if (state.searchAbortController) state.searchAbortController.abort(); if (searchAbortController) searchAbortController.abort();
state.searchAbortController = new AbortController(); searchAbortController = new AbortController();
const searchId = ++state.currentSearchId; const searchId = ++currentSearchId;
showLoading(); showLoading();
let url = `/api/search?q=${encodeURIComponent(query)}&vault=${encodeURIComponent(vaultFilter)}`; let url = `/api/search?q=${encodeURIComponent(query)}&vault=${encodeURIComponent(vaultFilter)}`;
if (tagFilter) url += `&tag=${encodeURIComponent(tagFilter)}`; if (tagFilter) url += `&tag=${encodeURIComponent(tagFilter)}`;
try { try {
const data = await api(url, { signal: state.searchAbortController.signal }); const data = await api(url, { signal: searchAbortController.signal });
if (searchId !== state.currentSearchId) return; if (searchId !== currentSearchId) return;
renderSearchResults(data, query, tagFilter); renderSearchResults(data, query, tagFilter);
} catch (err) { } catch (err) {
if (err.name === "AbortError") return; if (err.name === "AbortError") return;
if (searchId !== state.currentSearchId) return; if (searchId !== currentSearchId) return;
showWelcome(); showWelcome();
} finally { } finally {
hideProgressBar(); hideProgressBar();
if (searchId === state.currentSearchId) state.searchAbortController = null; if (searchId === currentSearchId) searchAbortController = null;
} }
} }
// --- Advanced search with TF-IDF, facets, pagination --- // --- Advanced search with TF-IDF, facets, pagination ---
export async function performAdvancedSearch(query, vaultFilter, tagFilter, offset, sort) { async function performAdvancedSearch(query, vaultFilter, tagFilter, offset, sort) {
if (state.searchAbortController) state.searchAbortController.abort(); if (searchAbortController) searchAbortController.abort();
state.searchAbortController = new AbortController(); searchAbortController = new AbortController();
const searchId = ++state.currentSearchId; const searchId = ++currentSearchId;
showLoading(); showLoading();
const ofs = offset !== undefined ? offset : state.advancedSearchOffset; const ofs = offset !== undefined ? offset : advancedSearchOffset;
const sortBy = sort || state.advancedSearchSort; const sortBy = sort || advancedSearchSort;
state.advancedSearchLastQuery = query; advancedSearchLastQuery = query;
// Update chips from parsed query // Update chips from parsed query
const parsed = QueryParser.parse(query); const parsed = QueryParser.parse(query);
SearchChips.update(parsed); SearchChips.update(parsed);
const effectiveLimit = _getEffective("results_per_page", state.ADVANCED_SEARCH_LIMIT); const effectiveLimit = _getEffective("results_per_page", ADVANCED_SEARCH_LIMIT);
let url = `/api/search/advanced?q=${encodeURIComponent(query)}&vault=${encodeURIComponent(vaultFilter)}&limit=${effectiveLimit}&offset=${ofs}&sort=${sortBy}`; let url = `/api/search/advanced?q=${encodeURIComponent(query)}&vault=${encodeURIComponent(vaultFilter)}&limit=${effectiveLimit}&offset=${ofs}&sort=${sortBy}`;
if (tagFilter) url += `&tag=${encodeURIComponent(tagFilter)}`; if (tagFilter) url += `&tag=${encodeURIComponent(tagFilter)}`;
if (state.searchCaseSensitive) url += "&case_sensitive=true"; if (searchCaseSensitive) url += "&case_sensitive=true";
if (state.searchWholeWord) url += "&whole_word=true"; if (searchWholeWord) url += "&whole_word=true";
if (state.searchRegex) url += "&regex=true"; if (searchRegex) url += "&regex=true";
const includeEl = document.getElementById("search-include-input"); const includeEl = document.getElementById("search-include-input");
const excludeEl = document.getElementById("search-exclude-input"); const excludeEl = document.getElementById("search-exclude-input");
if (includeEl?.value.trim()) url += `&include_paths=${encodeURIComponent(includeEl.value.trim())}`; if (includeEl?.value.trim()) url += `&include_paths=${encodeURIComponent(includeEl.value.trim())}`;
@@ -705,31 +755,31 @@ export async function performAdvancedSearch(query, vaultFilter, tagFilter, offse
// Search timeout — abort if server takes too long // Search timeout — abort if server takes too long
const timeoutId = setTimeout( const timeoutId = setTimeout(
() => { () => {
if (state.searchAbortController) state.searchAbortController.abort(); if (searchAbortController) searchAbortController.abort();
}, },
_getEffective("search_timeout_ms", state.SEARCH_TIMEOUT_MS), _getEffective("search_timeout_ms", SEARCH_TIMEOUT_MS),
); );
try { try {
const data = await api(url, { signal: state.searchAbortController.signal }); const data = await api(url, { signal: searchAbortController.signal });
clearTimeout(timeoutId); clearTimeout(timeoutId);
if (searchId !== state.currentSearchId) return; if (searchId !== currentSearchId) return;
state.advancedSearchTotal = data.total; advancedSearchTotal = data.total;
state.advancedSearchOffset = ofs; advancedSearchOffset = ofs;
renderAdvancedSearchResults(data, query, tagFilter); renderAdvancedSearchResults(data, query, tagFilter);
} catch (err) { } catch (err) {
clearTimeout(timeoutId); clearTimeout(timeoutId);
if (err.name === "AbortError") return; if (err.name === "AbortError") return;
if (searchId !== state.currentSearchId) return; if (searchId !== currentSearchId) return;
showWelcome(); showWelcome();
} finally { } finally {
hideProgressBar(); hideProgressBar();
if (searchId === state.currentSearchId) state.searchAbortController = null; if (searchId === currentSearchId) searchAbortController = null;
} }
} }
// --- Legacy search results renderer (kept for backward compat) --- // --- Legacy search results renderer (kept for backward compat) ---
export function renderSearchResults(data, query, tagFilter) { function renderSearchResults(data, query, tagFilter) {
const area = document.getElementById("content-area"); const area = document.getElementById("content-area");
area.innerHTML = ""; area.innerHTML = "";
const header = buildSearchResultsHeader(data, query, tagFilter); const header = buildSearchResultsHeader(data, query, tagFilter);
@@ -747,7 +797,7 @@ export function renderSearchResults(data, query, tagFilter) {
const titleDiv = el("div", { class: "search-result-title" }); const titleDiv = el("div", { class: "search-result-title" });
if (query && query.trim()) { if (query && query.trim()) {
highlightSearchText(titleDiv, r.title, query, state.searchCaseSensitive); highlightSearchText(titleDiv, r.title, query, searchCaseSensitive);
} else { } else {
titleDiv.textContent = r.title; titleDiv.textContent = r.title;
} }
@@ -755,7 +805,7 @@ export function renderSearchResults(data, query, tagFilter) {
if (r.snippet && r.snippet.includes("<mark>")) { if (r.snippet && r.snippet.includes("<mark>")) {
snippetDiv.innerHTML = r.snippet; snippetDiv.innerHTML = r.snippet;
} else if (query && query.trim() && r.snippet) { } else if (query && query.trim() && r.snippet) {
highlightSearchText(snippetDiv, r.snippet, query, state.searchCaseSensitive); highlightSearchText(snippetDiv, r.snippet, query, searchCaseSensitive);
} else { } else {
snippetDiv.textContent = r.snippet || ""; snippetDiv.textContent = r.snippet || "";
} }
@@ -787,7 +837,7 @@ export function renderSearchResults(data, query, tagFilter) {
} }
// --- Advanced search results renderer (facets, highlighted snippets, pagination, sort) --- // --- Advanced search results renderer (facets, highlighted snippets, pagination, sort) ---
export function renderAdvancedSearchResults(data, query, tagFilter) { function renderAdvancedSearchResults(data, query, tagFilter) {
const area = document.getElementById("content-area"); const area = document.getElementById("content-area");
area.innerHTML = ""; area.innerHTML = "";
@@ -819,9 +869,9 @@ export function renderAdvancedSearchResults(data, query, tagFilter) {
// Active filter badges // Active filter badges
const filtersRow = el("div", { class: "search-filters-row" }); const filtersRow = el("div", { class: "search-filters-row" });
if (state.searchCaseSensitive) filtersRow.appendChild(el("span", { class: "search-filter-badge" }, [document.createTextNode("Aa")])); if (searchCaseSensitive) filtersRow.appendChild(el("span", { class: "search-filter-badge" }, [document.createTextNode("Aa")]));
if (state.searchWholeWord) filtersRow.appendChild(el("span", { class: "search-filter-badge" }, [document.createTextNode("wd")])); if (searchWholeWord) filtersRow.appendChild(el("span", { class: "search-filter-badge" }, [document.createTextNode("wd")]));
if (state.searchRegex) filtersRow.appendChild(el("span", { class: "search-filter-badge" }, [document.createTextNode(".*")])); if (searchRegex) filtersRow.appendChild(el("span", { class: "search-filter-badge" }, [document.createTextNode(".*")]));
const inclEl = document.getElementById("search-include-input"); const inclEl = document.getElementById("search-include-input");
const exclEl = document.getElementById("search-exclude-input"); const exclEl = document.getElementById("search-exclude-input");
if (inclEl?.value.trim()) filtersRow.appendChild(el("span", { class: "search-filter-badge path" }, [document.createTextNode("incl: " + inclEl.value.trim())])); if (inclEl?.value.trim()) filtersRow.appendChild(el("span", { class: "search-filter-badge path" }, [document.createTextNode("incl: " + inclEl.value.trim())]));
@@ -830,19 +880,19 @@ export function renderAdvancedSearchResults(data, query, tagFilter) {
// Sort controls // Sort controls
const sortDiv = el("div", { class: "search-sort" }); const sortDiv = el("div", { class: "search-sort" });
const btnRelevance = el("button", { class: "search-sort__btn" + (state.advancedSearchSort === "relevance" ? " active" : ""), type: "button" }); const btnRelevance = el("button", { class: "search-sort__btn" + (advancedSearchSort === "relevance" ? " active" : ""), type: "button" });
btnRelevance.textContent = "Pertinence"; btnRelevance.textContent = "Pertinence";
btnRelevance.addEventListener("click", () => { btnRelevance.addEventListener("click", () => {
state.advancedSearchSort = "relevance"; advancedSearchSort = "relevance";
state.advancedSearchOffset = 0; advancedSearchOffset = 0;
const vault = document.getElementById("vault-filter").value; const vault = document.getElementById("vault-filter").value;
performAdvancedSearch(query, vault, tagFilter, 0, "relevance"); performAdvancedSearch(query, vault, tagFilter, 0, "relevance");
}); });
const btnDate = el("button", { class: "search-sort__btn" + (state.advancedSearchSort === "modified" ? " active" : ""), type: "button" }); const btnDate = el("button", { class: "search-sort__btn" + (advancedSearchSort === "modified" ? " active" : ""), type: "button" });
btnDate.textContent = "Date"; btnDate.textContent = "Date";
btnDate.addEventListener("click", () => { btnDate.addEventListener("click", () => {
state.advancedSearchSort = "modified"; advancedSearchSort = "modified";
state.advancedSearchOffset = 0; advancedSearchOffset = 0;
const vault = document.getElementById("vault-filter").value; const vault = document.getElementById("vault-filter").value;
performAdvancedSearch(query, vault, tagFilter, 0, "modified"); performAdvancedSearch(query, vault, tagFilter, 0, "modified");
}); });
@@ -863,9 +913,9 @@ export function renderAdvancedSearchResults(data, query, tagFilter) {
body: JSON.stringify({ body: JSON.stringify({
query: query, query: query,
vault: document.getElementById("vault-filter")?.value || "all", vault: document.getElementById("vault-filter")?.value || "all",
case_sensitive: state.searchCaseSensitive, case_sensitive: searchCaseSensitive,
whole_word: state.searchWholeWord, whole_word: searchWholeWord,
regex: state.searchRegex, regex: searchRegex,
include_paths: inclEl?.value || "", include_paths: inclEl?.value || "",
exclude_paths: exclEl?.value || "", exclude_paths: exclEl?.value || "",
}), }),
@@ -877,9 +927,9 @@ export function renderAdvancedSearchResults(data, query, tagFilter) {
area.appendChild(header); area.appendChild(header);
// Active sidebar tag chips // Active sidebar tag chips
if (state.selectedTags.length > 0) { if (selectedTags.length > 0) {
const activeTags = el("div", { class: "search-results-active-tags" }); const activeTags = el("div", { class: "search-results-active-tags" });
state.selectedTags.forEach((tag) => { selectedTags.forEach((tag) => {
const removeBtn = el( const removeBtn = el(
"button", "button",
{ {
@@ -962,7 +1012,7 @@ export function renderAdvancedSearchResults(data, query, tagFilter) {
const titleDiv = el("div", { class: "search-result-title" }); const titleDiv = el("div", { class: "search-result-title" });
if (freeText) { if (freeText) {
highlightSearchText(titleDiv, r.title, freeText, state.searchCaseSensitive); highlightSearchText(titleDiv, r.title, freeText, searchCaseSensitive);
} else { } else {
titleDiv.textContent = r.title; titleDiv.textContent = r.title;
} }
@@ -972,7 +1022,7 @@ export function renderAdvancedSearchResults(data, query, tagFilter) {
if (r.snippet && r.snippet.includes("<mark>")) { if (r.snippet && r.snippet.includes("<mark>")) {
snippetDiv.innerHTML = r.snippet; snippetDiv.innerHTML = r.snippet;
} else if (freeText && r.snippet) { } else if (freeText && r.snippet) {
highlightSearchText(snippetDiv, r.snippet, freeText, state.searchCaseSensitive); highlightSearchText(snippetDiv, r.snippet, freeText, searchCaseSensitive);
} else { } else {
snippetDiv.textContent = r.snippet || ""; snippetDiv.textContent = r.snippet || "";
} }
@@ -1010,30 +1060,30 @@ export function renderAdvancedSearchResults(data, query, tagFilter) {
area.appendChild(container); area.appendChild(container);
// Pagination // Pagination
if (data.total > state.ADVANCED_SEARCH_LIMIT) { if (data.total > ADVANCED_SEARCH_LIMIT) {
const paginationDiv = el("div", { class: "search-pagination" }); const paginationDiv = el("div", { class: "search-pagination" });
const prevBtn = el("button", { class: "search-pagination__btn", type: "button" }); const prevBtn = el("button", { class: "search-pagination__btn", type: "button" });
prevBtn.textContent = "← Précédent"; prevBtn.textContent = "← Précédent";
prevBtn.disabled = state.advancedSearchOffset === 0; prevBtn.disabled = advancedSearchOffset === 0;
prevBtn.addEventListener("click", () => { prevBtn.addEventListener("click", () => {
state.advancedSearchOffset = Math.max(0, state.advancedSearchOffset - state.ADVANCED_SEARCH_LIMIT); advancedSearchOffset = Math.max(0, advancedSearchOffset - ADVANCED_SEARCH_LIMIT);
const vault = document.getElementById("vault-filter").value; const vault = document.getElementById("vault-filter").value;
performAdvancedSearch(query, vault, tagFilter, state.advancedSearchOffset); performAdvancedSearch(query, vault, tagFilter, advancedSearchOffset);
document.getElementById("content-area").scrollTop = 0; document.getElementById("content-area").scrollTop = 0;
}); });
const info = el("span", { class: "search-pagination__info" }); const info = el("span", { class: "search-pagination__info" });
const from = state.advancedSearchOffset + 1; const from = advancedSearchOffset + 1;
const to = Math.min(state.advancedSearchOffset + state.ADVANCED_SEARCH_LIMIT, data.total); const to = Math.min(advancedSearchOffset + ADVANCED_SEARCH_LIMIT, data.total);
info.textContent = `${from}–${to} sur ${data.total}`; info.textContent = `${from}–${to} sur ${data.total}`;
const nextBtn = el("button", { class: "search-pagination__btn", type: "button" }); const nextBtn = el("button", { class: "search-pagination__btn", type: "button" });
nextBtn.textContent = "Suivant →"; nextBtn.textContent = "Suivant →";
nextBtn.disabled = state.advancedSearchOffset + state.ADVANCED_SEARCH_LIMIT >= data.total; nextBtn.disabled = advancedSearchOffset + ADVANCED_SEARCH_LIMIT >= data.total;
nextBtn.addEventListener("click", () => { nextBtn.addEventListener("click", () => {
state.advancedSearchOffset += state.ADVANCED_SEARCH_LIMIT; advancedSearchOffset += ADVANCED_SEARCH_LIMIT;
const vault = document.getElementById("vault-filter").value; const vault = document.getElementById("vault-filter").value;
performAdvancedSearch(query, vault, tagFilter, state.advancedSearchOffset); performAdvancedSearch(query, vault, tagFilter, advancedSearchOffset);
document.getElementById("content-area").scrollTop = 0; document.getElementById("content-area").scrollTop = 0;
}); });
+484 -325
View File
@@ -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 { state } from './state.js';
import { openFile } from './viewer.js';
import { safeCreateIcons } from './utils.js';
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Right Sidebar Manager // Right Sidebar Manager
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export const RightSidebarManager = { const RightSidebarManager = {
init() { init() {
this.loadState(); this.loadState();
this.initToggle(); this.initToggle();
@@ -19,11 +16,11 @@ export const RightSidebarManager = {
const savedWidth = localStorage.getItem("obsigate-right-sidebar-width"); const savedWidth = localStorage.getItem("obsigate-right-sidebar-width");
if (savedVisible !== null) { if (savedVisible !== null) {
state.rightSidebarVisible = savedVisible === "true"; rightSidebarVisible = savedVisible === "true";
} }
if (savedWidth) { if (savedWidth) {
state.rightSidebarWidth = parseInt(savedWidth) || 280; rightSidebarWidth = parseInt(savedWidth) || 280;
} }
this.applyState(); this.applyState();
@@ -37,9 +34,9 @@ export const RightSidebarManager = {
if (!sidebar) return; if (!sidebar) return;
if (state.rightSidebarVisible) { if (rightSidebarVisible) {
sidebar.classList.remove("hidden"); sidebar.classList.remove("hidden");
sidebar.style.width = `${state.rightSidebarWidth}px`; sidebar.style.width = `${rightSidebarWidth}px`;
if (handle) handle.classList.remove("hidden"); if (handle) handle.classList.remove("hidden");
if (tocBtn) { if (tocBtn) {
tocBtn.classList.add("active"); tocBtn.classList.add("active");
@@ -67,8 +64,8 @@ export const RightSidebarManager = {
}, },
toggle() { toggle() {
state.rightSidebarVisible = !state.rightSidebarVisible; rightSidebarVisible = !rightSidebarVisible;
localStorage.setItem("obsigate-right-sidebar-visible", state.rightSidebarVisible); localStorage.setItem("obsigate-right-sidebar-visible", rightSidebarVisible);
this.applyState(); this.applyState();
}, },
@@ -108,7 +105,7 @@ export const RightSidebarManager = {
newWidth = Math.max(200, Math.min(400, newWidth)); newWidth = Math.max(200, Math.min(400, newWidth));
sidebar.style.width = `${newWidth}px`; sidebar.style.width = `${newWidth}px`;
state.rightSidebarWidth = newWidth; rightSidebarWidth = newWidth;
}; };
const onMouseUp = () => { const onMouseUp = () => {
@@ -119,7 +116,7 @@ export const RightSidebarManager = {
document.body.style.cursor = ""; document.body.style.cursor = "";
document.body.style.userSelect = ""; document.body.style.userSelect = "";
localStorage.setItem("obsigate-right-sidebar-width", state.rightSidebarWidth); localStorage.setItem("obsigate-right-sidebar-width", rightSidebarWidth);
}; };
handle.addEventListener("mousedown", onMouseDown); handle.addEventListener("mousedown", onMouseDown);
@@ -131,12 +128,12 @@ export const RightSidebarManager = {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Theme // Theme
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export function initTheme() { function initTheme() {
const saved = localStorage.getItem("obsigate-theme") || "dark"; const saved = localStorage.getItem("obsigate-theme") || "dark";
applyTheme(saved); applyTheme(saved);
} }
export function applyTheme(theme) { function applyTheme(theme) {
document.documentElement.setAttribute("data-theme", theme); document.documentElement.setAttribute("data-theme", theme);
localStorage.setItem("obsigate-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"); const current = document.documentElement.getAttribute("data-theme");
applyTheme(current === "dark" ? "light" : "dark"); applyTheme(current === "dark" ? "light" : "dark");
} }
export function initHeaderMenu() { function initHeaderMenu() {
const menuBtn = document.getElementById("header-menu-btn"); const menuBtn = document.getElementById("header-menu-btn");
const menuDropdown = document.getElementById("header-menu-dropdown"); const menuDropdown = document.getElementById("header-menu-dropdown");
@@ -203,7 +200,7 @@ function closeHeaderMenu() {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Custom Dropdowns // Custom Dropdowns
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export function initCustomDropdowns() { function initCustomDropdowns() {
document.querySelectorAll(".custom-dropdown").forEach((dropdown) => { document.querySelectorAll(".custom-dropdown").forEach((dropdown) => {
const trigger = dropdown.querySelector(".custom-dropdown-trigger"); const trigger = dropdown.querySelector(".custom-dropdown-trigger");
const options = dropdown.querySelectorAll(".custom-dropdown-option"); 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. */ /** 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); console.log("showToast called with:", message, type);
type = type || "info"; type = type || "info";
let container = document.getElementById("toast-container"); let container = document.getElementById("toast-container");
@@ -365,7 +362,7 @@ export function showToast(message, type) {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Sidebar toggle (desktop) // Sidebar toggle (desktop)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export function initSidebarToggle() { function initSidebarToggle() {
const toggleBtn = document.getElementById("sidebar-toggle-btn"); const toggleBtn = document.getElementById("sidebar-toggle-btn");
const sidebar = document.getElementById("sidebar"); const sidebar = document.getElementById("sidebar");
const resizeHandle = document.getElementById("sidebar-resize-handle"); const resizeHandle = document.getElementById("sidebar-resize-handle");
@@ -391,7 +388,7 @@ export function initSidebarToggle() {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Mobile sidebar // Mobile sidebar
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export function initMobile() { function initMobile() {
const hamburger = document.getElementById("hamburger-btn"); const hamburger = document.getElementById("hamburger-btn");
const overlay = document.getElementById("sidebar-overlay"); const overlay = document.getElementById("sidebar-overlay");
const sidebar = document.getElementById("sidebar"); const sidebar = document.getElementById("sidebar");
@@ -417,7 +414,7 @@ function closeMobileSidebar() {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Resizable sidebar (horizontal) // Resizable sidebar (horizontal)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export function initSidebarResize() { function initSidebarResize() {
const handle = document.getElementById("sidebar-resize-handle"); const handle = document.getElementById("sidebar-resize-handle");
const sidebar = document.getElementById("sidebar"); const sidebar = document.getElementById("sidebar");
if (!handle || !sidebar) return; if (!handle || !sidebar) return;
@@ -458,7 +455,7 @@ export function initSidebarResize() {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Resizable tag section (vertical) // Resizable tag section (vertical)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export function initTagResize() { function initTagResize() {
const handle = document.getElementById("tag-resize-handle"); const handle = document.getElementById("tag-resize-handle");
const tagSection = document.getElementById("tag-cloud-section"); const tagSection = document.getElementById("tag-cloud-section");
if (!handle || !tagSection) return; if (!handle || !tagSection) return;
@@ -489,11 +486,374 @@ export function initTagResize() {
handle.addEventListener("mousedown", (e) => { handle.addEventListener("mousedown", (e) => {
e.preventDefault(); e.preventDefault();
startY = e.clientY; 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> <i data-lucide="alert-triangle" class="icon"></i>
<div> <div>
<strong>Attention !</strong> Cette action est irréversible. <strong>Attention !</strong> Cette action est irréversible.
@@ -532,7 +892,7 @@ export function initTagResize() {
this._closeModal(overlay); this._closeModal(overlay);
await refreshSidebarTreePreservingState(); await refreshSidebarTreePreservingState();
if (state.currentVault === vault && state.currentPath === path) { if (currentVault === vault && currentPath === path) {
showWelcome(); showWelcome();
} }
} catch (err) { } catch (err) {
@@ -566,7 +926,7 @@ export function initTagResize() {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Find in Page Manager // Find in Page Manager
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export const FindInPageManager = { const FindInPageManager = {
isOpen: false, isOpen: false,
searchTerm: "", searchTerm: "",
matches: [], matches: [],
@@ -985,313 +1345,112 @@ export const FindInPageManager = {
}; };
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Tab Manager // Init
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export const TabManager = { async function init() {
_tabs: [], initTheme();
_activeTabId: null, initHeaderMenu();
_previewTabId: null, // single-click preview tab (temporary, replaced on next preview) initCustomDropdowns();
_tabCache: {}, // { tabId: { vault, path, title, data, rawSource, sourceView, scrollTop, icon } } document.getElementById("theme-toggle").addEventListener("click", toggleTheme);
_tabBar: null, document.getElementById("header-logo").addEventListener("click", goHome);
_tabList: null, const refreshBtn = document.getElementById("header-refresh-btn");
_dirtyTabs: new Set(), 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() { // Check auth status first
this._tabBar = document.getElementById("tab-bar"); const authOk = await AuthManager.initAuth();
this._tabList = document.getElementById("tab-list");
},
/** Open a file as a preview tab (single-click). if (authOk) {
* Replaces any existing preview tab. If the file is already // Start SSE sync AFTER auth is established (cookie available)
* open as a persistent tab, just activates it. */ initSyncStatus();
async openPreview(vault, path) {
const tabId = `${vault}::${path}`;
// If already open as persistent tab, just activate it try {
const existing = this._tabs.find(t => t.id === tabId && !t.preview); await Promise.all([loadVaultSettings(), loadVaults(), loadTags()]);
if (existing) {
this.activate(tabId);
return;
}
// Close existing preview tab // Initialize dashboard widgets now that vaults are loaded
if (this._previewTabId && this._previewTabId !== tabId) { if (typeof DashboardRecentWidget !== "undefined") {
this.close(this._previewTabId); DashboardRecentWidget.init();
}
// If already open as preview, just focus it
const previewExisting = this._tabs.find(t => t.id === tabId && t.preview);
if (previewExisting) {
this.activate(tabId);
return;
}
// Create preview tab
const name = path.split("/").pop().replace(/\.md$/i, "");
const icon = getFileIcon(name + ".md");
this._tabs.push({ id: tabId, vault, path, name, icon, preview: true });
this._tabCache[tabId] = { vault, path, title: name, data: null, rawSource: null, sourceView: false, scrollTop: 0, icon };
this._previewTabId = tabId;
this._renderTabs();
this.activate(tabId);
},
/** Convert a preview tab to a persistent tab (double-click).
* If already persistent, opens a new duplicate (same file, different tab). */
async openPersistent(vault, path) {
const tabId = `${vault}::${path}`;
// If it's already a preview tab, convert it to persistent
const previewTab = this._tabs.find(t => t.id === tabId && t.preview);
if (previewTab) {
previewTab.preview = false;
if (this._previewTabId === tabId) {
this._previewTabId = null;
} }
this._renderTabs();
this.activate(tabId);
return;
}
// If already persistent, just focus it // Check for popup mode query parameter
const existing = this._tabs.find(t => t.id === tabId && !t.preview); const urlParams = new URLSearchParams(window.location.search);
if (existing) { if (urlParams.get("popup") === "true") {
this.activate(tabId); document.body.classList.add("popup-mode");
return;
}
// Create a new persistent tab
this.open(vault, path);
},
/** Open a file in a tab (or focus existing) */
async open(vault, path, options = {}) {
const tabId = `${vault}::${path}`;
// If already open, just focus it
const existing = this._tabs.find(t => t.id === tabId);
if (existing) {
// Convert preview to persistent if needed
if (existing.preview) {
existing.preview = false;
if (this._previewTabId === tabId) this._previewTabId = null;
this._renderTabs();
} }
this.activate(tabId);
return;
}
// Create new tab // Handle direct deep-link to file via #file=...
const name = path.split("/").pop().replace(/\.md$/i, ""); if (window.location.hash && window.location.hash.startsWith("#file=")) {
const icon = getFileIcon(name + ".md"); const hashVal = window.location.hash.substring(6);
const sepIndex = hashVal.indexOf(":");
this._tabs.push({ id: tabId, vault, path, name, icon }); if (sepIndex > -1) {
this._tabCache[tabId] = { vault, path, title: name, data: null, rawSource: null, sourceView: false, scrollTop: 0, icon }; const vault = decodeURIComponent(hashVal.substring(0, sepIndex));
const path = decodeURIComponent(hashVal.substring(sepIndex + 1));
this._renderTabs(); openFile(vault, path);
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);
} }
if (cache.scrollTop) { } else if (urlParams.get("popup") !== "true") {
area.scrollTop = cache.scrollTop; // Default to dashboard if no deep link and not in popup mode
} showWelcome();
} catch (err) { }
area.innerHTML = `<div style="padding:40px;text-align:center;color:var(--text-error)">Erreur: ${escapeHtml(err.message)}</div>`; } 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 // ---- Modify init to include TabManager ----
if (history.pushState) { const _origInit2 = init;
history.pushState(null, "", `#/file/${encodeURIComponent(cache.vault)}/${encodeURIComponent(cache.path)}`); init = function() {
} _origInit2();
TabManager.init();
// 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 */
+216 -85
View File
@@ -1,9 +1,5 @@
/* ObsiGate — Viewer module: Outline, ScrollSpy, ReadingProgress, file viewer, frontmatter card, editor init */ /* ObsiGate — Auto-extracted module */
import { state } from './state.js'; import { state } from './state.js';
import { escapeHtml, safeCreateIcons, safeHighlight, getFileIcon } from "./utils.js";
// initEditor is defined in utils.js — re-exported below.
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Outline/TOC Manager // Outline/TOC Manager
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -118,7 +114,7 @@ const OutlineManager = {
outlineList.appendChild(item); outlineList.appendChild(item);
}); });
state.headingsCache = headings; headingsCache = headings;
}, },
/** /**
@@ -148,9 +144,9 @@ const OutlineManager = {
* Set active heading in outline * Set active heading in outline
*/ */
setActiveHeading(headingId) { setActiveHeading(headingId) {
if (state.activeHeadingId === headingId) return; if (activeHeadingId === headingId) return;
state.activeHeadingId = headingId; activeHeadingId = headingId;
const items = document.querySelectorAll(".outline-item"); const items = document.querySelectorAll(".outline-item");
items.forEach((item) => { items.forEach((item) => {
@@ -182,12 +178,11 @@ const OutlineManager = {
destroy() { destroy() {
ScrollSpyManager.destroy(); ScrollSpyManager.destroy();
ReadingProgressManager.destroy(); ReadingProgressManager.destroy();
state.headingsCache = []; headingsCache = [];
state.activeHeadingId = null; activeHeadingId = null;
}, },
}; };
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Scroll Spy Manager // Scroll Spy Manager
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -245,7 +240,6 @@ const ScrollSpyManager = {
}, },
}; };
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Reading Progress Manager // Reading Progress Manager
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -311,15 +305,14 @@ const ReadingProgressManager = {
}, },
}; };
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// File viewer // File viewer
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
async function openFile(vaultName, filePath) { async function openFile(vaultName, filePath) {
state.currentVault = vaultName; currentVault = vaultName;
state.currentPath = filePath; currentPath = filePath;
state.showingSource = false; showingSource = false;
state.cachedRawSource = null; cachedRawSource = null;
// Highlight active // Highlight active
syncActiveFileTreeItem(vaultName, filePath); syncActiveFileTreeItem(vaultName, filePath);
@@ -442,12 +435,12 @@ function renderFile(data) {
copyBtn.addEventListener("click", async () => { copyBtn.addEventListener("click", async () => {
try { try {
// Fetch raw content if not already cached // Fetch raw content if not already cached
if (!state.cachedRawSource) { if (!cachedRawSource) {
const rawUrl = `/api/file/${encodeURIComponent(data.vault)}/raw?path=${encodeURIComponent(data.path)}`; const rawUrl = `/api/file/${encodeURIComponent(data.vault)}/raw?path=${encodeURIComponent(data.path)}`;
const rawData = await api(rawUrl); const rawData = await api(rawUrl);
state.cachedRawSource = rawData.raw; cachedRawSource = rawData.raw;
} }
await navigator.clipboard.writeText(state.cachedRawSource); await navigator.clipboard.writeText(cachedRawSource);
copyBtn.lastChild.textContent = "Copié !"; copyBtn.lastChild.textContent = "Copié !";
setTimeout(() => (copyBtn.lastChild.textContent = "Copier"), 1500); setTimeout(() => (copyBtn.lastChild.textContent = "Copier"), 1500);
} catch (err) { } catch (err) {
@@ -505,10 +498,194 @@ function renderFile(data) {
} }
} catch (e) { /* ignore */ } } catch (e) { /* ignore */ }
})(); })();
shareBtn.addEventListener("click", shareBtn.addEventListener("click", () => openShareDialog(data.vault, data.path));
... [OUTPUT TRUNCATED - 13907 chars omitted out of 63907 total] ... // Bookmark button — check if already bookmarked
const bookmarkBtn = el("button", { class: "btn-action btn-bookmark", title: "Ajouter/Retirer des bookmarks" }, [icon("bookmark-plus", 14), document.createTextNode("Bookmark")]);
// Check bookmark status and color the button
(async () => {
try {
const bms = await api("/api/bookmarks");
if (Array.isArray(bms) && bms.some(b => b.vault === data.vault && b.path === data.path)) {
bookmarkBtn.classList.add("active");
bookmarkBtn.title = "Retirer des bookmarks";
}
} catch (e) { /* ignore */ }
})();
bookmarkBtn.addEventListener("click", async () => {
try {
const res = await api("/api/bookmarks/toggle", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ vault: data.vault, path: data.path, title: data.title }) });
bookmarkBtn.classList.toggle("active", res.bookmarked);
bookmarkBtn.title = res.bookmarked ? "Retirer des bookmarks" : "Ajouter aux bookmarks";
showToast(res.bookmarked ? "Ajouté aux bookmarks" : "Retiré des bookmarks", "success");
if (typeof DashboardBookmarkWidget !== "undefined") DashboardBookmarkWidget.load();
} catch (err) { showToast("Erreur: " + err.message, "error"); }
});
// Frontmatter — Accent Card
let fmSection = null;
if (data.frontmatter && Object.keys(data.frontmatter).length > 0) {
fmSection = buildFrontmatterCard(data.frontmatter);
}
// 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, mdBtn, pdfBtn, editBtn, openNewWindowBtn, tocBtn, shareBtn, bookmarkBtn])]));
if (fmSection) area.appendChild(fmSection);
area.appendChild(mdDiv);
area.appendChild(rawDiv);
// Backlinks panel
if (data.is_markdown) {
renderBacklinksPanel(data.vault, data.path, area);
}
// 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;
// Initialize outline/TOC for this document
OutlineManager.init();
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function escapeHtml(str) {
if (!str) return "";
return String(str).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}
function el(tag, attrs, children) {
const e = document.createElement(tag);
if (attrs) {
Object.entries(attrs).forEach(([k, v]) => {
// Skip boolean false for standard HTML boolean attributes to avoid setAttribute("checked", "false") bug
if (v === false && (k === "checked" || k === "disabled" || k === "hidden" || k === "required" || k === "readonly")) {
return;
}
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.className = "badge-small";
s.style.cssText = "font-size:0.68rem;color:var(--text-muted);margin-left:4px";
s.textContent = `(${count})`;
return s;
}
function getContextMenuPositionFromElement(target) {
const rect = target.getBoundingClientRect();
return {
x: Math.min(rect.right - 8, window.innerWidth - 16),
y: Math.min(rect.top + rect.height / 2, window.innerHeight - 16),
};
}
function attachTreeItemActionButton(itemEl, vault, path, type, isReadonly) {
const button = document.createElement("button");
button.type = "button";
button.className = "tree-item-action-btn";
button.setAttribute("aria-label", "Afficher le menu d’actions");
button.setAttribute("title", "Actions");
const iconEl = icon("more-vertical", 16);
button.appendChild(iconEl);
button.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
const pos = getContextMenuPositionFromElement(button);
ContextMenuManager.show(pos.x, pos.y, vault, path, type, isReadonly);
});
itemEl.appendChild(button);
// Ensure Lucide icons are rendered for the button
setTimeout(() => {
safeCreateIcons();
}, 0);
}
function attachTreeItemLongPress(itemEl, getMenuData) {
let pressTimer = null;
let pressHandled = false;
let startX = 0;
let startY = 0;
const longPressDelay = 550;
const moveThreshold = 10;
const clearPressTimer = () => {
if (pressTimer) {
clearTimeout(pressTimer);
pressTimer = null;
}
};
itemEl.addEventListener("touchstart", (e) => {
if (!e.touches || e.touches.length !== 1) return;
pressHandled = false;
startX = e.touches[0].clientX;
startY = e.touches[0].clientY; startY = e.touches[0].clientY;
clearPressTimer(); clearPressTimer();
pressTimer = setTimeout(() => { pressTimer = setTimeout(() => {
@@ -548,7 +725,7 @@ function renderFile(data) {
} }
function getVaultIcon(vaultName, size = 16) { function getVaultIcon(vaultName, size = 16) {
const v = state.allVaults.find((val) => val.name === vaultName); const v = allVaults.find((val) => val.name === vaultName);
const type = v ? v.type : "VAULT"; const type = v ? v.type : "VAULT";
if (type === "DIR") { if (type === "DIR") {
@@ -771,10 +948,10 @@ function showWelcome() {
DashboardConflictsWidget.load(); DashboardConflictsWidget.load();
} }
if (typeof DashboardRecentWidget !== "undefined") { if (typeof DashboardRecentWidget !== "undefined") {
DashboardRecentWidget.load(state.selectedContextVault); DashboardRecentWidget.load(selectedContextVault);
} }
if (typeof DashboardBookmarkWidget !== "undefined") { if (typeof DashboardBookmarkWidget !== "undefined") {
DashboardBookmarkWidget.load(state.selectedContextVault); DashboardBookmarkWidget.load(selectedContextVault);
} }
if (typeof DashboardSharedWidget !== "undefined") { if (typeof DashboardSharedWidget !== "undefined") {
DashboardSharedWidget.load(); DashboardSharedWidget.load();
@@ -825,9 +1002,9 @@ async function loadSavedSearches() {
// Apply the saved search // Apply the saved search
const input = document.getElementById("search-input"); const input = document.getElementById("search-input");
if (input) input.value = s.query; if (input) input.value = s.query;
state.searchCaseSensitive = s.case_sensitive || false; searchCaseSensitive = s.case_sensitive || false;
state.searchWholeWord = s.whole_word || false; searchWholeWord = s.whole_word || false;
state.searchRegex = s.regex || false; searchRegex = s.regex || false;
if (typeof _updateToggleUI === "function") _updateToggleUI(); if (typeof _updateToggleUI === "function") _updateToggleUI();
if (s.include_paths) { if (s.include_paths) {
const incl = document.getElementById("search-include-input"); const incl = document.getElementById("search-include-input");
@@ -842,8 +1019,8 @@ async function loadSavedSearches() {
AutocompleteDropdown._suppressNext = true; AutocompleteDropdown._suppressNext = true;
const vault = s.vault || "all"; const vault = s.vault || "all";
if (input) { input.dispatchEvent(new Event("input")); } if (input) { input.dispatchEvent(new Event("input")); }
clearTimeout(state.searchTimeout); clearTimeout(searchTimeout);
state.advancedSearchOffset = 0; advancedSearchOffset = 0;
performAdvancedSearch(s.query, vault, null); performAdvancedSearch(s.query, vault, null);
}); });
}); });
@@ -882,58 +1059,15 @@ function goHome() {
document.querySelectorAll(".tree-item.active").forEach((el) => el.classList.remove("active")); document.querySelectorAll(".tree-item.active").forEach((el) => el.classList.remove("active"));
state.currentVault = null; currentVault = null;
state.currentPath = null; currentPath = null;
state.showingSource = false; showingSource = false;
state.cachedRawSource = null; cachedRawSource = null;
closeMobileSidebar(); closeMobileSidebar();
showWelcome(); showWelcome();
} }
// initEditor wires up the editor modal — editor functions (openEditor, closeEditor, saveFile, deleteFile) are in utils.js
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 },
);
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// SSE Client — IndexUpdateManager // SSE Client — IndexUpdateManager
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -1082,15 +1216,15 @@ const IndexUpdateManager = (() => {
// Toast removed: silent auto-indexing — no notification needed // Toast removed: silent auto-indexing — no notification needed
// Refresh sidebar and tags if affected vault matches current context // Refresh sidebar and tags if affected vault matches current context
const affectsCurrentVault = state.selectedContextVault === "all" || (data.vaults || []).includes(state.selectedContextVault); const affectsCurrentVault = selectedContextVault === "all" || (data.vaults || []).includes(selectedContextVault);
if (affectsCurrentVault) { if (affectsCurrentVault) {
try { try {
await Promise.all([refreshSidebarTreePreservingState(), refreshTagsForContext()]); await Promise.all([refreshSidebarTreePreservingState(), refreshTagsForContext()]);
// Refresh current file if it was updated // Refresh current file if it was updated
if (currentVault && state.currentPath) { if (currentVault && currentPath) {
const changed = (data.changes || []).some((c) => c.vault === currentVault && c.path === state.currentPath); const changed = (data.changes || []).some((c) => c.vault === currentVault && c.path === currentPath);
if (changed) { if (changed) {
openFile(state.currentVault, state.currentPath); openFile(currentVault, currentPath);
} }
} }
} catch (err) { } catch (err) {
@@ -1099,7 +1233,7 @@ const IndexUpdateManager = (() => {
} }
// Refresh recent tab if it is active // Refresh recent tab if it is active
if (state.activeSidebarTab === "recent") { if (activeSidebarTab === "recent") {
const vaultFilter = document.getElementById("recent-vault-filter"); const vaultFilter = document.getElementById("recent-vault-filter");
loadRecentFiles(vaultFilter ? vaultFilter.value || null : null); loadRecentFiles(vaultFilter ? vaultFilter.value || null : null);
} }
@@ -1250,6 +1384,3 @@ function _renderSyncPanel(panel) {
panel.innerHTML = html; panel.innerHTML = html;
} }
export { OutlineManager, ScrollSpyManager, ReadingProgressManager, openFile, buildFrontmatterCard, initEditor };