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.