1(function () { 2 3 // === UTILS =============================================================== 4 5 const debounce = (fn, delay = 250) => { 6 let t; 7 return (...args) => { 8 clearTimeout(t); 9 t = setTimeout(() => fn.apply(null, args), delay); 10 }; 11 }; 12 13 function stripTags(html = '') { 14 const doc = new DOMParser().parseFromString(String(html), 'text/html'); 15 return doc.body.textContent || ''; 16 } 17 18 function uniqueTerms(terms) { 19 const out = []; 20 const seen = new Set(); 21 for (const t of (terms || [])) { 22 const s = String(t).trim(); 23 if (!s) continue; 24 const k = s.toLowerCase(); 25 if (seen.has(k)) continue; 26 seen.add(k); 27 out.push(s); 28 } 29 return out; 30 } 31 32 function buildHighlightRegex(terms) { 33 const safe = uniqueTerms(terms) 34 .map(t => t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')) 35 .join('|'); 36 37 if (!safe) return null; 38 return new RegExp(`(${safe})`, 'gi'); 39 } 40 41 function makeHighlightedFragment(text, terms) { 42 const frag = document.createDocumentFragment(); 43 const plain = String(text || ''); 44 45 const regex = buildHighlightRegex(terms); 46 if (!regex) { 47 frag.appendChild(document.createTextNode(plain)); 48 return frag; 49 } 50 51 let lastIndex = 0; 52 let match; 53 54 while ((match = regex.exec(plain)) !== null) { 55 const start = match.index; 56 const end = start + match[0].length; 57 58 if (start > lastIndex) { 59 frag.appendChild(document.createTextNode(plain.slice(lastIndex, start))); 60 } 61 62 const mark = document.createElement('mark'); 63 mark.textContent = plain.slice(start, end); 64 frag.appendChild(mark); 65 66 lastIndex = end; 67 68 // Protezione anti-loop (regex global + match vuoti) 69 if (regex.lastIndex === start) regex.lastIndex++; 70 } 71 72 if (lastIndex < plain.length) { 73 frag.appendChild(document.createTextNode(plain.slice(lastIndex))); 74 } 75 76 return frag; 77 } 78 79 function parseTerms(q) { 80 return q 81 .trim() 82 .split(/\s+/) 83 .filter(t => t.length > 1); 84 } 85 86 // === RENDER ============================================================== 87 function renderResults(container, items, terms) { 88 container.textContent = ''; 89 90 if (!items || !items.length) { 91 const empty = document.createElement('div'); 92 empty.className = 'livenetwork-live-search__empty'; 93 empty.textContent = 'Nessun risultato'; 94 container.appendChild(empty); 95 return; 96 } 97 98 const frag = document.createDocumentFragment(); 99 100 for (const item of items) { 101 const url = (item && item.url) ? String(item.url) : '#'; 102 const thumb = (item && item.thumb) ? String(item.thumb).trim() : ''; 103 104 const hasThumb = thumb.length > 0; 105 106 const a = document.createElement('a'); 107 a.className = 'livenetwork-live-search__item' + (hasThumb ? '' : ' is-no-thumb'); 108 a.setAttribute('role', 'option'); 109 a.setAttribute('aria-selected', 'false'); 110 a.href = url; // DOM API: non rompe HTML anche se url ha caratteri strani 111 112 const textWrap = document.createElement('div'); 113 textWrap.className = 'livenetwork-live-search__text'; 114 115 // category 116 if (item && item.category) { 117 const cat = document.createElement('div'); 118 cat.className = 'livenetwork-live-search__cat'; 119 cat.textContent = stripTags(item.category); 120 textWrap.appendChild(cat); 121 } 122 123 // title (highlight sicuro) 124 const title = document.createElement('div'); 125 title.className = 'livenetwork-live-search__title'; 126 title.appendChild(makeHighlightedFragment(stripTags(item && item.title ? item.title : ''), terms)); 127 textWrap.appendChild(title); 128 129 // meta 130 if (item && item.date_h) { 131 const meta = document.createElement('div'); 132 meta.className = 'livenetwork-live-search__meta'; 133 meta.textContent = stripTags(item.date_h); 134 textWrap.appendChild(meta); 135 } 136 137 // excerpt (highlight sicuro) 138 const excerpt = document.createElement('div'); 139 excerpt.className = 'livenetwork-live-search__excerpt'; 140 excerpt.appendChild(makeHighlightedFragment(stripTags(item && item.excerpt ? item.excerpt : ''), terms)); 141 textWrap.appendChild(excerpt); 142 143 a.appendChild(textWrap); 144 145 // thumb 146 if (hasThumb) { 147 const img = document.createElement('img'); 148 img.className = 'livenetwork-live-search__thumb'; 149 img.alt = ''; 150 img.src = thumb; // DOM API 151 a.appendChild(img); 152 } 153 154 frag.appendChild(a); 155 } 156 157 container.appendChild(frag); 158 } 159 160 // === INIT ================================================================ 161 function initLiveSearch(root) { 162 const input = root.querySelector('.livenetwork-live-search__input'); 163 const resultsEl = root.querySelector('.livenetwork-live-search__results'); 164 const spinner = root.querySelector('.livenetwork-live-search__spinner'); 165 const form = root.querySelector('.live-search-form'); 166 const endpoint = root.dataset.endpoint; 167 168 const perPage = parseInt(root.dataset.perPage || '10', 10); 169 const minChars = parseInt(root.dataset.minChars || '2', 10); 170 171 if (!input || !resultsEl || !endpoint) return; 172
173 let controller = null; 174 let requestSeq = 0; 175 176 const resetUI = () => { 177 resultsEl.textContent = ''; 178 spinner && spinner.classList.remove('is-loading'); 179 input.setAttribute('aria-expanded', 'false'); 180 resultsEl.removeAttribute('aria-busy'); 181 }; 182 183 const doSearch = debounce(async (q) => { 184 q = (q || '').trim(); 185 186 if (q.length < minChars) { 187 if (controller) controller.abort(); 188 resetUI(); 189 return; 190 } 191 192 const terms = parseTerms(q); 193 194 // abort precedente + seq 195 if (controller) controller.abort(); 196 controller = new AbortController(); 197 const mySeq = ++requestSeq; 198 199 spinner && spinner.classList.add('is-loading'); 200 input.setAttribute('aria-expanded', 'true'); 201 resultsEl.setAttribute('aria-busy', 'true'); 202 203 try { 204 const url = `${endpoint}?s=${encodeURIComponent(q)}&per_page=${perPage}`; 205 const res = await fetch(url, { 206 credentials: 'same-origin', 207 signal: controller.signal 208 }); 209 210 if (mySeq !== requestSeq) return; 211 212 const data = await res.json(); 213 renderResults(resultsEl, data, terms); 214 215 } catch (e) { 216 if (e && e.name === 'AbortError') return; 217 218 resultsEl.textContent = ''; 219 const err = document.createElement('div'); 220 err.className = 'livenetwork-live-search__empty'; 221 err.textContent = 'Errore nella ricerca'; 222 resultsEl.appendChild(err); 223 224 } finally { 225 if (mySeq === requestSeq) { 226 spinner && spinner.classList.remove('is-loading'); 227 resultsEl.removeAttribute('aria-busy'); 228 } 229 } 230 }, 250); 231 232 input.addEventListener('input', (e) => doSearch(e.target.value)); 233 input.addEventListener('focus', (e) => doSearch(e.target.value)); 234 235 input.addEventListener('keydown', (e) => { 236 if (e.key === 'Enter') { 237 e.preventDefault(); 238 const q = input.value.trim(); 239 if (q.length >= minChars) { 240 window.location.href = `/search/?q=${encodeURIComponent(q)}&page=1`; 241 } 242 } 243 }); 244 245 if (form) { 246 form.addEventListener('submit', (e) => { 247 e.preventDefault(); 248 const q = input.value.trim(); 249 if (!q.length) return; 250 window.location.href = `/search/?q=${encodeURIComponent(q)}&page=1`; 251 }); 252 } 253 } 254 255 document.addEventListener('DOMContentLoaded', () => { 256 document.querySelectorAll('.livenetwork-live-search').forEach(initLiveSearch); 257 }); 258 259})();
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.