`;
events.slice(0, 10).forEach((ev) => {
const time = new Date(ev.timestamp).toLocaleTimeString();
const typeLabels = {
index_updated: "Mise à jour",
index_reloaded: "Rechargement",
vault_added: "Vault ajouté",
vault_removed: "Vault supprimé",
index_start: "Démarrage index.",
index_progress: "Vault indexé",
index_complete: "Indexation tech.",
};
const label = typeLabels[ev.type] || ev.type;
let detail = ev.data.vaults ? ev.data.vaults.join(", ") : (ev.data.vault || "");
if (ev.type === "index_start") detail = `${ev.data.total_vaults} vaults à traiter`;
if (ev.type === "index_progress") detail = `${ev.data.vault} (${ev.data.files} fichiers)`;
if (ev.type === "index_complete" && ev.data.total_files !== undefined) detail = `${ev.data.total_files} fichiers total`;
html += `
${label}
${detail}
${time}
`;
});
html += `
`;
}
panel.innerHTML = html;
}
// ---------------------------------------------------------------------------
// Find in Page Manager
// ---------------------------------------------------------------------------
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
}
}
};
// ---------------------------------------------------------------------------
// Init
// ---------------------------------------------------------------------------
async function init() {
initTheme();
initHeaderMenu();
initCustomDropdowns();
document.getElementById("theme-toggle").addEventListener("click", toggleTheme);
document.getElementById("header-logo").addEventListener("click", goHome);
initSearch();
initSidebarToggle();
initMobile();
initVaultContext();
initSidebarTabs();
initHelpModal();
initConfigModal();
initSidebarFilter();
initSidebarResize();
initEditor();
initSyncStatus();
initLoginForm();
initRecentTab();
RightSidebarManager.init();
FindInPageManager.init();
// Check auth status first
const authOk = await AuthManager.initAuth();
if (authOk) {
try {
await Promise.all([loadVaults(), loadTags()]);
// Check for popup mode query parameter
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get("popup") === "true") {
document.body.classList.add("popup-mode");
}
// Handle direct deep-link to file via #file=...
if (window.location.hash && window.location.hash.startsWith("#file=")) {
const hashVal = window.location.hash.substring(6);
const sepIndex = hashVal.indexOf(":");
if (sepIndex > -1) {
const vault = decodeURIComponent(hashVal.substring(0, sepIndex));
const path = decodeURIComponent(hashVal.substring(sepIndex + 1));
openFile(vault, path);
}
}
} catch (err) {
console.error("Failed to initialize ObsiGate:", err);
showToast("Erreur lors de l'initialisation");
}
}
safeCreateIcons();
}
document.addEventListener("DOMContentLoaded", init);
})();