diff --git a/frontend/js/search.js b/frontend/js/search.js index 0605a8f..72bcce1 100644 --- a/frontend/js/search.js +++ b/frontend/js/search.js @@ -1,12 +1,13 @@ -/* ObsiGate — Auto-extracted module */ -import { state } from './state.js';; +/* ObsiGate — Search module */ +import { state } from './state.js'; +import { safeCreateIcons } from './utils.js'; // --------------------------------------------------------------------------- // Search History Service (localStorage, LIFO, max 50, dedup) // --------------------------------------------------------------------------- const SearchHistory = { _load() { try { - const raw = localStorage.getItem(state.SEARCH_HISTORY_KEY); + const raw = localStorage.getItem(SEARCH_HISTORY_KEY); return raw ? JSON.parse(raw) : []; } catch { return []; @@ -14,7 +15,7 @@ const SearchHistory = { }, _save(entries) { try { - localStorage.setItem(state.SEARCH_HISTORY_KEY, JSON.stringify(entries)); + localStorage.setItem(SEARCH_HISTORY_KEY, JSON.stringify(entries)); } catch {} }, getAll() { @@ -25,7 +26,7 @@ const SearchHistory = { const q = query.trim(); let entries = this._load().filter((e) => e !== 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); }, remove(query) { @@ -44,6 +45,7 @@ const SearchHistory = { }, }; + // --------------------------------------------------------------------------- // Query Parser — extracts operators (tag:, #, vault:, title:, path:, ext:) // --------------------------------------------------------------------------- @@ -119,6 +121,7 @@ const QueryParser = { }, }; + // --------------------------------------------------------------------------- // Autocomplete Dropdown Controller // --------------------------------------------------------------------------- @@ -179,7 +182,7 @@ const AutocompleteDropdown = { async populate(inputValue, cursorPos) { if (this._suppressNext) { this._suppressNext = false; return; } // Cancel previous suggestion request - if (state.suggestAbortController) { + if (suggestAbortController) { state.suggestAbortController.abort(); state.suggestAbortController = null; } @@ -370,29 +373,30 @@ const AutocompleteDropdown = { navigateDown() { if (!this.isVisible() || state.dropdownItems.length === 0) return; - if (dropdownActiveIndex >= 0) state.dropdownItems[state.dropdownActiveIndex].classList.remove("active"); + if (dropdownActiveIndex >= 0) dropdownItems[dropdownActiveIndex].classList.remove("active"); state.dropdownActiveIndex = (dropdownActiveIndex + 1) % state.dropdownItems.length; - state.dropdownItems[state.dropdownActiveIndex].classList.add("active"); - state.dropdownItems[state.dropdownActiveIndex].scrollIntoView({ block: "nearest" }); + dropdownItems[dropdownActiveIndex].classList.add("active"); + dropdownItems[dropdownActiveIndex].scrollIntoView({ block: "nearest" }); }, navigateUp() { if (!this.isVisible() || state.dropdownItems.length === 0) return; - if (dropdownActiveIndex >= 0) state.dropdownItems[state.dropdownActiveIndex].classList.remove("active"); + if (dropdownActiveIndex >= 0) dropdownItems[dropdownActiveIndex].classList.remove("active"); state.dropdownActiveIndex = dropdownActiveIndex <= 0 ? state.dropdownItems.length - 1 : dropdownActiveIndex - 1; - state.dropdownItems[state.dropdownActiveIndex].classList.add("active"); - state.dropdownItems[state.dropdownActiveIndex].scrollIntoView({ block: "nearest" }); + dropdownItems[dropdownActiveIndex].classList.add("active"); + dropdownItems[dropdownActiveIndex].scrollIntoView({ block: "nearest" }); }, selectActive() { if (dropdownActiveIndex >= 0 && dropdownActiveIndex < state.dropdownItems.length) { - state.dropdownItems[state.dropdownActiveIndex].click(); + dropdownItems[dropdownActiveIndex].click(); return true; } return false; }, }; + // --------------------------------------------------------------------------- // Search Chips Controller — renders active filter chips from parsed query // --------------------------------------------------------------------------- @@ -454,6 +458,7 @@ const SearchChips = { }, }; + // --------------------------------------------------------------------------- // Helper: trigger advanced search from input value // --------------------------------------------------------------------------- @@ -471,6 +476,7 @@ function _triggerAdvancedSearch(rawQuery) { } } + // --------------------------------------------------------------------------- // Search (enhanced with autocomplete, keyboard nav, global shortcuts) // --------------------------------------------------------------------------- @@ -490,10 +496,10 @@ function initSearch() { const countEl = document.getElementById("search-match-count"); function _updateToggleUI() { - caseBtn.classList.toggle("active", state.searchCaseSensitive); - wordBtn.classList.toggle("active", state.searchWholeWord); - regexBtn.classList.toggle("active", state.searchRegex); - filterBtn.classList.toggle("active", state.searchFilterVisible); + caseBtn.classList.toggle("active", searchCaseSensitive); + wordBtn.classList.toggle("active", searchWholeWord); + regexBtn.classList.toggle("active", searchRegex); + filterBtn.classList.toggle("active", searchFilterVisible); } // Toggle buttons @@ -536,7 +542,7 @@ function initSearch() { function _research() { const q = input.value.trim(); if (q.length >= _getEffective("min_query_length", 2)) { - clearTimeout(state.searchTimeout); + clearTimeout(searchTimeout); state.searchTimeout = setTimeout(() => { const vault = document.getElementById("vault-filter").value; const tagFilter = state.selectedTags.length > 0 ? state.selectedTags.join(",") : null; @@ -572,14 +578,14 @@ function initSearch() { AutocompleteDropdown.populate(input.value, input.selectionStart); // Debounced search execution - clearTimeout(state.searchTimeout); + clearTimeout(searchTimeout); state.searchTimeout = setTimeout( () => { const q = input.value.trim(); const vault = document.getElementById("vault-filter").value; const tagFilter = state.selectedTags.length > 0 ? state.selectedTags.join(",") : null; state.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); } else if (q.length === 0) { SearchChips.clear(); @@ -630,7 +636,7 @@ function initSearch() { const q = input.value.trim(); if (q) { SearchHistory.add(q); - clearTimeout(state.searchTimeout); + clearTimeout(searchTimeout); state.advancedSearchOffset = 0; const vault = document.getElementById("vault-filter").value; const tagFilter = state.selectedTags.length > 0 ? state.selectedTags.join(",") : null; @@ -654,7 +660,7 @@ function initSearch() { const q = input.value.trim(); if (q) { SearchHistory.add(q); - clearTimeout(state.searchTimeout); + clearTimeout(searchTimeout); state.advancedSearchOffset = 0; const vault = document.getElementById("vault-filter").value; const tagFilter = state.selectedTags.length > 0 ? state.selectedTags.join(",") : null; @@ -706,7 +712,7 @@ function _isInputFocused() { // --- Backward-compatible search (existing /api/search endpoint) --- async function performSearch(query, vaultFilter, tagFilter) { - if (state.searchAbortController) state.searchAbortController.abort(); + if (searchAbortController) state.searchAbortController.abort(); state.searchAbortController = new AbortController(); const searchId = ++state.currentSearchId; showLoading(); @@ -714,21 +720,21 @@ async function performSearch(query, vaultFilter, tagFilter) { if (tagFilter) url += `&tag=${encodeURIComponent(tagFilter)}`; try { const data = await api(url, { signal: state.searchAbortController.signal }); - if (searchId !== state.currentSearchId) return; + if (searchId !== currentSearchId) return; renderSearchResults(data, query, tagFilter); } catch (err) { if (err.name === "AbortError") return; - if (searchId !== state.currentSearchId) return; + if (searchId !== currentSearchId) return; showWelcome(); } finally { hideProgressBar(); - if (searchId === state.currentSearchId) state.searchAbortController = null; + if (searchId === currentSearchId) state.searchAbortController = null; } } // --- Advanced search with TF-IDF, facets, pagination --- async function performAdvancedSearch(query, vaultFilter, tagFilter, offset, sort) { - if (state.searchAbortController) state.searchAbortController.abort(); + if (searchAbortController) state.searchAbortController.abort(); state.searchAbortController = new AbortController(); const searchId = ++state.currentSearchId; showLoading(); @@ -741,12 +747,12 @@ async function performAdvancedSearch(query, vaultFilter, tagFilter, offset, sort const parsed = QueryParser.parse(query); 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}`; if (tagFilter) url += `&tag=${encodeURIComponent(tagFilter)}`; - if (state.searchCaseSensitive) url += "&case_sensitive=true"; - if (state.searchWholeWord) url += "&whole_word=true"; - if (state.searchRegex) url += "®ex=true"; + if (searchCaseSensitive) url += "&case_sensitive=true"; + if (searchWholeWord) url += "&whole_word=true"; + if (searchRegex) url += "®ex=true"; const includeEl = document.getElementById("search-include-input"); const excludeEl = document.getElementById("search-exclude-input"); if (includeEl?.value.trim()) url += `&include_paths=${encodeURIComponent(includeEl.value.trim())}`; @@ -755,26 +761,26 @@ async function performAdvancedSearch(query, vaultFilter, tagFilter, offset, sort // Search timeout — abort if server takes too long const timeoutId = setTimeout( () => { - if (state.searchAbortController) state.searchAbortController.abort(); + if (searchAbortController) state.searchAbortController.abort(); }, - _getEffective("search_timeout_ms", state.SEARCH_TIMEOUT_MS), + _getEffective("search_timeout_ms", SEARCH_TIMEOUT_MS), ); try { const data = await api(url, { signal: state.searchAbortController.signal }); clearTimeout(timeoutId); - if (searchId !== state.currentSearchId) return; + if (searchId !== currentSearchId) return; state.advancedSearchTotal = data.total; state.advancedSearchOffset = ofs; renderAdvancedSearchResults(data, query, tagFilter); } catch (err) { clearTimeout(timeoutId); if (err.name === "AbortError") return; - if (searchId !== state.currentSearchId) return; + if (searchId !== currentSearchId) return; showWelcome(); } finally { hideProgressBar(); - if (searchId === state.currentSearchId) state.searchAbortController = null; + if (searchId === currentSearchId) state.searchAbortController = null; } } @@ -797,7 +803,7 @@ function renderSearchResults(data, query, tagFilter) { const titleDiv = el("div", { class: "search-result-title" }); if (query && query.trim()) { - highlightSearchText(titleDiv, r.title, query, state.searchCaseSensitive); + highlightSearchText(titleDiv, r.title, query, searchCaseSensitive); } else { titleDiv.textContent = r.title; } @@ -805,7 +811,7 @@ function renderSearchResults(data, query, tagFilter) { if (r.snippet && r.snippet.includes("")) { snippetDiv.innerHTML = r.snippet; } else if (query && query.trim() && r.snippet) { - highlightSearchText(snippetDiv, r.snippet, query, state.searchCaseSensitive); + highlightSearchText(snippetDiv, r.snippet, query, searchCaseSensitive); } else { snippetDiv.textContent = r.snippet || ""; } @@ -869,9 +875,9 @@ function renderAdvancedSearchResults(data, query, tagFilter) { // Active filter badges const filtersRow = el("div", { class: "search-filters-row" }); - if (state.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 (state.searchRegex) filtersRow.appendChild(el("span", { class: "search-filter-badge" }, [document.createTextNode(".*")])); + if (searchCaseSensitive) filtersRow.appendChild(el("span", { class: "search-filter-badge" }, [document.createTextNode("Aa")])); + if (searchWholeWord) filtersRow.appendChild(el("span", { class: "search-filter-badge" }, [document.createTextNode("wd")])); + if (searchRegex) filtersRow.appendChild(el("span", { class: "search-filter-badge" }, [document.createTextNode(".*")])); const inclEl = document.getElementById("search-include-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())])); @@ -913,9 +919,9 @@ function renderAdvancedSearchResults(data, query, tagFilter) { body: JSON.stringify({ query: query, vault: document.getElementById("vault-filter")?.value || "all", - case_sensitive: state.searchCaseSensitive, - whole_word: state.searchWholeWord, - regex: state.searchRegex, + case_sensitive: searchCaseSensitive, + whole_word: searchWholeWord, + regex: searchRegex, include_paths: inclEl?.value || "", exclude_paths: exclEl?.value || "", }), @@ -1012,7 +1018,7 @@ function renderAdvancedSearchResults(data, query, tagFilter) { const titleDiv = el("div", { class: "search-result-title" }); if (freeText) { - highlightSearchText(titleDiv, r.title, freeText, state.searchCaseSensitive); + highlightSearchText(titleDiv, r.title, freeText, searchCaseSensitive); } else { titleDiv.textContent = r.title; } @@ -1022,7 +1028,7 @@ function renderAdvancedSearchResults(data, query, tagFilter) { if (r.snippet && r.snippet.includes("")) { snippetDiv.innerHTML = r.snippet; } else if (freeText && r.snippet) { - highlightSearchText(snippetDiv, r.snippet, freeText, state.searchCaseSensitive); + highlightSearchText(snippetDiv, r.snippet, freeText, searchCaseSensitive); } else { snippetDiv.textContent = r.snippet || ""; } @@ -1060,21 +1066,21 @@ function renderAdvancedSearchResults(data, query, tagFilter) { area.appendChild(container); // Pagination - if (data.total > state.ADVANCED_SEARCH_LIMIT) { + if (data.total > ADVANCED_SEARCH_LIMIT) { const paginationDiv = el("div", { class: "search-pagination" }); const prevBtn = el("button", { class: "search-pagination__btn", type: "button" }); prevBtn.textContent = "← Précédent"; prevBtn.disabled = state.advancedSearchOffset === 0; prevBtn.addEventListener("click", () => { - state.advancedSearchOffset = Math.max(0, advancedSearchOffset - state.ADVANCED_SEARCH_LIMIT); + state.advancedSearchOffset = Math.max(0, advancedSearchOffset - ADVANCED_SEARCH_LIMIT); const vault = document.getElementById("vault-filter").value; - performAdvancedSearch(query, vault, tagFilter, state.advancedSearchOffset); + performAdvancedSearch(query, vault, tagFilter, advancedSearchOffset); document.getElementById("content-area").scrollTop = 0; }); const info = el("span", { class: "search-pagination__info" }); const from = advancedSearchOffset + 1; - const to = Math.min(advancedSearchOffset + state.ADVANCED_SEARCH_LIMIT, data.total); + const to = Math.min(advancedSearchOffset + ADVANCED_SEARCH_LIMIT, data.total); info.textContent = `${from}–${to} sur ${data.total}`; const nextBtn = el("button", { class: "search-pagination__btn", type: "button" }); @@ -1083,7 +1089,7 @@ function renderAdvancedSearchResults(data, query, tagFilter) { nextBtn.addEventListener("click", () => { advancedSearchOffset += state.ADVANCED_SEARCH_LIMIT; const vault = document.getElementById("vault-filter").value; - performAdvancedSearch(query, vault, tagFilter, state.advancedSearchOffset); + performAdvancedSearch(query, vault, tagFilter, advancedSearchOffset); document.getElementById("content-area").scrollTop = 0; }); @@ -1096,4 +1102,6 @@ function renderAdvancedSearchResults(data, query, tagFilter) { safeCreateIcons(); // Initialize result navigation (select first result) setTimeout(() => { if (window.navigateSearchResults) window.navigateSearchResults(0); }, 50); -} \ No newline at end of file +} + + diff --git a/frontend/js/ui.js b/frontend/js/ui.js index ea16a20..0f9e8de 100644 --- a/frontend/js/ui.js +++ b/frontend/js/ui.js @@ -1,5 +1,7 @@ -/* ObsiGate — Auto-extracted module */ -import { state } from './state.js';; +/* ObsiGate — UI module */ +import { state } from './state.js'; +import { openFile } from './viewer.js'; +import { safeCreateIcons } from './utils.js'; // --------------------------------------------------------------------------- // Right Sidebar Manager // --------------------------------------------------------------------------- @@ -34,9 +36,9 @@ export const RightSidebarManager = { if (!sidebar) return; - if (state.rightSidebarVisible) { + if (rightSidebarVisible) { sidebar.classList.remove("hidden"); - sidebar.style.width = `${state.rightSidebarWidth}px`; + sidebar.style.width = `${rightSidebarWidth}px`; if (handle) handle.classList.remove("hidden"); if (tocBtn) { tocBtn.classList.add("active"); @@ -65,7 +67,7 @@ export const RightSidebarManager = { toggle() { state.rightSidebarVisible = !state.rightSidebarVisible; - localStorage.setItem("obsigate-right-sidebar-visible", state.rightSidebarVisible); + localStorage.setItem("obsigate-right-sidebar-visible", rightSidebarVisible); this.applyState(); }, @@ -116,7 +118,7 @@ export const RightSidebarManager = { document.body.style.cursor = ""; document.body.style.userSelect = ""; - localStorage.setItem("obsigate-right-sidebar-width", state.rightSidebarWidth); + localStorage.setItem("obsigate-right-sidebar-width", rightSidebarWidth); }; handle.addEventListener("mousedown", onMouseDown); @@ -125,6 +127,7 @@ export const RightSidebarManager = { }, }; + // --------------------------------------------------------------------------- // Theme // --------------------------------------------------------------------------- @@ -197,6 +200,7 @@ function closeHeaderMenu() { menuDropdown.classList.remove("active"); } + // --------------------------------------------------------------------------- // Custom Dropdowns // --------------------------------------------------------------------------- @@ -327,6 +331,7 @@ function populateCustomDropdown(dropdownId, optionsList, defaultValue) { }); } + // --------------------------------------------------------------------------- // Toast notifications // --------------------------------------------------------------------------- @@ -359,6 +364,7 @@ function showToast(message, type) { }, 3500); } + // --------------------------------------------------------------------------- // Sidebar toggle (desktop) // --------------------------------------------------------------------------- @@ -385,6 +391,7 @@ function initSidebarToggle() { }); } + // --------------------------------------------------------------------------- // Mobile sidebar // --------------------------------------------------------------------------- @@ -411,6 +418,7 @@ function closeMobileSidebar() { if (overlay) overlay.classList.remove("active"); } + // --------------------------------------------------------------------------- // Resizable sidebar (horizontal) // --------------------------------------------------------------------------- @@ -452,6 +460,7 @@ function initSidebarResize() { }); } + // --------------------------------------------------------------------------- // Resizable tag section (vertical) // --------------------------------------------------------------------------- @@ -494,6 +503,7 @@ function initTagResize() { }); } + // --------------------------------------------------------------------------- // Frontmatter Accent Card Builder // --------------------------------------------------------------------------- @@ -557,11 +567,163 @@ function buildFrontmatterCard(frontmatter) { // 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 || + 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 || "—")])]), + el("div", { class: "ac-row" }, [el("span", { class: "ac-k" }, [document.createTextNode("statut")]), el("span", { class: "ac-v" }, [document.createTextNode(frontmatter.statut || "—")])]), + el("div", { class: "ac-row" }, [el("span", { class: "ac-k" }, [document.createTextNode("aliases")]), el("span", { class: "ac-v muted" }, [document.createTextNode(frontmatter.aliases && frontmatter.aliases.length > 0 ? frontmatter.aliases.join(", ") : "[]")])]), + ]); -... [OUTPUT TRUNCATED - 6724 chars omitted out of 56724 total] ... + const rightCol = el("div", { class: "ac-col" }, [ + el("div", { class: "ac-row" }, [el("span", { class: "ac-k" }, [document.createTextNode("creation_date")]), el("span", { class: "ac-v mono" }, [document.createTextNode(formatDate(frontmatter.creation_date))])]), + el("div", { class: "ac-row" }, [el("span", { class: "ac-k" }, [document.createTextNode("modification_date")]), el("span", { class: "ac-v mono" }, [document.createTextNode(formatDate(frontmatter.modification_date))])]), + el("div", { class: "ac-row" }, [el("span", { class: "ac-k" }, [document.createTextNode("publish")]), el("span", { class: "ac-v" }, [document.createTextNode(String(frontmatter.publish || false))])]), + el("div", { class: "ac-row" }, [el("span", { class: "ac-k" }, [document.createTextNode("favoris")]), el("span", { class: "ac-v" }, [document.createTextNode(String(frontmatter.favoris || false))])]), + ]); -'div'); + const acBody = el("div", { class: "ac-body" }, [leftCol, rightCol]); + + // ZONE 3: Tags row + const tagPills = []; + if (frontmatter.tags && frontmatter.tags.length > 0) { + frontmatter.tags.forEach((tag) => { + tagPills.push(el("span", { class: "ac-tag" }, [document.createTextNode(tag)])); + }); + } + + const acTagsRow = el("div", { class: "ac-tags-row" }, [el("span", { class: "ac-tags-k" }, [document.createTextNode("tags")]), el("div", { class: "ac-tags-wrap" }, tagPills)]); + + // ZONE 4: Flags row + const flagChips = []; + booleanFlags.forEach((flag) => { + const chipClass = flag.value ? "flag-chip on" : "flag-chip off"; + flagChips.push(el("span", { class: chipClass }, [el("span", { class: "flag-dot" }), document.createTextNode(flag.key)])); + }); + + const acFlagsRow = el("div", { class: "ac-flags-row" }, [el("span", { class: "ac-flags-k" }, [document.createTextNode("flags")]), ...flagChips]); + + // Assemble the card + const acCard = el("div", { class: "ac-card" }, [acTop, acBody, acTagsRow, acFlagsRow]); + + // Toggle functionality + fmHeader.addEventListener("click", () => { + isOpen = !isOpen; + if (isOpen) { + acCard.style.display = "block"; + chevron.classList.remove("closed"); + chevron.classList.add("open"); + } else { + acCard.style.display = "none"; + chevron.classList.remove("open"); + chevron.classList.add("closed"); + } + safeCreateIcons(); + }); + + // Wrap in section + const fmSection = el("div", { class: "fm-section" }, [fmHeader, acCard]); + + return fmSection; +} + + +// --------------------------------------------------------------------------- +// File Operations Manager +// --------------------------------------------------------------------------- + +const FileOperations = { + showCreateDirectoryModal(vault, parentPath) { + const overlay = this._createModalOverlay(); + const modal = document.createElement('div'); + modal.className = 'obsigate-modal'; + modal.innerHTML = ` +
+

