1document.addEventListener('DOMContentLoaded', () => { 2 const config = window.decorfoodAiSearch || {}; 3 const endpoint = config.endpoint; 4 const forms = document.querySelectorAll('[data-ai-product-search]'); 5 6 if (!endpoint || !forms.length) { 7 return; 8 } 9 10 const debounce = (callback, wait = 260) => { 11 let timer = null; 12 13 return (...args) => { 14 window.clearTimeout(timer); 15 timer = window.setTimeout(() => callback(...args), wait); 16 }; 17 }; 18 19 const setStatus = (form, message) => { 20 const status = form.querySelector('[data-ai-search-status]'); 21 22 if (status) { 23 status.textContent = message || ''; 24 } 25 }; 26 27 const clearResults = (form) => { 28 const results = form.querySelector('[data-ai-search-results]'); 29 30 if (results) { 31 results.replaceChildren(); 32 } 33 }; 34 35 const createTextNode = (tagName, className, value) => { 36 const element = document.createElement(tagName); 37 38 if (className) { 39 element.className = className; 40 } 41 42 element.textContent = value || ''; 43 return element; 44 }; 45 46 const createResultCard = (product) => { 47 const link = document.createElement('a'); 48 link.className = 'ai-search-result'; 49 link.href = product.url; 50 51 const imageWrap = document.createElement('span'); 52 imageWrap.className = 'ai-search-result__image'; 53 54 const image = document.createElement('img'); 55 image.src = product.image; 56 image.alt = product.title || ''; 57 image.loading = 'lazy'; 58 imageWrap.appendChild(image); 59 60 const content = document.createElement('span'); 61 content.className = 'ai-search-result__content'; 62 content.appendChild(createTextNode('span', 'ai-search-result__meta', product.category || 'Decorfood Italy')); 63 content.appendChild(createTextNode('strong', 'ai-search-result__title', product.title)); 64 content.appendChild(createTextNode('span', 'ai-search-result__excerpt', product.excerpt)); 65 66 const price = createTextNode('span', 'ai-search-result__price', product.price); 67 68 link.append(imageWrap, content, price); 69 return link; 70 }; 71 72 const renderResults = (form, payload) => { 73 const results = form.querySelector('[data-ai-search-results]'); 74 75 if (!results) { 76 return; 77 } 78 79 results.replaceChildren(); 80 81 if (!payload.results || !payload.results.length) { 82 const empty = document.createElement('div'); 83 empty.className = 'ai-search-empty'; 84 empty.textContent = config.i18n?.empty || 'Nessun prodotto trovato.'; 85 results.appendChild(empty); 86 return; 87 } 88 89 payload.results.forEach((product) => { 90 results.appendChild(createResultCard(product)); 91 }); 92 93 if (payload.search_url && payload.total > payload.results.length) { 94 const more = document.createElement('a'); 95 more.className = 'ai-search-result ai-search-result--more'; 96 more.href = payload.search_url; 97 more.appendChild(createTextNode('span', 'ai-search-result__content', config.i18n?.allResults || 'Vedi tutti i risultati')); 98 more.appendChild(createTextNode('span', 'ai-search-result__price', String(payload.total))); 99 results.appendChild(more); 100 } 101 }; 102 103 const runSearch = async (form, query) => { 104 const trimmedQuery = query.trim(); 105 106 if (trimmedQuery.length < 2) { 107 setStatus(form, ''); 108 clearResults(form); 109 return; 110 } 111 112 if (form.decorfoodSearchAbort) { 113 form.decorfoodSearchAbort.abort(); 114 } 115 116 form.decorfoodSearchAbort = new AbortController(); 117 setStatus(form, config.i18n?.loading || 'Ricerca in corso...'); 118 119 try { 120 const url = new URL(endpoint); 121 url.searchParams.set('q', trimmedQuery); 122 url.searchParams.set('limit', '6'); 123 124 const response = await fetch(url.toString(), { 125 method: 'GET', 126 signal: form.decorfoodSearchAbort.signal, 127 headers: { 128 Accept: 'application/json', 129 'X-WP-Nonce': config.nonce || '', 130 }, 131 }); 132 133 if (!response.ok) { 134 throw new Error('Search failed'); 135 } 136 137 const payload = await response.json(); 138 renderResults(form, payload); 139 140 const count = payload.total || 0; 141 const label = count === 1 ? (config.i18n?.oneResult || '1 prodotto consigliato') : `${count} ${config.i18n?.results || 'prodotti consigliati'}`; 142 setStatus(form, count ? label : ''); 143 } catch (error) { 144 if (error.name === 'AbortError') { 145 return; 146 } 147 148 setStatus(form, config.i18n?.error || 'Ricerca momentaneamente non disponibile.'); 149 } 150 }; 151
152 forms.forEach((form) => { 153 const input = form.querySelector('[data-ai-search-input]'); 154 const suggestions = form.querySelectorAll('[data-ai-search-suggestion]'); 155 156 if (!input) { 157 return; 158 } 159 160 const debouncedSearch = debounce(() => runSearch(form, input.value)); 161 162 input.addEventListener('input', debouncedSearch); 163 164 form.addEventListener('submit', (event) => { 165 if (input.value.trim().length < 2) { 166 return; 167 } 168 169 event.preventDefault(); 170 runSearch(form, input.value); 171 }); 172 173 suggestions.forEach((button) => { 174 button.addEventListener('click', () => { 175 input.value = button.dataset.aiSearchSuggestion || ''; 176 input.focus(); 177 runSearch(form, input.value); 178 }); 179 }); 180 }); 181});
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.