PageSourceSearch

https://zaarastore.tn/wp-content/themes/zaara-modernist/assets/js/theme.js?v=1.4.3

js zaarastore.tn collected 2026-10-02 01:09:22 UTC 12,876 bytes, 322 lines download raw bytes

1(function ($) {
2	'use strict';
3
4	// Hero flash-sale countdown — computed from a real date_on_sale_to
5	// timestamp (see template-parts/home/hero.php), not a fake fixed clock.
6	function tickCountdowns() {
7		document.querySelectorAll('[data-zs-countdown]').forEach(function (el) {
8			var end = parseInt(el.getAttribute('data-zs-countdown'), 10) * 1000;
9			var diff = Math.max(0, end - Date.now());
10			var h = Math.floor(diff / 3600000);
11			var m = Math.floor((diff % 3600000) / 60000);
12			var s = Math.floor((diff % 60000) / 1000);
13			var pad = function (n) { return String(n).padStart(2, '0'); };
14			var hEl = el.querySelector('[data-unit="h"]');
15			var mEl = el.querySelector('[data-unit="m"]');
16			var sEl = el.querySelector('[data-unit="s"]');
17			if (hEl) hEl.textContent = pad(h);
18			if (mEl) mEl.textContent = pad(m);
19			if (sEl) sEl.textContent = pad(s);
20		});
21	}
22	if (document.querySelector('[data-zs-countdown]')) {
23		tickCountdowns();
24		setInterval(tickCountdowns, 1000);
25	}
26
27	// Toast on WooCommerce's own "added_to_cart" event (real add-to-cart,
28	// AJAX or not) — mirrors the mock's toast without faking the event.
29	var toastTimer = null;
30	function showToast(message) {
31		var toast = document.getElementById('zs-toast');
32		if (!toast) return;
33		toast.querySelector('.zs-toast__msg').textContent = message;
34		toast.classList.add('is-visible');
35		clearTimeout(toastTimer);
36		toastTimer = setTimeout(function () {
37			toast.classList.remove('is-visible');
38		}, 2200);
39	}
40	$(document.body).on('added_to_cart', function (event, fragments, cart_hash, $button) {
41		var name = $button && $button.closest('.zs-product-card, .zs-buybox').find('.zs-product-card__title, .zs-buybox__title').first().text().trim();
42		showToast((name || '') + (name ? ' — ' : '') + (window.zaaraStrings ? window.zaaraStrings.addedToCart : 'ajouté au panier'));
43	});
44
45	// Shop sidebar: anything that opts in submits the filter form on change.
46	// Deliberately opt-in only. Every checkbox in .zs-filters used to submit on
47	// change, which made the brand and rayon groups single-select in practice:
48	// ticking a second box reloaded the page before the first tick was sent.
49	// The groups are multi-select and the form has a "Filtrer" button.
50	document.querySelectorAll('[data-zs-autosubmit]').forEach(function (el) {
51		el.addEventListener('change', function () {
52			if (!el.form) return;
53			el.form.requestSubmit ? el.form.requestSubmit() : el.form.submit();
54		});
55	});
56
57	// Price slider drives the "Max" number input rather than submitting: a
58	// range fires change on every step, so one drag was dozens of page loads.
59	document.querySelectorAll('[data-zs-range-for]').forEach(function (range) {
60		var target = document.getElementById(range.getAttribute('data-zs-range-for'));
61		if (!target) return;
62		var out = range.closest('.zs-filters__group');
63		out = out && out.querySelector('[data-zs-range-out]');
64		var sync = function () {
65			target.value = range.value;
66			if (out) out.textContent = range.value;
67		};
68		range.addEventListener('input', sync);
69		target.addEventListener('input', function () {
70			var v = parseFloat(target.value);
71			if (!isNaN(v)) range.value = v;
72		});
73	});
74
75	// Type-to-narrow for the long term lists — the filter sidebar's rayon and
76	// marque groups and the "Tous les rayons" drawer. Both sides of the compare
77	// are already lower-cased and accent-stripped (see zaara_search_key()).
78	document.querySelectorAll('[data-zs-listfilter]').forEach(function (input) {
79		var list = document.getElementById(input.getAttribute('data-zs-listfilter'));
80		if (!list) return;
81
82		var items = list.querySelectorAll('[data-zs-listfilter-item]');
83		var headings = list.querySelectorAll('[data-zs-listfilter-heading]');
84		var empty = list.querySelector('[data-zs-listfilter-empty]');
85
86		var apply = function () {
87			var q = input.value.trim().toLowerCase()
88				.normalize('NFD').replace(RegExp('[\u0300-\u036f]', 'g'), '');
89			var shown = 0;
90			items.forEach(function (item) {
91				var hit = !q || item.getAttribute('data-zs-listfilter-item').indexOf(q) !== -1;
92				item.hidden = !hit;
93				if (hit) shown++;
94			});
95			// Section headings and the brand chip strip are not searchable rows;
96			// hide them while a query is active so the results read as one list.
97			headings.forEach(function (el) { el.hidden = !!q; });
98			if (empty) empty.hidden = shown !== 0;
99		};
100
101		input.addEventListener('input', apply);
102		input.addEventListener('search', apply);
103	});
104
105	// Departments off-canvas. The bar only holds a handful of departments; the
106	// drawer carries the full list, so it has to work without JS-heavy deps.
107	var offcanvasOpener = null;
108
109	function openOffcanvas(panel, opener) {
110		panel.hidden = false;
111		// Next frame, so the transition runs from the hidden state.
112		requestAnimationFrame(function () { panel.classList.add('is-open'); });
113		document.body.style.overflow = 'hidden';
114		offcanvasOpener = opener || null;
115		if (opener) opener.setAttribute('aria-expanded', 'true');
116		var focusable = panel.querySelector('.zs-offcanvas__close');
117		if (focusable) focusable.focus();
118	}
119
120	function closeOffcanvas(panel) {
121		panel.classList.remove('is-open');
122		document.body.style.overflow = '';
123		if (offcanvasOpener) {
124			offcanvasOpener.setAttribute('aria-expanded', 'false');
125			offcanvasOpener.focu
125s();
126			offcanvasOpener = null;
127		}
128		var done = function () { panel.hidden = true; panel.removeEventListener('transitionend', done); };
129		panel.addEventListener('transitionend', done);
130		// Fallback for when the panel isn't transitioned (reduced motion, etc.).
131		setTimeout(function () { if (!panel.classList.contains('is-open')) panel.hidden = true; }, 400);
132	}
133
134	document.addEventListener('click', function (e) {
135		var opener = e.target.closest('[data-zs-offcanvas-open]');
136		if (opener) {
137			var panel = document.getElementById(opener.getAttribute('data-zs-offcanvas-open'));
138			if (panel) { e.preventDefault(); openOffcanvas(panel, opener); }
139			return;
140		}
141		var closer = e.target.closest('[data-zs-offcanvas-close]');
142		if (closer) {
143			var open = closer.closest('.zs-offcanvas');
144			if (open) { e.preventDefault(); closeOffcanvas(open); }
145		}
146	});
147
148	document.addEventListener('keydown', function (e) {
149		if (e.key !== 'Escape') return;
150		var open = document.querySelector('.zs-offcanvas.is-open');
151		if (open) closeOffcanvas(open);
152	});
153
154	// Shop archive: infinite scroll, layered over the real pagination.
155	//
156	// The paginated links stay in the DOM and keep working without JS — they
157	// are how crawlers reach pages 2..n. This only appends their contents in
158	// place. Auto-loading stops after a few pages so the footer stays
159	// reachable and the back button still means something; from there it is
160	// the explicit "Charger plus" button, which is also the no-JS-observer
161	// fallback.
162	(function () {
163		var loop = document.querySelector('[data-zs-infinite]');
164		if (!loop || !window.fetch || !window.DOMParser) return;
165
166		var list = loop.querySelector('ul.products');
167		var status = loop.querySelector('.zs-loop__status');
168		var moreWrap = loop.querySelector('.zs-loop__more');
169		var moreBtn = loop.querySelector('.zs-loop__more-btn');
170		var pagination = loop.querySelector('.zs-pagination');
171		if (!list || !moreBtn) return;
172
173		var nextUrl = loop.getAttribute('data-zs-next') || '';
174		var page = parseInt(loop.getAttribute('data-zs-page'), 10) || 1;
175		var maxPages = parseInt(loop.getAttribute('data-zs-max'), 10) || 1;
176		var AUTO_LIMIT = 3; // pages fetched automatically before asking
177		var autoLoaded = 0;
178		var busy = false;
179
180		var strings = window.zaaraStrings || {};
181		var say = function (msg) { if (status) status.textContent = msg; };
182
183		function showButton() {
184			if (moreWrap) moreWrap.hidden = !nextUrl;
185		}
186
187		function done() {
188			nextUrl = '';
189			if (moreWrap) moreWrap.hidden = true;
190			if (observer) observer.disconnect();
191			say(strings.allLoaded || 'Tous les produits sont affichés.');
192		}
193
194		function load() {
195			if (busy || !nextUrl) return;
196			busy = true;
197			moreBtn.disabled = true;
198			say(strings.loading || 'Chargement…');
199
200			var url = nextUrl;
201			fetch(url, { credentials: 'same-origin' })
202				.then(function (r) {
203					if (!r.ok) throw new Error(r.status);
204					return r.text();
205				})
206				.then(function (html) {
207					var doc = new DOMParser().parseFromString(html, 'text/html');
208					var incoming = doc.querySelectorAll('ul.products > li.product');
209					if (!incoming.length) { done(); return; }
210
211					var frag = document.createDocumentFragment();
212					Array.prototype.forEach.call(incoming, function (li) {
213						frag.appendChild(document.importNode(li, true));
214					});
215					list.appendChild(frag);
216
217					page++;
218					// Take the next link from the page just fetched rather than
219					// guessing a URL, so it stays correct with filters applied.
220					var nextLoop = doc.querySelector('[data-zs-infinite]');
221					nextUrl = nextLoop ? (nextLoop.getAttribute('data-zs-next') || '') : '';
222
223					// Keep the crawlable pagination in step with what is shown.
224					var nextPagination = doc.querySelector('.zs-pagination');
225					if (pagination && nextPagination) pagination.innerHTML = nextPagination.innerHTML;
226
227					// The address bar follows the content, so a reload or a
228					// shared link lands where the shopper actually was.
229					if (window.history && window.history.replaceState) {
230						try { window.history.replaceState({}, '', url); } catch (e) {}
231					}
232
233					say((strings.loadedPage || 'Page %d chargée.').replace('%d', page));
234					$(document.body).trigger('zaara_products_appended', [list]);
235
236					if (!nextUrl || page >= maxPages) { done(); return; }
237
238					autoLoaded++;
239					if (autoLoaded >= AUTO_LIMIT) {
240						if (observer) observer.disconnect();
241						showButton();
242					}
243				})
244				.catch(function () {
245					// Leave the real pagination visible: it still works.
246					say(strings.loadError || 'Impossible de charger la suite. Utilisez la pagination ci-dessous.');
247					showButton();
248				})
249				.then(function () {
250					busy = false;
251					moreBtn.disabled = false;
252				});
253		}
254
255		moreBtn.addEventListener('click', function () {
256			if (observer) observer.disconnect();
257			load();
258		});
259
260		var observer = null;
261		var sentinel = loop.querySelector('.zs-loop__sentinel');
262
263		if ('IntersectionObserver' in window && nextUrl && sentinel) {
264			// A generous margin is what makes this read as infinite scroll
265			// rather than a stall at the bottom: the next page is already on
266			// its way well before the shopper reaches the end of this one.
267			observer = new IntersectionObserver(function (entries) {
268				entries.forEach(function (entry) { if (entry.isIntersecting) load(); });
269			}, { rootMargin: '1200px 0px' });
270			observer.observe(sentinel);
271			// The button stays hidden while auto-loading is doing the work —
272			// showing it up front made this look like a manual "load more".
273		} else {
274			showButton();
275		}
276	})();
277
278	// Product page: the sections below the buy box are <details> the shopper can
279	// fold. A jump link, WooCommerce's "n avis" link or a #hash in the address
280	// has to open the section it points into, or it scrolls to a closed bar.
281	function openSection(hash) {
282		if (!hash || hash.length < 2) return;
283		var target;
284		try { target = document.getElementById(decodeURIComponent(hash.slice(1))); } catch (e) { return; }
285		var details = target && (target.tagName === 'DETAILS' ? target : target.closest('details'));
286		if (details && !details.open) details.open = true;
287	}
288	document.addEventListener('click', function (e) {
289		var link = e.target.closest('a[href^="#"]');
290		if (link) openSection(link.getAttribute('href'));
291	});
292	window.addEventListener('hashchange', function () { openSection(location.hash); });
293	openSection(location.hash);
294
295	// Cart page: qty stepper buttons adjust the real WooCommerce quantity
296	// input, then submit the cart form (classic template = full-page
297	// update, same as clicking "Update cart").
298	document.addEventListener('click', function (e) {
299		var btn = e.target.closest('.quantity .plus, .quantity .minus');
300		if (!btn) return;
301		e.preventDefault();
302		var input = btn.parentElement.querySelector('input.qty');
303		if (!input) return;
304		var step = parseFloat(input.getAttribute('step')) || 1;
305		var min = parseFloat(input.getAttribute('min'));
306		var val = parseFloat(input.value) || 0;
307		val = btn.classList.contains('plus') ? val + step : val - step;
308		if (!isNaN(min)) val = Math.max(min, val);
309		input.value = val;
310		input.dispatchEvent(new Event('change', { bubbles: true }));
311	});
312	var cartQtyTimer = null;
313	document.addEventListener('change', function (e) {
314		if (e.target.matches && e.target.matches('.woocommerce-cart-form input.qty')) {
315			clearTimeout(cartQtyTimer);
316			cartQtyTimer = setTimeout(function () {
317				var form = e.target.closest('form');
318				if (form) form.requestSubmit ? form.requestSubmit() : form.submit();
319			}, 500);
320		}
321	});
322})(jQuery);

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.