diff --git a/frontend/js/ui.js b/frontend/js/ui.js index 20bc7c7..ed3c49c 100644 --- a/frontend/js/ui.js +++ b/frontend/js/ui.js @@ -1804,427 +1804,6 @@ export const ContextMenuManager = { } catch (err) { showToast("Erreur: " + err.message, "error"); } } }; - - -export const FindInPageManager = { - isOpen: false, - searchTerm: "", - matches: [], - currentIndex: -1, - options: { - caseSensitive: false, - wholeWord: false, - useRegex: false, - }, - debounceTimer: null, - previousFocus: null, - - init() { - const bar = document.getElementById("find-in-page-bar"); - const input = document.getElementById("find-input"); - const prevBtn = document.getElementById("find-prev"); - const nextBtn = document.getElementById("find-next"); - const closeBtn = document.getElementById("find-close"); - const caseSensitiveBtn = document.getElementById("find-case-sensitive"); - const wholeWordBtn = document.getElementById("find-whole-word"); - const regexBtn = document.getElementById("find-regex"); - - if (!bar || !input) return; - - // Keyboard shortcuts - document.addEventListener("keydown", (e) => { - // Ctrl+F or Cmd+F to open - if ((e.ctrlKey || e.metaKey) && e.key === "f") { - e.preventDefault(); - this.open(); - } - // Escape to close - if (e.key === "Escape" && this.isOpen) { - e.preventDefault(); - this.close(); - } - // Enter to go to next - if (e.key === "Enter" && this.isOpen && document.activeElement === input) { - e.preventDefault(); - if (e.shiftKey) { - this.goToPrevious(); - } else { - this.goToNext(); - } - } - // F3 for next/previous - if (e.key === "F3" && this.isOpen) { - e.preventDefault(); - if (e.shiftKey) { - this.goToPrevious(); - } else { - this.goToNext(); - } - } - }); - - // Input event with debounce - input.addEventListener("input", (e) => { - clearTimeout(this.debounceTimer); - this.debounceTimer = setTimeout(() => { - this.search(e.target.value); - }, 250); - }); - - // Navigation buttons - prevBtn.addEventListener("click", () => this.goToPrevious()); - nextBtn.addEventListener("click", () => this.goToNext()); - - // Close button - closeBtn.addEventListener("click", () => this.close()); - - // Option toggles - caseSensitiveBtn.addEventListener("click", () => { - this.options.caseSensitive = !this.options.caseSensitive; - caseSensitiveBtn.setAttribute("aria-pressed", this.options.caseSensitive); - this.saveState(); - if (this.searchTerm) this.search(this.searchTerm); - }); - - wholeWordBtn.addEventListener("click", () => { - this.options.wholeWord = !this.options.wholeWord; - wholeWordBtn.setAttribute("aria-pressed", this.options.wholeWord); - this.saveState(); - if (this.searchTerm) this.search(this.searchTerm); - }); - - regexBtn.addEventListener("click", () => { - this.options.useRegex = !this.options.useRegex; - regexBtn.setAttribute("aria-pressed", this.options.useRegex); - this.saveState(); - if (this.searchTerm) this.search(this.searchTerm); - }); - - // Load saved state - this.loadState(); - }, - - open() { - const bar = document.getElementById("find-in-page-bar"); - const input = document.getElementById("find-input"); - if (!bar || !input) return; - - this.previousFocus = document.activeElement; - this.isOpen = true; - bar.hidden = false; - input.focus(); - input.select(); - safeCreateIcons(); - }, - - close() { - const bar = document.getElementById("find-in-page-bar"); - if (!bar) return; - - this.isOpen = false; - bar.hidden = true; - this.clearHighlights(); - this.matches = []; - this.currentIndex = -1; - this.searchTerm = ""; - - // Restore previous focus - if (this.previousFocus && this.previousFocus.focus) { - this.previousFocus.focus(); - } - }, - - search(term) { - this.searchTerm = term; - this.clearHighlights(); - this.hideError(); - - if (!term || term.trim().length === 0) { - this.updateCounter(); - this.updateNavButtons(); - return; - } - - const contentArea = document.querySelector(".md-content"); - if (!contentArea) { - this.updateCounter(); - this.updateNavButtons(); - return; - } - - try { - const regex = this.createRegex(term); - this.matches = []; - this.findMatches(contentArea, regex); - this.currentIndex = this.matches.length > 0 ? 0 : -1; - this.highlightMatches(); - this.updateCounter(); - this.updateNavButtons(); - - if (this.matches.length > 0) { - this.scrollToMatch(0); - } - } catch (err) { - this.showError(err.message); - this.matches = []; - this.currentIndex = -1; - this.updateCounter(); - this.updateNavButtons(); - } - }, - - createRegex(term) { - let pattern = term; - - if (!this.options.useRegex) { - // Escape special regex characters - pattern = term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - } - - if (this.options.wholeWord) { - pattern = "\\b" + pattern + "\\b"; - } - - const flags = this.options.caseSensitive ? "g" : "gi"; - return new RegExp(pattern, flags); - }, - - findMatches(container, regex) { - const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, { - acceptNode: (node) => { - // Skip code blocks, scripts, styles - const parent = node.parentElement; - if (!parent) return NodeFilter.FILTER_REJECT; - const tagName = parent.tagName.toLowerCase(); - if (["code", "pre", "script", "style"].includes(tagName)) { - return NodeFilter.FILTER_REJECT; - } - // Skip empty text nodes - if (!node.textContent || node.textContent.trim().length === 0) { - return NodeFilter.FILTER_REJECT; - } - return NodeFilter.FILTER_ACCEPT; - }, - }); - - let node; - while ((node = walker.nextNode())) { - const text = node.textContent; - let match; - regex.lastIndex = 0; // Reset regex - - while ((match = regex.exec(text)) !== null) { - this.matches.push({ - node: node, - index: match.index, - length: match[0].length, - text: match[0], - }); - - // Prevent infinite loop with zero-width matches - if (match.index === regex.lastIndex) { - regex.lastIndex++; - } - } - } - }, - - highlightMatches() { - const matchesByNode = new Map(); - - this.matches.forEach((match, idx) => { - if (!matchesByNode.has(match.node)) { - matchesByNode.set(match.node, []); - } - matchesByNode.get(match.node).push({ match, idx }); - }); - - matchesByNode.forEach((entries, node) => { - if (!node || !node.parentNode) return; - - const text = node.textContent || ""; - let cursor = 0; - const fragment = document.createDocumentFragment(); - - entries.sort((a, b) => a.match.index - b.match.index); - - entries.forEach(({ match, idx }) => { - if (match.index > cursor) { - fragment.appendChild(document.createTextNode(text.substring(cursor, match.index))); - } - - const matchText = text.substring(match.index, match.index + match.length); - const mark = document.createElement("mark"); - mark.className = idx === this.currentIndex ? "find-highlight find-highlight-active" : "find-highlight"; - mark.textContent = matchText; - mark.setAttribute("data-find-index", idx); - fragment.appendChild(mark); - - match.element = mark; - cursor = match.index + match.length; - }); - - if (cursor < text.length) { - fragment.appendChild(document.createTextNode(text.substring(cursor))); - } - - node.parentNode.replaceChild(fragment, node); - }); - }, - - clearHighlights() { - const contentArea = document.querySelector(".md-content"); - if (!contentArea) return; - - const marks = contentArea.querySelectorAll("mark.find-highlight"); - marks.forEach((mark) => { - if (!mark.parentNode) return; - const text = mark.textContent; - const textNode = document.createTextNode(text); - mark.parentNode.replaceChild(textNode, mark); - }); - - // Normalize text nodes to merge adjacent text nodes - contentArea.normalize(); - }, - - goToNext() { - if (this.matches.length === 0) return; - - // Remove active class from current - if (this.currentIndex >= 0 && this.matches[this.currentIndex].element) { - this.matches[this.currentIndex].element.classList.remove("find-highlight-active"); - } - - // Move to next (with wrapping) - this.currentIndex = (this.currentIndex + 1) % this.matches.length; - - // Add active class to new current - if (this.matches[this.currentIndex].element) { - this.matches[this.currentIndex].element.classList.add("find-highlight-active"); - } - - this.scrollToMatch(this.currentIndex); - this.updateCounter(); - }, - - goToPrevious() { - if (this.matches.length === 0) return; - - // Remove active class from current - if (this.currentIndex >= 0 && this.matches[this.currentIndex].element) { - this.matches[this.currentIndex].element.classList.remove("find-highlight-active"); - } - - // Move to previous (with wrapping) - this.currentIndex = this.currentIndex <= 0 ? this.matches.length - 1 : this.currentIndex - 1; - - // Add active class to new current - if (this.matches[this.currentIndex].element) { - this.matches[this.currentIndex].element.classList.add("find-highlight-active"); - } - - this.scrollToMatch(this.currentIndex); - this.updateCounter(); - }, - - scrollToMatch(index) { - if (index < 0 || index >= this.matches.length) return; - - const match = this.matches[index]; - if (!match.element) return; - - const contentArea = document.getElementById("content-area"); - if (!contentArea) { - match.element.scrollIntoView({ behavior: "smooth", block: "center" }); - return; - } - - // Calculate position with offset for header - const elementTop = match.element.offsetTop; - const offset = 100; // Offset for header - - contentArea.scrollTo({ - top: elementTop - offset, - behavior: "smooth", - }); - }, - - updateCounter() { - const counter = document.getElementById("find-counter"); - if (!counter) return; - - const count = this.matches.length; - if (count === 0) { - counter.textContent = "0 occurrence"; - } else if (count === 1) { - counter.textContent = "1 occurrence"; - } else { - counter.textContent = `${count} occurrences`; - } - }, - - updateNavButtons() { - const prevBtn = document.getElementById("find-prev"); - const nextBtn = document.getElementById("find-next"); - if (!prevBtn || !nextBtn) return; - - const hasMatches = this.matches.length > 0; - prevBtn.disabled = !hasMatches; - nextBtn.disabled = !hasMatches; - }, - - showError(message) { - const errorEl = document.getElementById("find-error"); - if (!errorEl) return; - - errorEl.textContent = message; - errorEl.hidden = false; - }, - - hideError() { - const errorEl = document.getElementById("find-error"); - if (!errorEl) return; - - errorEl.hidden = true; - }, - - saveState() { - try { - const state = { - options: this.options, - }; - localStorage.setItem("obsigate-find-in-page-state", JSON.stringify(state)); - } catch (e) { - // Ignore localStorage errors - } - }, - - loadState() { - try { - const saved = localStorage.getItem("obsigate-find-in-page-state"); - if (saved) { - const state = JSON.parse(saved); - if (state.options) { - this.options = { ...this.options, ...state.options }; - - // Update button states - const caseSensitiveBtn = document.getElementById("find-case-sensitive"); - const wholeWordBtn = document.getElementById("find-whole-word"); - const regexBtn = document.getElementById("find-regex"); - - if (caseSensitiveBtn) caseSensitiveBtn.setAttribute("aria-pressed", this.options.caseSensitive); - if (wholeWordBtn) wholeWordBtn.setAttribute("aria-pressed", this.options.wholeWord); - if (regexBtn) regexBtn.setAttribute("aria-pressed", this.options.useRegex); - } - } - } catch (e) { - // Ignore localStorage errors - } - }, -}; - - export const TabManager = { _tabs: [], _activeTabId: null,