1import {ApplicationController, useDebounce} from 'stimulus-use'; 2 3/** 4 * Champ de recherche : suggestions à la volée + bouton de réinitialisation (croix). 5 * 6 * Monté sur le `<form>` de la barre de recherche. Reprend les blocs 7 * « Suggestions de recherche » et « gestion de la croix » de l'ancien search.js. 8 * 9 * L'appel réseau à `/suggest` est debouncé (via stimulus-use) pour ne pas tirer 10 * une requête à chaque frappe ; la croix de clear, elle, reste instantanée. 11 */ 12/* stimulusFetch: 'lazy' */ 13export default class extends ApplicationController { 14 static debounces = ['fetchSuggestions']; 15 static targets = ['input', 'clearIcon', 'suggestions']; 16 static values = { suggestUrl: String }; 17 18 connect() { 19 useDebounce(this, { wait: 330 }); 20 this.applySuggestionBoxStyles(); 21 this.updateClearButton(); 22 23 this.onDocumentClick = this.onDocumentClick.bind(this); 24 document.addEventListener('click', this.onDocumentClick); 25 } 26 27 disconnect() { 28 document.removeEventListener('click', this.onDocumentClick); 29 } 30 31 // action : input->search--autocomplete#input 32 input() { 33 this.updateClearButton(); // immédiat 34 this.fetchSuggestions(); // debouncé (cf. static debounces) 35 } 36 37 async fetchSuggestions() { 38 const query = this.inputTarget.value.trim(); 39 40 if (query.length < 2) { 41 this.hideSuggestions(); 42 return; 43 } 44 45 try { 46 const response = await fetch(`${this.suggestUrlValue}?prefix=${encodeURIComponent(query)}`); 47 const suggestions = await response.json(); 48 this.renderSuggestions(suggestions); 49 } catch (error) { 50 console.error('Erreur lors de la récupération des suggestions:', error); 51 } 52 } 53 54 // action : click->search--autocomplete#clear (sur la croix) 55 clear() { 56 this.inputTarget.value = ''; 57 this.updateClearButton(); 58 this.inputTarget.focus(); 59 } 60 61 renderSuggestions(suggestions) { 62 const box = this.suggestionsTarget; 63 64 if (!suggestions.length) { 65 this.hideSuggestions(); 66 return; 67 } 68 69 box.innerHTML = suggestions 70 .map((item) => `<div class="suggestion-item">${item}</div>`) 71 .join(''); 72 box.style.display = 'block'; 73 box.style.width = `${this.inputTarget.offsetWidth}px`; 74 box.style.left = `${this.inputTarget.offsetLeft}px`; 75 box.style.top = `${this.inputTarget.offsetTop + this.inputTarget.offsetHeight}px`; 76 77 box.querySelectorAll('.suggestion-item').forEach((item) => { 78 item.style.cursor = 'pointer'; 79 item.addEventListener('click', () => { 80 this.inputTarget.value = item.textContent; 81 this.hideSuggestions(); 82 this.element.submit(); 83 }); 84 }); 85 } 86 87 hideSuggestions() { 88 this.suggestionsTarget.style.display = 'none'; 89 this.suggestionsTarget.innerHTML = ''; 90 } 91 92 onDocumentClick(event) { 93 if (!this.suggestionsTarget.contains(event.target) && event.target !== this.inputTarget) { 94 this.suggestionsTarget.style.display = 'none'; 95 } 96 } 97 98 updateClearButton() { 99 if (!this.hasClearIconTarget) return; 100 const hasValue = this.inputTarget.value.trim().length > 0; 101 this.clearIconTarget.classList.toggle('hide', !hasValue); 102 } 103 104 applySuggestionBoxStyles() { 105 const box = this.suggestionsTarget; 106 box.style.position = 'absolute'; 107 box.style.background = '#fff'; 108 box.style.border = '1px solid #ccc'; 109 box.style.display = 'none'; 110 box.style.zIndex = '1000'; 111 } 112}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.