fix: remove duplicate FindInPageManager block in ui.js
This commit is contained in:
@@ -1804,427 +1804,6 @@ export const ContextMenuManager = {
|
|||||||
} catch (err) { showToast("Erreur: " + err.message, "error"); }
|
} 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 = {
|
export const TabManager = {
|
||||||
_tabs: [],
|
_tabs: [],
|
||||||
_activeTabId: null,
|
_activeTabId: null,
|
||||||
|
|||||||
Reference in New Issue
Block a user