PageSourceSearch

https://www.saporinostri.it/app/plugins/saporinostri-core/assets/js/live-search.js?ver=1779959404

js saporinostri.it collected 2026-10-02 12:37:20 UTC 9,875 bytes, 347 lines download raw bytes

1(function () {
2	'use strict';
3
4	if (!window.snCoreLiveSearch) {
5		return;
6	}
7
8	var config = window.snCoreLiveSearch;
9
10	function debounce(fn, delay) {
11		var timer = null;
12		var debounced = function () {
13			var ctx = this;
14			var args = arguments;
15			window.clearTimeout(timer);
16			timer = window.setTimeout(function () {
17				fn.apply(ctx, args);
18			}, delay);
19		};
20		debounced.cancel = function () {
21			window.clearTimeout(timer);
22			timer = null;
23		};
24		return debounced;
25	}
26
27	function createPanel(form) {
28		var inner = form.querySelector('.header-search-inner');
29		if (!inner) {
30			return null;
31		}
32
33		var panel = document.createElement('div');
34		panel.className = 'sn-live-search';
35		panel.hidden = true;
36		panel.innerHTML = '<div class="sn-live-search__content"></div>';
37		form.appendChild(panel);
38		return panel;
39	}
40
41	function escapeHtml(value) {
42		return String(value)
43			.replace(/&/g, '&amp;')
44			.replace(/</g, '&lt;')
45			.replace(/>/g, '&gt;')
46			.replace(/"/g, '&quot;')
47			.replace(/'/g, '&#039;');
48	}
49
50	function render(panel, data, query) {
51		var content = panel.querySelector('.sn-live-search__content');
52		if (!content) {
53			return;
54		}
55
56		var categories = Array.isArray(data.categories) ? data.categories : [];
57		var brands = Array.isArray(data.brands) ? data.brands : [];
58		var products = Array.isArray(data.products) ? data.products : [];
59
60		if (!categories.length && !brands.length && !products.length) {
61			content.innerHTML = '<div class="sn-live-search__empty">' + escapeHtml(config.i18n.noResults) + '</div>';
62			panel.hidden = false;
63			return;
64		}
65
66		var categoriesHtml = '';
67		if (categories.length) {
68			categoriesHtml += '<section class="sn-live-search__section">';
69			categoriesHtml += '<h4>' + escapeHtml(config.i18n.categories) + '</h4>';
70			categoriesHtml += '<ul class="sn-live-search__categories">';
71			categories.forEach(function (item) {
72				categoriesHtml += '<li><a href="' + escapeHtml(item.url) + '">' + escapeHtml(item.name) + '</a></li>';
73			});
74			categoriesHtml += '</ul></section>';
75		}
76
77		var brandsHtml = '';
78		if (brands.length) {
79			brandsHtml += '<section class="sn-live-search__section">';
80			brandsHtml += '<h4>' + escapeHtml(config.i18n.brands || 'Brand') + '</h4>';
81			brandsHtml += '<ul class="sn-live-search__brands">';
82			brands.forEach(function (item) {
83				brandsHtml += '<li><a href="' + escapeHtml(item.url) + '">' + escapeHtml(item.name) + '</a></li>';
84			});
85			brandsHtml += '</ul></section>';
86		}
87
88		var productsHtml = '';
89		if (products.length) {
90			productsHtml += '<section class="sn-live-search__section">';
91			productsHtml += '<h4>' + escapeHtml(config.i18n.products) + '</h4>';
92			productsHtml += '<ul class="sn-live-search__products">';
93			products.forEach(function (item) {
94				productsHtml += '<li>';
95				productsHtml += '<a class="sn-live-search__product" href="' + escapeHtml(item.url) + '">';
96				productsHtml += '<span class="sn-live-search__thumb"><img src="' + escapeHtml(item.image || '') + '" alt="' + escapeHtml(item.name) + '" loading="lazy"></span>';
97				productsHtml += '<span class="sn-live-search__meta">';
98				productsHtml += '<span class="sn-live-search__name">' + escapeHtml(item.name) + '</span>';
99				if (item.priceHtml) {
100					// priceHtml è HTML sicuro generato server-side da wc_price() (WooCommerce).
101					// Può contenere <del>, <ins>, <span> — non va escapato con escapeHtml().
102					productsHtml += '<span class="sn-live-search__price">' + item.priceHtml + '</span>';
103				}
104				productsHtml += '</span></a></li>';
105			});
106			productsHtml += '</ul></section>';
107		}
108
109		var allResultsUrl = data.allResultsUrl || ('/?s=' + encodeURIComponent(query) + '&post_type=product');
110		var hint = config.i18n.enterHint ? '<span class="sn-live-search__hint">' + escapeHtml(config.i18n.enterHint) + '</span>' : '';
111		var footerHtml = '<div class="sn-live-search__footer"><a href="' + escapeHtml(allResultsUrl) + '">' + escapeHtml(config.i18n.viewAll) + '</a>' + hint + '</div>';
112
113		content.innerHTML = '<div class="sn-live-search__groups">' + categoriesHtml + brandsHtml + productsHtml + '</div>' + footerHtml;
114		panel.hidden = false;
115	}
116
117	function setLoading(panel) {
118		var content = panel.querySelector('.sn-live-search__content');
119		if (!content) {
120			return;
121		}
122		content.innerHTML = '' +
123			'<div class="sn-live-search__groups">' +
124				'<section class="sn-live-search__section">' +
125					'<h4>' + escapeHtml(config.i18n.products) + '</h4>' +
126					'<div class="sn-live-search__skeleton-list">' +
127						'<div class="sn-live-search__skeleton-row"></div>' +
128						'<div class="sn-live-search__skeleton-row"></div>' +
129						'<div class="sn-live-search__skeleton-row"></div>' +
130					'</div>' +
131				'</section>' +
132			'</div>' +
133			'<div class="sn-live-search__loading">' + escapeHtml(config.i18n.loading) + '</div>';
134		panel.hidden = false;
135	}
136
137	function setError(panel, onRetry) {
138		var content = panel.querySelector('.sn-live-search__content');
139		if (!content) {
140			return;
141		}
142
143		content.innerHTML = '' +
144			'<div class="sn-live-search__error">' +
145				'<p>' + escapeHtml(config.i18n.error || 'Errore durante la ricerca.') + '</p>' +
146				'<button type="button" class="sn-live-search__retry">' + escapeHtml(config.i18n.retry || 'Riprova') + '</button>' +
147			'</div>';
148		panel.hidden = false;
149
150		var retry = content.querySelector('.sn-live-search__retry');
151		if (retry) {
152			retry.addEventListener('click', function () {
153				if (typeof onRetry === 'function') {
154					onRetry();
155				}
156			});
157		}
158	}
159
160	function hidePanel(panel) {
161		panel.hidden = true;
162	}
163
164	function bindForm(form) {
165		var input = form.querySelector('.search-field');
166		if (!input) {
167			return;
168		}
169
170		var panel = createPanel(form);
171		if (!panel) {
172			return;
173		}
174
175		var requestSeq = 0;
176		var activeXhr = null;
177		var activeIndex = -1;
178		var debounceMs = Math.max(240, Number(config.debounceMs || 260));
179		var lastQuery = '';
180		var fetchResults = null;
181
182		function getResultLinks() {
183			return Array.prototype.slice.call(panel.querySelectorAll('.sn-live-search__categories a, .sn-live-search__brands a, .sn-live-search__product'));
184		}
185
186		function clearActive() {
187			getResultLinks().forEach(function (link) {
188				link.classList.remove('is-active');
189			});
190		}
191
192		function setActive(index) {
193			var links = getResultLinks();
194			if (!links.length) {
195				activeIndex = -1;
196				return;
197			}
198
199			if (index < 0) {
200				index = links.length - 1;
201			}
202			if (index >= links.length) {
203				index = 0;
204			}
205
206			activeIndex = index;
207			clearActive();
208			links[activeIndex].classList.add('is-active');
209			links[activeIndex].scrollIntoView({ block: 'nearest' });
210		}
211
212		function dismissPanel(cancelRequest) {
213			if (fetchResults && typeof fetchResults.cancel === 'function') {
214				fetchResults.cancel();
215			}
216			if (cancelRequest) {
217				requestSeq += 1;
218				if (activeXhr && activeXhr.readyState !== 4) {
219					activeXhr.abort();
220				}
221				activeXhr = null;
222			}
223			activeIndex = -1;
224			clearActive();
225			hidePanel(panel);
226		}
227
228		function requestResults(query) {
229			if (query.length < Number(config.minChars || 2)) {
230				dismissPanel(true);
231				return;
232			}
233
234			lastQuery = query;
235
236			requestSeq += 1;
237			var currentRequest = requestSeq;
238			if (activeXhr && activeXhr.readyState !== 4) {
239				activeXhr.abort();
240			}
241			setLoading(panel);
242
243			var url = config.endpoint + '?q=' + encodeURIComponent(query) + '&limit=' + encodeURIComponent(String(config.maxResults || 8));
244			var xhr = new XMLHttpRequest();
245			activeXhr = xhr;
246			xhr.open('GET', url, true);
247			xhr.timeout = Number(config.timeoutMs || 10000);
248			xhr.setRequestHeader('Accept', 'application/json');
249			xhr.onreadystatechange = function () {
250				if (xhr.readyState !== 4 || currentRequest !== requestSeq) {
251					return;
252				}
253
254				if (xhr.status < 200 || xhr.status >= 300) {
255					setError(panel, function () {
256						requestResults(lastQuery);
257					});
258					return;
259				}
260
261				try {
262					var data = JSON.parse(xhr.responseText || '{}');
263					activeIndex = -1;
264					clearActive();
265					render(panel, data, query);
266				} catch (error) {
267					setError(panel, function () {
268						requestResults(lastQuery);
269					});
270				}
271			};
272			xhr.ontimeout = function () {
273				if (currentRequest !== requestSeq) {
274					return;
275				}
276				setError(panel, function () {
277					requestResults(lastQuery);
278				});
279			};
280			xhr.onerror = function () {
281				if (currentRequest !== requestSeq) {
282					return;
283				}
284				setError(panel, function () {
285					requestResults(lastQuery);
286				});
287			};
288			xhr.send();
289		}
290
291		fetchResults = debounce(function () {
292			var query = (input.value || '').trim();
293			requestResults(query);
294		}, debounceMs);
295
296		input.addEventListener('input', fetchResults);
297
298		input.addEventListener('focus', function () {
299			if ((input.value || '').trim().length >= Number(config.minChars || 2)) {
300				fetchResults();
301			}
302		});
303
304		document.addEventListener('click', function (event) {
305			if (!form.contains(event.target)) {
306				dismissPanel(true);
307			}
308		});
309
310		input.addEventListener('keydown', function (event) {
311			var key = event.key || '';
312
313			if (key === 'Escape' || key === 'Esc' || event.keyCode === 27) {
314				dismissPanel(true);
315				return;
316			}
317
318			if (panel.hidden) {
319				return;
320			}
321
322			if (key === 'ArrowDown' || key === 'Down' || event.keyCode === 40) {
323				event.preventDefault();
324				setActive(activeIndex + 1);
325				return;
326			}
327
328			if (key === 'ArrowUp' || key === 'Up' || event.keyCode === 38) {
329				event.preventDefault();
330				setActive(activeIndex - 1);
331				return;
332			}
333
334			if (key === 'Enter' || event.keyCode === 13) {
335				var links = getResultLinks();
336				if (activeIndex >= 0 && links[activeIndex]) {
337					event.preventDefault();
338					window.location.href = links[activeIndex].getAttribute('href');
339				}
340			}
341		});
342	}
343
344	document.addEventListener('DOMContentLoaded', function () {
345		document.querySelectorAll('form.header-search').forEach(bindForm);
346	});
347})();

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.