Créer un dossier

+
+
+ +
+ + `; + + overlay.appendChild(modal); + document.body.appendChild(overlay); + + setTimeout(() => overlay.classList.add('active'), 10); + + const input = modal.querySelector('#dir-name-input'); + const errorDiv = modal.querySelector('#dir-error'); + const createBtn = modal.querySelector('#dir-create-btn'); + const cancelBtn = modal.querySelector('#dir-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 () => { + const name = input.value.trim(); + const error = validateName(name); + if (error) { + errorDiv.textContent = error; + errorDiv.style.display = 'block'; + return; + } + + const path = parentPath ? `${parentPath}/${name}` : name; + createBtn.disabled = true; + createBtn.textContent = 'Création...'; + + try { + await api(`/api/directory/${encodeURIComponent(vault)}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ path }), + }); + + showToast(`Dossier "${name}" créé`, 'success'); + this._closeModal(overlay); + await refreshSidebarTreePreservingState(); + } 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); + }); + }, + + showCreateFileModal(vault, parentPath) { + const overlay = this._createModalOverlay(); + const modal = document.createElement('div'); modal.className = 'obsigate-modal'; modal.innerHTML = `
@@ -743,10 +905,10 @@ function buildFrontmatterCard(frontmatter) { if (type === 'file' && state.currentVault === vault && state.currentPath === path) { await openFile(vault, nextPath); - } else if (type === 'directory' && state.currentVault === vault && currentPath && (state.currentPath === path || state.currentPath.startsWith(`${path}/`))) { - const suffix = state.currentPath === path ? '' : state.currentPath.slice(path.length); + } else if (type === 'directory' && state.currentVault === vault && currentPath && (state.currentPath === path || currentPath.startsWith(`${path}/`))) { + const suffix = state.currentPath === path ? '' : currentPath.slice(path.length); state.currentPath = `${nextPath}${suffix}`; - await focusPathInSidebar(vault, state.currentPath, { alignToTop: false }); + await focusPathInSidebar(vault, currentPath, { alignToTop: false }); } showToast(type === 'directory' ? 'Dossier renommé' : 'Fichier renommé', 'success'); @@ -830,7 +992,7 @@ function buildFrontmatterCard(frontmatter) { this._closeModal(overlay); await refreshSidebarTreePreservingState(); - if (state.currentVault === vault && currentPath && state.currentPath.startsWith(path)) { + if (state.currentVault === vault && currentPath && currentPath.startsWith(path)) { showWelcome(); } } catch (err) { @@ -923,6 +1085,7 @@ function buildFrontmatterCard(frontmatter) { } }; + // --------------------------------------------------------------------------- // Find in Page Manager // --------------------------------------------------------------------------- @@ -1344,6 +1507,7 @@ const FindInPageManager = { }, }; + // --------------------------------------------------------------------------- // Init // --------------------------------------------------------------------------- @@ -1414,6 +1578,7 @@ async function init() { safeCreateIcons(); } + // ---- Modify openFile to use TabManager ---- const _originalOpenFile = openFile; openFile = function(vault, path) { @@ -1453,4 +1618,6 @@ const _origInit2 = init; init = function() { _origInit2(); TabManager.init(); -}; \ No newline at end of file +}; + + diff --git a/frontend/js/viewer.js b/frontend/js/viewer.js index 8e19461..bec38d6 100644 --- a/frontend/js/viewer.js +++ b/frontend/js/viewer.js @@ -1,5 +1,6 @@ -/* ObsiGate — Auto-extracted module */ -import { state } from './state.js';; +/* ObsiGate — Viewer module */ +import { state } from './state.js'; +import { escapeHtml, safeCreateIcons, safeHighlight, getFileIcon } from './utils.js'; // --------------------------------------------------------------------------- // Outline/TOC Manager // --------------------------------------------------------------------------- @@ -183,6 +184,7 @@ const OutlineManager = { }, }; + // --------------------------------------------------------------------------- // Scroll Spy Manager // --------------------------------------------------------------------------- @@ -240,6 +242,7 @@ const ScrollSpyManager = { }, }; + // --------------------------------------------------------------------------- // Reading Progress Manager // --------------------------------------------------------------------------- @@ -305,6 +308,7 @@ const ReadingProgressManager = { }, }; + // --------------------------------------------------------------------------- // File viewer // --------------------------------------------------------------------------- @@ -435,12 +439,12 @@ function renderFile(data) { copyBtn.addEventListener("click", async () => { try { // 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 rawData = await api(rawUrl); state.cachedRawSource = rawData.raw; } - await navigator.clipboard.writeText(state.cachedRawSource); + await navigator.clipboard.writeText(cachedRawSource); copyBtn.lastChild.textContent = "Copié !"; setTimeout(() => (copyBtn.lastChild.textContent = "Copier"), 1500); } catch (err) { @@ -542,9 +546,9 @@ function renderFile(data) { if (!rendered || !raw) return; state.showingSource = !state.showingSource; - if (state.showingSource) { + if (showingSource) { sourceBtn.classList.add("active"); - if (!state.cachedRawSource) { + if (!cachedRawSource) { const rawUrl = `/api/file/${encodeURIComponent(data.vault)}/raw?path=${encodeURIComponent(data.path)}`; const rawData = await api(rawUrl); state.cachedRawSource = rawData.raw; @@ -594,6 +598,7 @@ function renderFile(data) { OutlineManager.init(); } + // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- @@ -725,7 +730,7 @@ function attachTreeItemLongPress(itemEl, getMenuData) { } 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"; if (type === "DIR") { @@ -948,10 +953,10 @@ function showWelcome() { DashboardConflictsWidget.load(); } if (typeof DashboardRecentWidget !== "undefined") { - DashboardRecentWidget.load(state.selectedContextVault); + DashboardRecentWidget.load(selectedContextVault); } if (typeof DashboardBookmarkWidget !== "undefined") { - DashboardBookmarkWidget.load(state.selectedContextVault); + DashboardBookmarkWidget.load(selectedContextVault); } if (typeof DashboardSharedWidget !== "undefined") { DashboardSharedWidget.load(); @@ -1002,9 +1007,9 @@ async function loadSavedSearches() { // Apply the saved search const input = document.getElementById("search-input"); if (input) input.value = s.query; - state.searchCaseSensitive = s.case_sensitive || false; - state.searchWholeWord = s.whole_word || false; - state.searchRegex = s.regex || false; + 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"); @@ -1019,8 +1024,8 @@ async function loadSavedSearches() { AutocompleteDropdown._suppressNext = true; const vault = s.vault || "all"; if (input) { input.dispatchEvent(new Event("input")); } - clearTimeout(state.searchTimeout); - state.advancedSearchOffset = 0; + clearTimeout(searchTimeout); + advancedSearchOffset = 0; performAdvancedSearch(s.query, vault, null); }); }); @@ -1068,6 +1073,7 @@ function goHome() { showWelcome(); } + // --------------------------------------------------------------------------- // SSE Client — IndexUpdateManager // --------------------------------------------------------------------------- @@ -1216,15 +1222,15 @@ const IndexUpdateManager = (() => { // Toast removed: silent auto-indexing — no notification needed // Refresh sidebar and tags if affected vault matches current context - const affectsCurrentVault = state.selectedContextVault === "all" || (data.vaults || []).includes(state.selectedContextVault); + const affectsCurrentVault = state.selectedContextVault === "all" || (data.vaults || []).includes(selectedContextVault); if (affectsCurrentVault) { try { await Promise.all([refreshSidebarTreePreservingState(), refreshTagsForContext()]); // Refresh current file if it was updated - if (currentVault && state.currentPath) { - const changed = (data.changes || []).some((c) => c.vault === currentVault && c.path === state.currentPath); + if (currentVault && currentPath) { + const changed = (data.changes || []).some((c) => c.vault === currentVault && c.path === currentPath); if (changed) { - openFile(state.currentVault, state.currentPath); + openFile(currentVault, currentPath); } } } catch (err) { @@ -1233,7 +1239,7 @@ const IndexUpdateManager = (() => { } // Refresh recent tab if it is active - if (state.activeSidebarTab === "recent") { + if (activeSidebarTab === "recent") { const vaultFilter = document.getElementById("recent-vault-filter"); loadRecentFiles(vaultFilter ? vaultFilter.value || null : null); } @@ -1383,4 +1389,6 @@ function _renderSyncPanel(panel) { } panel.innerHTML = html; -} \ No newline at end of file +} + +