import { debug, debugError } from './debug.js'; /** * SearchModal - Système de recherche modale avec raccourcis clavier * Inspiré des Command Palettes modernes (VSCode, Notion, etc.) */ class SearchModal { constructor() { this.modal = null; this.input = null; this.resultsContainer = null; this.isOpen = false; this.searchTimeout = null; this.selectedIndex = 0; this.results = []; this._keydownHandler = null; this.init(); } init() { this.createModal(); this.attachKeyboardShortcuts(); } createModal() { // Créer la modale this.modal = document.createElement('div'); this.modal.id = 'search-modal'; this.modal.className = 'search-modal'; this.modal.style.display = 'none'; this.modal.innerHTML = `
ESC
Recherche avancée
tag:projet Filtrer par tag
title:réunion Rechercher dans les titres
path:docs Rechercher dans les chemins
"phrase exacte" Recherche de phrase
`; document.body.appendChild(this.modal); // Références aux éléments this.input = this.modal.querySelector('.search-modal-input'); this.resultsContainer = this.modal.querySelector('.search-modal-results'); // Event listeners this.modal.querySelector('.search-modal-overlay').addEventListener('click', () => { this.close(); }); this.input.addEventListener('input', (e) => { this.handleSearch(e.target.value); }); this.input.addEventListener('keydown', (e) => { this.handleKeyNavigation(e); }); } attachKeyboardShortcuts() { this._keydownHandler = (event) => { // Ctrl+K ou Cmd+K pour ouvrir if ((event.ctrlKey || event.metaKey) && event.key === 'k') { event.preventDefault(); this.toggle(); } // Escape pour fermer if (event.key === 'Escape' && this.isOpen) { event.preventDefault(); this.close(); } }; document.addEventListener('keydown', this._keydownHandler); } handleSearch(query) { // Debounce de 300ms clearTimeout(this.searchTimeout); if (!query.trim()) { this.showHelp(); return; } this.showLoading(); this.searchTimeout = setTimeout(async () => { try { const response = await fetch(`/api/search?query=${encodeURIComponent(query)}`); const html = await response.text(); // Parser le HTML pour extraire les résultats const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); // Extraire les liens de résultats const resultLinks = doc.querySelectorAll('.search-result-link'); this.results = Array.from(resultLinks).map((link, index) => ({ index, element: link.cloneNode(true) })); if (this.results.length > 0) { this.renderResults(query); } else { this.showNoResults(query); } } catch (error) { console.error('[SearchModal] Erreur de recherche:', error); this.showError(); } }, 300); } handleKeyNavigation(event) { if (this.results.length === 0) return; switch (event.key) { case 'ArrowDown': event.preventDefault(); this.selectedIndex = Math.min(this.selectedIndex + 1, this.results.length - 1); this.updateSelection(); break; case 'ArrowUp': event.preventDefault(); this.selectedIndex = Math.max(this.selectedIndex - 1, 0); this.updateSelection(); break; case 'Enter': event.preventDefault(); this.selectResult(); break; } } updateSelection() { const resultItems = this.resultsContainer.querySelectorAll('.search-modal-result-item'); resultItems.forEach((item, index) => { if (index === this.selectedIndex) { item.classList.add('selected'); item.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); } else { item.classList.remove('selected'); } }); } selectResult() { if (this.results.length === 0) return; const selected = this.results[this.selectedIndex]; if (selected && selected.element) { // Déclencher le clic sur le lien htmx const path = selected.element.getAttribute('hx-get'); if (path) { // Utiliser htmx pour charger la note htmx.ajax('GET', path, { target: '#editor-container', swap: 'innerHTML' }); this.close(); } } } renderResults(query) { this.resultsContainer.innerHTML = ''; this.selectedIndex = 0; const resultsHeader = document.createElement('div'); resultsHeader.className = 'search-modal-results-header'; resultsHeader.innerHTML = ` ${this.results.length} résultat${this.results.length > 1 ? 's' : ''} `; this.resultsContainer.appendChild(resultsHeader); this.results.forEach((result, index) => { const item = document.createElement('div'); item.className = 'search-modal-result-item'; if (index === 0) item.classList.add('selected'); // Copier le contenu du lien de résultat const originalLink = result.element; const icon = originalLink.querySelector('.search-result-icon')?.textContent || '📄'; const title = originalLink.querySelector('.search-result-title')?.textContent || 'Sans titre'; const path = originalLink.querySelector('.search-result-path')?.textContent || ''; const snippet = originalLink.querySelector('.search-result-snippet')?.textContent || ''; const tags = Array.from(originalLink.querySelectorAll('.tag-pill')).map(t => t.textContent); const lastModified = originalLink.querySelector('.search-result-date')?.textContent || ''; item.innerHTML = `
${icon}
${this.highlightQuery(title, query)}
${path}
${snippet ? `
${this.highlightQuery(snippet, query)}
` : ''}
`; // Click handler item.addEventListener('click', () => { this.selectedIndex = index; this.selectResult(); }); // Hover handler item.addEventListener('mouseenter', () => { this.selectedIndex = index; this.updateSelection(); }); this.resultsContainer.appendChild(item); }); } highlightQuery(text, query) { if (!query || !text) return text; // Extraire les termes de recherche (ignore les préfixes tag:, title:, etc.) const terms = query.split(/\s+/) .filter(term => !term.includes(':')) .map(term => term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')); // Escape regex if (terms.length === 0) return text; const regex = new RegExp(`(${terms.join('|')})`, 'gi'); return text.replace(regex, '$1'); } showHelp() { this.results = []; this.resultsContainer.innerHTML = `
Recherche avancée
tag:projet Filtrer par tag
title:réunion Rechercher dans les titres
path:docs Rechercher dans les chemins
"phrase exacte" Recherche de phrase
`; } showLoading() { this.resultsContainer.innerHTML = `

Recherche en cours...

`; } showNoResults(query) { this.results = []; this.resultsContainer.innerHTML = `

Aucun résultat pour « ${this.escapeHtml(query)} »

Essayez d'autres mots-clés ou utilisez les filtres

`; } showError() { this.resultsContainer.innerHTML = `

Une erreur s'est produite lors de la recherche

`; } escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } open() { if (this.isOpen) return; this.isOpen = true; this.modal.style.display = 'flex'; // Animation requestAnimationFrame(() => { this.modal.classList.add('active'); }); // Focus sur l'input setTimeout(() => { this.input.focus(); this.input.select(); }, 100); // Reset this.selectedIndex = 0; this.showHelp(); } close() { if (!this.isOpen) return; this.isOpen = false; this.modal.classList.remove('active'); setTimeout(() => { this.modal.style.display = 'none'; this.input.value = ''; this.results = []; }, 200); } toggle() { if (this.isOpen) { this.close(); } else { this.open(); } } destroy() { if (this._keydownHandler) { document.removeEventListener('keydown', this._keydownHandler); this._keydownHandler = null; } if (this.modal && this.modal.parentNode) { this.modal.parentNode.removeChild(this.modal); } this.modal = null; this.input = null; this.resultsContainer = null; } } // Instance globale window.searchModal = null; // Initialisation automatique document.addEventListener('DOMContentLoaded', () => { window.searchModal = new SearchModal(); }); export { SearchModal };