/** * LinkInserter - Modal de recherche pour insérer des liens vers d'autres notes * Intégré dans l'éditeur CodeMirror 6 */ class LinkInserter { constructor() { this.modal = null; this.input = null; this.resultsContainer = null; this.isOpen = false; this.searchTimeout = null; this.selectedIndex = 0; this.results = []; this.callback = null; // Fonction appelée quand un lien est sélectionné this.editorView = null; // Référence à l'instance CodeMirror this.init(); } init() { this.createModal(); } createModal() { // Créer la modale (plus compacte que SearchModal) this.modal = document.createElement('div'); this.modal.id = 'link-inserter-modal'; this.modal.className = 'link-inserter-modal'; this.modal.style.display = 'none'; this.modal.innerHTML = ` `; document.body.appendChild(this.modal); // Références aux éléments this.input = this.modal.querySelector('.link-inserter-input'); this.resultsContainer = this.modal.querySelector('.link-inserter-results'); // Event listeners this.modal.querySelector('.link-inserter-overlay').addEventListener('click', () => { this.close(); }); this.input.addEventListener('input', (e) => { this.handleSearch(e.target.value); }); this.input.addEventListener('keydown', (e) => { this.handleKeyNavigation(e); }); } handleSearch(query) { // Debounce de 200ms (plus rapide que SearchModal) clearTimeout(this.searchTimeout); if (!query.trim()) { this.showHelp(); return; } this.showLoading(); this.searchTimeout = setTimeout(async () => { try { // Utiliser l'API de recherche existante 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) => { const title = link.querySelector('.search-result-title')?.textContent || 'Sans titre'; const path = link.getAttribute('hx-get')?.replace('/api/notes/', '') || ''; const tags = Array.from(link.querySelectorAll('.tag-pill')).map(t => t.textContent); const pathDisplay = link.querySelector('.search-result-path')?.textContent || ''; return { index, title: title.trim(), path: path.trim(), pathDisplay: pathDisplay.trim(), tags }; }); if (this.results.length > 0) { this.renderResults(query); } else { this.showNoResults(query); } } catch (error) { console.error('[LinkInserter] Erreur de recherche:', error); this.showError(); } }, 200); } handleKeyNavigation(event) { console.log('[LinkInserter] Key pressed:', event.key, 'Results:', this.results.length); if (this.results.length === 0) { if (event.key === 'Escape') { event.preventDefault(); this.close(); } return; } switch (event.key) { case 'ArrowDown': console.log('[LinkInserter] Arrow Down - moving to index:', this.selectedIndex + 1); event.preventDefault(); this.selectedIndex = Math.min(this.selectedIndex + 1, this.results.length - 1); this.updateSelection(); break; case 'ArrowUp': console.log('[LinkInserter] Arrow Up - moving to index:', this.selectedIndex - 1); event.preventDefault(); this.selectedIndex = Math.max(this.selectedIndex - 1, 0); this.updateSelection(); break; case 'Enter': console.log('[LinkInserter] Enter pressed - calling selectResult()'); event.preventDefault(); this.selectResult(); break; case 'Escape': console.log('[LinkInserter] Escape pressed - closing modal'); event.preventDefault(); this.close(); break; } } updateSelection() { const resultItems = this.resultsContainer.querySelectorAll('.link-inserter-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() { console.log('[LinkInserter] selectResult called, results:', this.results.length); if (this.results.length === 0) { console.warn('[LinkInserter] No results to select'); return; } const selected = this.results[this.selectedIndex]; console.log('[LinkInserter] Selected:', selected); console.log('[LinkInserter] Callback exists:', !!this.callback); if (selected && this.callback) { console.log('[LinkInserter] Calling callback with:', { title: selected.title, path: selected.path }); // Sauvegarder le callback localement avant de fermer const callback = this.callback; // Fermer le modal d'abord this.close(); // Puis appeler le callback après un petit délai pour que le modal se ferme proprement setTimeout(() => { console.log('[LinkInserter] Executing callback now...'); callback({ title: selected.title, path: selected.path }); }, 50); } else { console.error('[LinkInserter] Cannot select: no callback or no selected item'); this.close(); } } renderResults(query) { this.resultsContainer.innerHTML = ''; this.selectedIndex = 0; const resultsHeader = document.createElement('div'); resultsHeader.className = 'link-inserter-results-header'; resultsHeader.innerHTML = ` ${this.results.length} note${this.results.length > 1 ? 's' : ''} `; this.resultsContainer.appendChild(resultsHeader); this.results.forEach((result, index) => { const item = document.createElement('div'); item.className = 'link-inserter-result-item'; if (index === 0) item.classList.add('selected'); item.innerHTML = ` `; // Click handler item.addEventListener('click', (e) => { console.log('[LinkInserter] Item clicked, index:', index); e.preventDefault(); e.stopPropagation(); 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; const terms = query.split(/\s+/) .filter(term => !term.includes(':')) .map(term => term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')); if (terms.length === 0) return text; const regex = new RegExp(`(${terms.join('|')})`, 'gi'); return text.replace(regex, '$1'); } showHelp() { this.results = []; this.resultsContainer.innerHTML = ` `; } showLoading() { this.resultsContainer.innerHTML = ` `; } showNoResults(query) { this.results = []; this.resultsContainer.innerHTML = ` `; } showError() { this.resultsContainer.innerHTML = ` `; } escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } /** * Ouvrir le modal de sélection de lien * @param {Object} options - Options d'ouverture * @param {EditorView} options.editorView - Instance CodeMirror * @param {Function} options.onSelect - Callback appelé avec {title, path} */ open({ editorView, onSelect }) { console.log('[LinkInserter] open() called with callback:', !!onSelect); if (this.isOpen) return; this.editorView = editorView; this.callback = onSelect; 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 = []; this.callback = null; this.editorView = null; }, 200); } destroy() { if (this.modal && this.modal.parentNode) { this.modal.parentNode.removeChild(this.modal); } this.modal = null; this.input = null; this.resultsContainer = null; } } // Instance globale window.linkInserter = null; // Initialisation automatique document.addEventListener('DOMContentLoaded', () => { window.linkInserter = new LinkInserter(); }); export { LinkInserter };