1/* global DataFirefly */ 2/** 3 * DataFirefly â frontend behaviour. 4 */ 5( function () { 6 'use strict'; 7 8 const L10n = window.DataFirefly || {}; 9 10 /* ---------- Theme (light / dark) ---------- */ 11 function initTheme() { 12 const html = document.documentElement; 13 // Cookie lookup. 14 const m = document.cookie.match( /(?:^|;\s*)df_theme=([^;]+)/ ); 15 const stored = m ? m[ 1 ] : null; 16 // Respect user choice, else fall back to OS. 17 const initial = stored || ( window.matchMedia && window.matchMedia( '(prefers-color-scheme: dark)' ).matches ? 'dark' : 'light' ); 18 html.setAttribute( 'data-theme', initial ); 19 20 document.querySelectorAll( '[data-df-theme-toggle]' ).forEach( ( btn ) => { 21 btn.addEventListener( 'click', () => { 22 const cur = html.getAttribute( 'data-theme' ) === 'dark' ? 'light' : 'dark'; 23 html.setAttribute( 'data-theme', cur ); 24 document.cookie = 'df_theme=' + cur + ';path=/;max-age=31536000;SameSite=Lax'; 25 } ); 26 } ); 27 } 28 29 /* ---------- Language switcher ---------- */ 30 function initLangSwitcher() { 31 document.querySelectorAll( '.df-lang-switch' ).forEach( ( sw ) => { 32 const btn = sw.querySelector( '.df-lang-btn' ); 33 if ( ! btn ) { return; } 34 btn.addEventListener( 'click', ( e ) => { 35 e.preventDefault(); 36 sw.classList.toggle( 'is-open' ); 37 } ); 38 } ); 39 document.addEventListener( 'click', ( e ) => { 40 document.querySelectorAll( '.df-lang-switch.is-open' ).forEach( ( sw ) => { 41 if ( ! sw.contains( e.target ) ) { sw.classList.remove( 'is-open' ); } 42 } ); 43 } ); 44 } 45 46 /* ---------- Newsletter form ---------- */ 47 function initNewsletter() { 48 document.querySelectorAll( '[data-df-newsletter]' ).forEach( ( form ) => { 49 form.addEventListener( 'submit', ( e ) => { 50 e.preventDefault(); 51 const email = form.querySelector( 'input[type=email]' ).value.trim(); 52 const status = form.querySelector( '.df-news-status' ); 53 const btn = form.querySelector( 'button[type=submit]' ); 54 if ( ! email ) { return; } 55 status.textContent = L10n.i18n?.subscribing || 'Subscribingâ¦'; 56 status.classList.remove( 'is-ok', 'is-err' ); 57 btn.disabled = true; 58 fetch( ( L10n.restUrl || '/wp-json/datafirefly/v1/' ) + 'subscribe', { 59 method: 'POST', 60 credentials: 'same-origin', 61 headers: { 'Content-Type': 'application/json' }, 62 body: JSON.stringify( { email } ), 63 } ).then( ( r ) => r.json() ).then( ( j ) => { 64 if ( j.ok ) { 65 status.textContent = L10n.i18n?.subscribed || 'Thanks â keep an eye on your inbox.'; 66 status.classList.add( 'is-ok' ); 67 form.reset(); 68 } else { 69 status.textContent = j.error || ( L10n.i18n?.subscribeError || 'Could not subscribe. Please try again.' ); 70 status.classList.add( 'is-err' ); 71 } 72 } ).catch( () => { 73 status.textContent = L10n.i18n?.subscribeError || 'Could not subscribe. Please try again.'; 74 status.classList.add( 'is-err' ); 75 } ).finally( () => { 76 btn.disabled = false; 77 } ); 78 } ); 79 } ); 80 } 81 82 /* ---------- FAQ â close siblings when one opens ---------- */ 83 function initFaq() { 84 document.querySelectorAll( '.df-faq-list' ).forEach( ( list ) => { 85 list.addEventListener( 'toggle', ( e ) => { 86 const t = e.target; 87 if ( t.tagName === 'DETAILS' && t.open ) { 88 list.querySelectorAll( 'details[open]' ).forEach( ( d ) => { 89 if ( d !== t ) { d.open = false; } 90 } ); 91 } 92 }, true ); 93 } ); 94 } 95 96 /* ---------- Hero firefly twinkle (cheap) ---------- */ 97 function initFireflies() { 98 document.querySelectorAll( '.df-hero-fly' ).forEach( ( el, i ) => { 99 el.style.animationDelay = ( i * 0.8 ) + 's'; 100 } ); 101 } 102 103 /* ---------- Cart fragments (vanilla, sans wc-cart-fragments) ---------- * 104 * 105 * v1.24.1 â wc-cart-fragments n'est plus chargé (perf : jQuery + un POST 106 * get_refreshed_fragments sur chaque page vue). On applique nous-mêmes 107 * les fragments { 'sélecteur': 'html' } renvoyés par les endpoints 108 * wc-ajax (add_to_cart / remove_from_cart / get_refreshed_fragments). 109 */ 110 let dfApplyingFragments = false; // guard : évite la double application via l'écouteur de compat jQuery 111 const DF_FRAG_KEY = 'df_cart_fragments'; 112 113 function applyFragments( fragments, cartHash ) { 114 if ( ! fragments ) { return; } 115 dfApplyingFragments = true; 116 Object.keys( fragments ).forEach( ( selector ) => { 117 document.querySelectorAll( selector ).forEach( ( el ) => { 118 el.outerHTML = fragments[ selector ]; 119 } ); 120 } ); 121 dfApplyingFragments = false;
122 // Cache sessionStorage : les pages suivantes servies d'un cache 123 // pleine page pourront ré-appliquer sans requête (cf. initCartSync). 124 if ( undefined !== cartHash ) { 125 try { 126 sessionStorage.setItem( DF_FRAG_KEY, JSON.stringify( { hash: cartHash || '', fragments } ) ); 127 } catch ( e ) { /* quota / private mode â non bloquant */ } 128 } 129 document.body.dispatchEvent( new CustomEvent( 'df:fragments_applied', { detail: { fragments } } ) ); 130 } 131 132 function readCookie( name ) { 133 const m = document.cookie.match( new RegExp( '(?:^|;\\s*)' + name + '=([^;]+)' ) ); 134 return m ? decodeURIComponent( m[ 1 ] ) : ''; 135 } 136 137 /** 138 * Synchro au chargement : si la page vient d'un cache pleine page, le 139 * drawer / badge rendus côté serveur peuvent être périmés. Le cookie 140 * `woocommerce_cart_hash` (posé par WC à chaque changement de panier) 141 * fait foi : s'il diffère du hash rendu (L10n.cartHash), on ré-applique 142 * depuis sessionStorage si le hash y correspond (zéro requête), sinon 143 * on rafraîchit UNE fois via get_refreshed_fragments. Cas nominal (pas 144 * de panier, ou page rendue à jour) : rien. 145 */ 146 function initCartSync() { 147 const cookieHash = readCookie( 'woocommerce_cart_hash' ); 148 const renderedHash = L10n.cartHash || ''; 149 if ( cookieHash === renderedHash ) { 150 try { if ( ! cookieHash ) { sessionStorage.removeItem( DF_FRAG_KEY ); } } catch ( e ) {} 151 return; 152 } 153 try { 154 const stored = JSON.parse( sessionStorage.getItem( DF_FRAG_KEY ) || 'null' ); 155 if ( stored && stored.hash === cookieHash && stored.fragments ) { 156 applyFragments( stored.fragments ); 157 return; 158 } 159 } catch ( e ) { /* JSON corrompu â on refetch */ } 160 refreshCartFragments(); 161 } 162 163 async function refreshCartFragments() { 164 const drawer = document.querySelector( '[data-df-cart-drawer]' ); 165 if ( drawer ) { drawer.classList.add( 'is-loading' ); } 166 try { 167 const resp = await fetch( wcAjaxUrl( 'get_refreshed_fragments' ), { 168 method: 'POST', 169 credentials: 'same-origin', 170 headers: { 'X-Requested-With': 'XMLHttpRequest' }, 171 } ); 172 const data = await resp.json(); 173 if ( data && data.fragments ) { 174 applyFragments( data.fragments, data.cart_hash || readCookie( 'woocommerce_cart_hash' ) ); 175 } 176 } catch ( err ) { 177 // Réseau KO : on garde le rendu serveur, pas bloquant. 178 } finally { 179 const d = document.querySelector( '[data-df-cart-drawer]' ); 180 if ( d ) { d.classList.remove( 'is-loading' ); } 181 } 182 } 183 window.dfRefreshCart = refreshCartFragments; 184 185 /* ---------- Side cart drawer ---------- */ 186 function initCartDrawer() { 187 const drawer = document.querySelector( '[data-df-cart-drawer]' ); 188 if ( ! drawer ) { return; } 189 190 // Defensive: move the drawer to be a direct child of <body> so no 191 // transformed / overflow:hidden ancestor can break its fixed positioning. 192 if ( drawer.parentElement !== document.body ) { 193 document.body.appendChild( drawer ); 194 } 195 196 function open() { 197 drawer.classList.add( 'is-open' ); 198 drawer.setAttribute( 'aria-hidden', 'false' ); 199 document.body.classList.add( 'df-cart-open' ); 200 } 201 function close() { 202 drawer.classList.remove( 'is-open' ); 203 drawer.setAttribute( 'aria-hidden', 'true' ); 204 document.body.classList.remove( 'df-cart-open' ); 205 } 206 window.dfCartDrawer = { open, close }; 207 208 // Click on the header cart button opens the drawer. Le drawer est 209 // rendu côté serveur et synchronisé par initCartSync() â plus besoin 210 // de wc-cart-fragments pour l'ouvrir (v1.24.1). 211 document.querySelectorAll( '[data-df-cart-toggle]' ).forEach( ( a ) => { 212 a.addEventListener( 'click', ( e ) => { 213 e.preventDefault(); 214 open(); 215 } ); 216 } ); 217 218 // Close when clicking the overlay or X button 219 drawer.querySelectorAll( '[data-df-cart-close]' ).forEach( ( el ) => { 220 el.addEventListener( 'click', ( e ) => { e.preventDefault(); close(); } ); 221 } ); 222 223 // ESC closes 224 document.addEventListener( 'keydown', ( e ) => { 225 if ( 'Escape' === e.key && drawer.classList.contains( 'is-open' ) ) { close(); } 226 } ); 227 228 // Auto-open quand un produit est ajouté (nos flows vanilla). 229 document.body.addEventListener( 'df:added_to_cart', open ); 230 231 // Compat jQuery (v1.24.1) : les boutons de loop WC (wc-add-to-cart.js) 232 // déclenchent `added_to_cart` avec les fragments en payload â 233 // cart-fragments.js les appliquait, on prend le relais. Le guard 234 // dfApplyingFragments évite la double application quand c'est NOUS 235 // qui avons déclenché l'événement après avoir déjà appliqué. 236 if ( window.jQuery ) { 237 window.jQuery( document.body ).on( 'added_to_cart', function ( e, fragments, cartHash ) { 238 if ( ! dfApplyingFragments && fragments ) { applyFragments( fragments, cartHash ); } 239 open(); 240 } ); 241 window.jQuery( document.body ).on( 'removed_from_cart', function ( e, fragments, cartHash ) { 242 if ( ! dfApplyingFragments && fragments ) { applyFragments( fragments, cartHash ); } 243 } ); 244 // Visual loading state during AJAX mini-cart refresh 245 window.jQuery( document.body ).on( 'wc_fragments_loaded wc_fragments_refreshed', function () { 246 drawer.classList.remove( 'is-loading' ); 247 } ); 248 window.jQuery( document.body ).on( 'wc_fragment_refresh adding_to_cart', function () { 249 drawer.classList.add( 'is-loading' ); 250 } ); 251 } 252 } 253 254 /* ---------- Shared AJAX add-to-cart routine ---------------- * 255 * 256 * v1.26.1 â extrait du handler `submit` pour être réutilisable par les 257 * boutons de liste `[data-df-add]` (partial product/row), qui ne sont 258 * plus des <form> : voir template-parts/product/row.php. 259 * 260 * @param {string} productId ID du produit. 261 * @param {FormData} fd Charge utile (doit contenir product_id). 262 * @param {Element} button Bouton à passer en état busy/added. 263 */ 264 async function dfAjaxAdd( productId, fd, button ) { 265 try { 266 const url = wcAjaxUrl( 'add_to_cart' ); 267 const resp = await fetch( url, { 268 method: 'POST', 269 body: fd, 270 credentials: 'same-origin', 271 headers: { 'X-Requested-With': 'XMLHttpRequest' }, 272 } ); 273 const data = await resp.json(); 274 275 if ( data && data.error && data.product_url ) { 276 window.location = data.product_url; 277 return; 278 } 279 280 if ( data && data.fragments ) { 281 applyFragments( data.fragments, data.cart_hash ); 282 } 283 284 // Ãvénement vanilla : ouvre le drawer (voir initCartDrawer). 285 document.body.dispatchEvent( new CustomEvent( 'df:added_to_cart', { 286 detail: { fragments: data && data.fragments, cartHash: data && data.cart_hash }, 287 } ) ); 288 289 // Compat jQuery : d'autres scripts (WC, plugins) écoutent ces 290 // événements. dfApplyingFragments reste posé le temps du 291 // trigger pour que notre propre écouteur de compat ne 292 // ré-applique pas les fragments déjà appliqués ci-dessus. 293 if ( window.jQuery ) { 294 dfApplyingFragments = true; 295 try { 296 window.jQuery( document.body ).trigger( 'wc_fragments_refreshed' ); 297 window.jQuery( document.body ).trigger( 'added_to_cart', [ 298 data && data.fragments, 299 data && data.cart_hash, 300 button ? window.jQuery( button ) : window.jQuery(), 301 ] ); 302 } finally { 303 dfApplyingFragments = false;
304 } 305 } 306 307 // Belt-and-suspenders : if any WC "added to cart" notice DOM 308 // element slipped through (legacy filter conflicts, plugin 309 // notices), strip them. The drawer is showing the cart already. 310 document.querySelectorAll( '.woocommerce-message, .wc-block-components-notice-banner.is-success' ).forEach( ( el ) => { 311 if ( /(panier|cart|carrito|warenkorb)/i.test( el.textContent || '' ) ) { 312 el.remove(); 313 } 314 } ); 315 316 if ( button ) { 317 button.classList.remove( 'df-busy' ); 318 button.classList.add( 'df-added' ); 319 button.disabled = false; 320 setTimeout( () => button.classList.remove( 'df-added' ), 2000 ); 321 } 322 } catch ( err ) { 323 if ( button ) { 324 button.classList.remove( 'df-busy' ); 325 button.disabled = false; 326 } 327 console.error( '[df] add-to-cart failed', err ); 328 } 329 } 330 331 /* ---------- AJAX add-to-cart on any custom form ----------- * 332 * 333 * The DataFirefly theme uses several custom forms with an 334 * `[name="add-to-cart"]` hidden input (df-sb-form on single product, 335 * forms on hero / row partials, etc.) â none of them have the WC 336 * default `class="cart"`. We delegate on submit at document level so 337 * we catch all of them without enumerating selectors. 338 * 339 * We intercept the submit, do the AJAX ourselves, apply fragments, 340 * and trigger `added_to_cart` so the drawer opens via its existing 341 * listener. 342 * 343 * Skipped : grouped products (.grouped_form), forms with the opt-out 344 * attribute `data-df-disable-ajax`, and submits on /cart/ or /checkout/ 345 * (where WC's own logic should run). 346 * 347 * v1.8.7 : double-add bug fixes ââââââââââââââââââââââââââââââââââââ 348 * 1. stopImmediatePropagation() so jQuery handlers bound by WC on 349 * `body` (wc-cart-fragments, wc-add-to-cart) cannot re-process 350 * the submit and fire a second AJAX call. 351 * 2. In-flight guard via `data-df-submitting` on the <form> â even 352 * if the user double-clicks faster than the disabled flip, we 353 * bail out on the second submit. 354 * 3. Idempotent init â if init runs twice (DOMContentLoaded fired 355 * after the listener was already wired by some race), we attach 356 * only once via a window flag. 357 */ 358 function initSingleProductAjax() { 359 // Idempotent : never attach twice. 360 if ( window.__dfAtcWired ) { return; } 361 window.__dfAtcWired = true; 362 363 // Don't intercept on cart / checkout â WC's cart.js owns those forms. 364 if ( document.body.classList.contains( 'woocommerce-cart' ) || 365 document.body.classList.contains( 'woocommerce-checkout' ) ) { 366 return; 367 } 368 369 document.addEventListener( 'submit', async function ( e ) { 370 const form = e.target; 371 if ( ! ( form instanceof HTMLFormElement ) ) { return; } 372 if ( form.classList.contains( 'grouped_form' ) ) { return; } 373 if ( form.hasAttribute( 'data-df-disable-ajax' ) ) { return; } 374 375 const productInput = form.querySelector( '[name="add-to-cart"]' ); 376 if ( ! productInput ) { return; } 377 const productId = productInput.value; 378 if ( ! productId || '0' === productId ) { return; } 379 380 // In-flight guard â drop any concurrent submit on the same form. 381 if ( form.hasAttribute( 'data-df-submitting' ) ) { 382 e.preventDefault(); 383 e.stopImmediatePropagation(); 384 return; 385 } 386 form.setAttribute( 'data-df-submitting', '1' ); 387 388 // Block native submit AND any other listener (jQuery on body, WC 389 // frontend handlers) from re-processing this same event. 390 e.preventDefault(); 391 e.stopImmediatePropagation(); 392 393 const button = form.querySelector( 'button[type=submit], .single_add_to_cart_button, .df-sb-ad
393d' ); 394 if ( button ) { 395 button.classList.add( 'df-busy' ); 396 button.disabled = true; 397 } 398 399 const fd = new FormData( form ); 400 // CRITICAL â strip the `add-to-cart` field before sending. 401 // 402 // WC has TWO server-side handlers that both react to that param : 403 // 1. WC_Form_Handler::add_to_cart_action() (wp_loaded prio 20) 404 // reads $_REQUEST['add-to-cart'] regardless of GET/POST/AJAX. 405 // 2. WC_AJAX::add_to_cart() (template_redirect) 406 // reads $_POST['product_id']. 407 // 408 // If both fire, the cart receives qty=1 twice â bug "ajouté Ã2". 409 // We send only `product_id` to keep handler #2 active and starve #1. 410 fd.delete( 'add-to-cart' ); 411 fd.set( 'product_id', productId ); 412 413 try { 414 await dfAjaxAdd( productId, fd, button ); 415 } finally { 416 form.removeAttribute( 'data-df-submitting' ); 417 } 418 }, true ); // capture phase to fire before any other listener 419 } 420 421 /* ---------- Loop "Add to cart" buttons ([data-df-add]) ------ * 422 * 423 * v1.26.1 â CORRECTIF CRITIQUE. 424 * 425 * Les lignes de la vue liste (template-parts/product/row.php) sont 426 * rendues à L'INTÃRIEUR du <form data-df-cat-filters> de la page 427 * catégorie / boutique. Elles embarquaient un <form> imbriqué â 428 * invalide en HTML5 : le parser ignore la balise ouvrante et le 429 * </form> ferme le formulaire PARENT. Résultat : 430 * 431 * 1. l'<input name="add-to-cart"> se retrouvait rattaché au 432 * formulaire de FILTRES â new FormData(form) le récupérait â 433 * chaque clic sur un filtre partait avec &add-to-cart=ID et 434 * ajoutait le produit au panier ; 435 * 2. le pointeur de formulaire étant nullifié après le premier 436 * </form>, les boutons des lignes suivantes ajoutaient le 437 * produit de la PREMIÃRE ligne. 438 * 439 * Le partial rend désormais un simple <a data-df-add="ID"> â aucun 440 * contrôle de formulaire, donc invisible pour FormData. On intercepte 441 * le clic ici ; sans JS le href reste un add-to-cart fonctionnel. 442 */ 443 function initLoopAddButtons() { 444 if ( window.__dfLoopAtcWired ) { return; } 445 window.__dfLoopAtcWired = true; 446 447 document.addEventListener( 'click', async function ( e ) { 448 const el = e.target.closest( '[data-df-add]' ); 449 if ( ! el ) { return; } 450 451 // Nouvel onglet / clic milieu : comportement natif du lien. 452 if ( e.metaKey || e.ctrlKey || e.shiftKey || 1 === e.button ) { return; } 453 454 const productId = el.getAttribute( 'data-df-add' ); 455 if ( ! productId || '0' === productId ) { return; } 456 457 e.preventDefault(); 458 e.stopPropagation(); 459 460 // Garde anti double-clic. 461 if ( el.hasAttribute( 'data-df-submitting' ) ) { return; } 462 el.setAttribute( 'data-df-submitting', '1' ); 463 464 el.classList.add( 'df-busy' ); 465 466 const fd = new FormData(); 467 fd.set( 'product_id', productId ); 468 fd.set( 'quantity', el.getAttribute( 'data-quantity' ) || '1' ); 469 470 try { 471 await dfAjaxAdd( productId, fd, el ); 472 } finally { 473 el.removeAttribute( 'data-df-submitting' ); 474 el.classList.remove( 'df-busy' ); 475 } 476 }, true ); // capture : avant wc-add-to-cart.js s'il est présent 477 } 478 479 /* ---------- AJAX remove-from-cart inside the drawer ---------- * 480 * 481 * WC's cart.js only handles `a.remove` on the /cart/ and /checkout/ pages. 482 * Anywhere else, clicking â in the mini-cart follows ?remove_item=KEY, 483 * triggers a full page reload, and closes the drawer. 484 * 485 * We delegate clicks on `a.remove` inside the drawer, do the AJAX 486 * ourselves, and refresh fragments â the drawer stays open. 487 */ 488 function initDrawerRemoveAjax() { 489 const drawer = document.querySelector( '[data-df-cart-drawer]' ); 490 if ( ! drawer ) { return; } 491 492 drawer.addEventListener( 'click', async function ( e ) { 493 const link = e.target.closest( 'a.remove' ); 494 if ( ! link ) { return; } 495 496 const cartItemKey = link.getAttribute( 'data-cart_item_key' ); 497 if ( ! cartItemKey ) { return; } // unknown payload â safer to let it navigate 498 499 e.preventDefault(); 500 drawer.classList.add( 'is-loading' ); 501 502 try { 503 const url = wcAjaxUrl( 'remove_from_cart' ); 504 const body = new URLSearchParams( { cart_item_key: cartItemKey } ); 505 const resp = await fetch( url, { 506 method: 'POST', 507 body: body, 508 credentials: 'same-origin', 509 headers: { 510 'X-Requested-With': 'XMLHttpRequest', 511 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 512 }, 513 } ); 514 const data = await resp.json(); 515 516 if ( data && data.fragments ) { 517 applyFragments( data.fragments, data.cart_hash ); 518 document.body.dispatchEvent( new CustomEvent( 'df:removed_from_cart', { 519 detail: { fragments: data.fragments, cartHash: data.cart_hash }, 520 } ) ); 521 if ( window.jQuery ) { 522 dfApplyingFragments = true; 523 try { 524 window.jQuery( document.body ).trigger( 'wc_fragments_refreshed' ); 525 window.jQuery( document.body ).trigger( 'removed_from_cart', [ 526 data.fragments, data.cart_hash, 527 ] ); 528 } finally { 529 dfApplyingFragments = false;
530 } 531 } 532 } 533 } catch ( err ) { 534 // Fallback to the link's href so the user can still remove the item. 535 window.location = link.getAttribute( 'href' ); 536 } finally { 537 drawer.classList.remove( 'is-loading' ); 538 } 539 } ); 540 } 541 542 function wcAjaxUrl( endpoint ) { 543 // v1.24.1 â endpoint localisé par le thème : dispo sur TOUTES les 544 // pages, y compris hors WooCommerce (wc_*_params n'y existent pas). 545 if ( L10n.wcAjax ) { 546 return L10n.wcAjax.toString().replace( '%%endpoint%%', endpoint ); 547 } 548 if ( window.wc_cart_fragments_params && window.wc_cart_fragments_params.wc_ajax_url ) { 549 return window.wc_cart_fragments_params.wc_ajax_url.toString().replace( '%%endpoint%%', endpoint ); 550 } 551 if ( window.wc_add_to_cart_params && window.wc_add_to_cart_params.wc_ajax_url ) { 552 return window.wc_add_to_cart_params.wc_ajax_url.toString().replace( '%%endpoint%%', endpoint ); 553 } 554 return '/?wc-ajax=' + endpoint; 555 } 556 557 /* ---------- Row-card click navigation -------------------- * 558 * 559 * The category list-view row (`.df-mrow[data-df-row-link]`) used to be 560 * a wrapping <a>, which meant we had to nest the buy <form> inside it 561 * (invalid HTML5). We replaced it with a <div> + delegated click; this 562 * function is the JS half of that contract. 563 * 564 * Click on the row navigates to the product, EXCEPT when the click 565 * originates from an interactive child (a button, a link, a form 566 * control) â those handle themselves. 567 */ 568 function initRowLinks() { 569 document.addEventListener( 'click', function ( e ) { 570 const row = e.target.closest( '.df-mrow[data-df-row-link]' ); 571 if ( ! row ) { return; } 572 // Let real interactive children handle their own click. 573 if ( e.target.closest( 'a, button, form, input, [role="button"]' ) ) { return; } 574 // Honor modifier keys / middle-click for new-tab UX. 575 if ( e.metaKey || e.ctrlKey || e.shiftKey || 1 === e.button ) { return; } 576 const href = row.getAttribute( 'data-df-row-link' ); 577 if ( href ) { window.location = href; } 578 } ); 579 } 580 581 /* ---------- Team bio "Read more / Read less" toggle ---------- 582 * 583 * Pour chaque bio de la page à propos : 584 * 1. Détecte si la bio dépasse vraiment la limite visuelle (clamp CSS 585 * à 4 lignes). On mesure scrollHeight vs clientHeight + 1px de 586 * tolérance pour éviter les faux-positifs sur les bios qui 587 * remplissent pile 4 lignes. 588 * 2. Si NON â masque le bouton + retire le clamp (sinon on aurait une 589 * bio raccourcie SANS toggle, ce qui n'a aucun intérêt). 590 * 3. Si OUI â branche le toggle. à chaque clic : flip de la classe 591 * .is-expanded sur la bio, flip du texte du bouton via les 592 * data-label-more / data-label-less, flip de aria-expanded. 593 * 594 * Recalcul au resize : sur passage portraitâpaysage mobile la mesure 595 * peut changer (font-size CSS responsive). On debounce 150ms. 596 */ 597 function initTeamBioToggle() { 598 const toggles = document.querySelectorAll( '.df-team-bio-toggle' ); 599 if ( ! toggles.length ) { return; } 600 601 function evaluate( btn ) { 602 const bio = btn.previousElementSibling; 603 if ( ! bio || ! bio.classList.contains( 'df-team-bio' ) ) { return; } 604 605 // On retire d'abord is-expanded pour mesurer dans l'état clampé 606 const wasExpanded = bio.classList.contains( 'is-expanded' ); 607 bio.classList.remove( 'is-expanded' ); 608 609 const overflows = bio.scrollHeight > bio.clientHeight + 1; 610 if ( overflows ) { 611 btn.hidden = false; 612 bio.classList.add( 'df-team-bio-clamp' ); 613 } else { 614 btn.hidden = true; 615 bio.classList.remove( 'df-team-bio-clamp' ); 616 } 617 618 // Restaure l'état si l'utilisateur avait déjà déplié 619 if ( wasExpanded && overflows ) { 620 bio.classList.add( 'is-expanded' ); 621 } 622 } 623 624 toggles.forEach( ( btn ) => { 625 evaluate( btn ); 626 btn.addEventListener( 'click', () => { 627 const bio = btn.previousElementSibling; 628 if ( ! bio ) { return; } 629 const expanded = bio.classList.toggle( 'is-expanded' ); 630 btn.setAttribute( 'aria-expanded', expanded ? 'true' : 'false' ); 631 btn.textContent = expanded 632 ? ( btn.dataset.labelLess || 'Read less' ) 633 : ( btn.dataset.labelMore || 'Read more' ); 634 } ); 635 } ); 636 637 // Re-évaluer si la fenêtre change de taille (debounced). 638 let resizeTimer = null; 639 window.addEventListener( 'resize', () => { 640 clearTimeout( resizeTimer ); 641 resizeTimer = setTimeout( () => {
642 toggles.forEach( evaluate ); 643 }, 150 ); 644 } ); 645 } 646 647 document.addEventListener( 'DOMContentLoaded', () => { 648 initTheme(); 649 initLangSwitcher(); 650 initNewsletter(); 651 initFaq(); 652 initFireflies(); 653 initCartDrawer(); 654 initCartSync(); 655 initSingleProductAjax(); 656 initLoopAddButtons(); 657 initRowLinks(); 658 initDrawerRemoveAjax(); 659 initTeamBioToggle(); 660 } ); 661} )();
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.