1/** 2 * TA FluentCart Variation Gallery â storefront loader. 3 * 4 * Shows only the selected pattern's images. Non-selected patterns are stored as 5 * data only (no <img> requested) and built on demand when that pattern is chosen 6 * via FluentCart's native `fluentCartSingleProductVariationChanged` event. 7 * 8 * v0.5.0 provides a pattern-isolated gallery lightbox: clicking the main image 9 * opens an accessible Alibaba-style viewer of ONLY the current pattern's six 10 * images, plus an optional governed product-level video, with Photos/Video modes, 11 * a thumbnail rail, counter, arrows, touch swipe, Esc to close, focus trap and 12 * focus restoration. 13 * 14 * v0.3.7 also initialises galleries inserted by FluentCart's AJAX "View Options" 15 * modal. The server has already removed the native all-images gallery before the 16 * response reaches the DOM, so this boot path never triggers the photo dump. 17 * 18 * No native FluentCart cart/variation/price logic is touched or duplicated. 19 */ 20( function () { 21 'use strict'; 22 23 function init( root ) { 24 if ( root.getAttribute( 'data-ta-fcvg-initialized' ) === 'true' ) { return; } 25 var dataEl = root.querySelector( '.ta-fcvg-data' ); 26 if ( ! dataEl ) { return; } 27 var payload; 28 try { payload = JSON.parse( dataEl.textContent ); } catch ( e ) { return; } 29 root.setAttribute( 'data-ta-fcvg-initialized', 'true' ); 30 31 var map = payload.map || {}; 32 var names = payload.names || {}; 33 var mainWrap = root.querySelector( '[data-ta-fcvg-main]' ); 34 var thumbsWrap = root.querySelector( '[data-ta-fcvg-thumbs]' ); 35 var nameOut = root.querySelector( '[data-ta-fcvg-selected-name]' ); 36 var live = root.querySelector( '[data-ta-fcvg-live]' ); 37 var videoSource = root.querySelector( '[data-ta-fcvg-video-source]' ); 38 var videoTitle = videoSource ? ( videoSource.getAttribute( 'data-video-title' ) || 'Product video' ) : ''; 39 var videoPoster = videoSource ? ( videoSource.getAttribute( 'data-video-poster' ) || '' ) : ''; 40 var hasVideo = !! videoSource; 41 var currentVid = root.getAttribute( 'data-default-vid' ); 42 var currentIndex = 0; 43 var lbMode = 'photos'; 44 45 function imgEl( desc, isMain ) { 46 var img = document.createElement( 'img' ); 47 img.src = desc.main; 48 if ( desc.srcset ) { img.srcset = desc.srcset; } 49 if ( desc.sizes ) { img.sizes = desc.sizes; } 50 if ( desc.w ) { img.width = desc.w; } 51 if ( desc.h ) { img.height = desc.h; } 52 img.alt = desc.alt || ''; 53 img.decoding = 'async'; 54 img.loading = 'lazy'; 55 if ( isMain ) { img.className = 'ta-fcvg-main-img'; } 56 return img; 57 } 58 59 function markActiveThumb( idx ) { 60 if ( ! thumbsWrap ) { return; } 61 var btns = thumbsWrap.querySelectorAll( '.ta-fcvg-thumb' ); 62 for ( var i = 0; i < btns.length; i++ ) { 63 if ( i === idx ) { btns[ i ].classList.add( 'is-active' ); btns[ i ].setAttribute( 'aria-current', 'true' ); } 64 else { btns[ i ].classList.remove( 'is-active' ); btns[ i ].removeAttribute( 'aria-current' ); } 65 } 66 } 67 68 function setMain( idx ) { 69 var images = map[ currentVid ]; 70 if ( ! images || ! images[ idx ] || ! mainWrap ) { return; } 71 currentIndex = idx; 72 root.setAttribute( 'data-ta-fcvg-index', String( idx ) ); 73 mainWrap.innerHTML = ''; 74 mainWrap.appendChild( imgEl( images[ idx ], true ) ); 75 markActiveThumb( idx ); 76 } 77 78 /** 79 * Keep the two native Divi editorial placements aligned with the selected 80 * FluentCart variation. The server-rendered product-meta images remain the 81 * no-JavaScript fallback; this adapter only replaces their image attributes 82 * after FluentCart has confirmed a variation change. 83 */ 84 function syncEditorialImages( images ) { 85 if ( ! images || ! images.length || root.closest( '.fluent-cart-shop-app-single-product-modal' ) ) { return; } 86 var editorial = document.querySelector( '.ta-product-editorial-showcase img' ); 87 var hero = document.querySelector( '.ta-product-editorial-hero img' ); 88 var front = images[ 0 ]; 89 var angled = images[ 1 ] || front; 90 91 function applyDescriptor( img, desc ) { 92 if ( ! img || ! desc || ! desc.main ) { return; } 93 if ( desc.srcset ) { img.srcset = desc.srcset; } else { img.removeAttribute( 'srcset' ); } 94 if ( desc.sizes ) { img.sizes = desc.sizes; } else { img.removeAttribute( 'sizes' ); } 95 if ( desc.w ) { img.width = desc.w; } 96 if ( desc.h ) { img.height = desc.h; } 97 img.alt = desc.alt || ''; 98 img.src = desc.main; 99 } 100 101 applyDescriptor( editorial, front ); 102 applyDescriptor( hero, angled ); 103 } 104 105 function buildThumbs( images ) { 106 if ( ! thumbsWrap ) { return; } 107 thumbsWrap.innerHTML = '';
108 images.forEach( function ( desc, idx ) { 109 var li = document.createElement( 'li' ); 110 li.setAttribute( 'role', 'listitem' ); 111 var btn = document.createElement( 'button' ); 112 btn.type = 'button'; 113 btn.className = 'ta-fcvg-thumb' + ( idx === 0 ? ' is-active' : '' ); 114 btn.setAttribute( 'data-idx', idx ); 115 btn.setAttribute( 'aria-label', 'Show image ' + ( idx + 1 ) ); 116 if ( idx === 0 ) { btn.setAttribute( 'aria-current', 'true' ); } 117 var t = document.createElement( 'img' ); 118 t.src = desc.thumb; t.width = 80; t.height = 80; t.alt = ''; 119 t.loading = 'lazy'; t.decoding = 'async'; 120 btn.appendChild( t ); 121 btn.addEventListener( 'click', function () { setMain( idx ); } ); 122 li.appendChild( btn ); 123 thumbsWrap.appendChild( li ); 124 } ); 125 appendPageVideoThumb(); 126 } 127 128 function appendPageVideoThumb() { 129 if ( ! hasVideo || ! thumbsWrap || thumbsWrap.querySelector( '[data-ta-fcvg-video-open]' ) ) { return; } 130 var li = document.createElement( 'li' ); 131 li.setAttribute( 'role', 'listitem' ); 132 var btn = document.createElement( 'button' ); 133 btn.type = 'button'; 134 btn.className = 'ta-fcvg-thumb ta-fcvg-video-thumb'; 135 btn.setAttribute( 'data-ta-fcvg-video-open', '' ); 136 btn.setAttribute( 'aria-label', 'Play video: ' + videoTitle ); 137 if ( videoPoster ) { 138 var poster = document.createElement( 'img' ); 139 poster.src = videoPoster; 140 poster.width = 80; 141 poster.height = 80; 142 poster.alt = ''; 143 poster.loading = 'lazy'; 144 poster.decoding = 'async'; 145 btn.appendChild( poster ); 146 } 147 var play = document.createElement( 'span' ); 148 play.className = 'ta-fcvg-video-thumb-play'; 149 play.setAttribute( 'aria-hidden', 'true' ); 150 play.textContent = 'â¶'; 151 btn.appendChild( play ); 152 li.appendChild( btn ); 153 thumbsWrap.appendChild( li ); 154 } 155 156 function renderVariation( vid ) { 157 var images = map[ vid ]; 158 if ( ! images || ! images.length ) { return; } 159 currentVid = String( vid ); 160 buildThumbs( images ); 161 setMain( 0 ); 162 syncEditorialImages( images ); 163 var nm = names[ vid ] || ''; 164 if ( nameOut ) { nameOut.textContent = nm; } 165 if ( live ) { live.textContent = 'Selected pattern: ' + nm + '. Showing ' + images.length + ' photographs of this product.'; } 166 } 167 168 // ---- Lightbox (pattern-isolated) ---- 169 var lb = document.createElement( 'div' ); 170 lb.className = 'ta-fcvg-lightbox'; 171 lb.setAttribute( 'role', 'dialog' ); 172 lb.setAttribute( 'aria-modal', 'true' ); 173 lb.setAttribute( 'aria-label', 'Product image viewer' ); 174 lb.hidden = true; 175 lb.innerHTML = 176 '<div class="ta-fcvg-lb-backdrop" data-lb-close></div>' + 177 '<div class="ta-fcvg-lb-dialog">' + 178 '<header class="ta-fcvg-lb-header">' + 179 '<p class="ta-fcvg-lb-pattern" data-lb-pattern></p>' + 180 ( hasVideo ? '<div class="ta-fcvg-lb-modes" role="tablist" aria-label="Product media"><button type="button" class="ta-fcvg-lb-mode is-active" data-lb-mode="photos" role="tab" aria-selected="true">Photos</button><button type="button" class="ta-fcvg-lb-mode" data-lb-mode="video" role="tab" aria-selected="false">Video</button></div>' : '' ) + 181 '<button type="button" class="ta-fcvg-lb-close" data-lb-close aria-label="Close image viewer">Ã</button>' + 182 '</header>' + 183 '<div class="ta-fcvg-lb-body">' + 184 '<div class="ta-fcvg-lb-rail" data-lb-rail>' + 185 '<div class="ta-fcvg-lb-thumbs" data-lb-thumbs role="list"></div>' + 186 '</div>' + 187 '<div class="ta-fcvg-lb-stage" data-lb-stage>' + 188 '<p class="ta-fcvg-lb-counter" data-lb-counter aria-live="polite"></p>' + 189 '<button type="button" class="ta-fcvg-lb-nav ta-fcvg-lb-prev" data-lb-prev aria-label="Previous image">â¹</button>' + 190 '<figure class="ta-fcvg-lb-figure">' + 191 '<img class="ta-fcvg-lb-img" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==" alt="" />' + 192 '<figcaption class="screen-reader-text" data-lb-caption></figcaption>' + 193 '</figure>' + 194 '<div class="ta-fcvg-lb-video" data-lb-video hidden></div>' + 195 '<button type="button" class="ta-fcvg-lb-nav ta-fcvg-lb-next" data-lb-next aria-label="Next image">âº</button>' + 196 '</div>' + 197 '</div>' + 198 '</div>'; 199 // Escape page-builder stacking contexts on the canonical product page. 200 // Keep quick-view lightboxes inside their AJAX modal so removing that modal 201 // also removes its viewer and event handlers. 202 var lbUsesBodyPortal = ! root.closest( '.fluent-cart-shop-app-single-product-modal' ); 203 ( lbUsesBodyPortal ? document.body : root ).appendChild( lb ); 204 var lbImg = lb.querySelector( '.ta-fcvg-lb-img' ); 205 var lbCounter = lb.querySelector( '[data-lb-counter]' ); 206 var lbPattern = lb.querySelector( '[data-lb-pattern]' ); 207 var lbCaption = lb.querySelector( '[data-lb-caption]' ); 208 var lbRail = lb.querySelector( '[data-lb-rail]' ); 209 var lbThumbs = lb.querySelector( '[data-lb-thumbs]' ); 210 var lbStage = lb.querySelector( '[data-lb-stage]' ); 211 var lbPrev = lb.querySelector( '[data-lb-prev]' ); 212 var lbNext = lb.querySelector( '[data-lb-next]' ); 213 var lbFigure = lb.querySelector( '.ta-fcvg-lb-figure' ); 214 var lbVideo = lb.querySelector( '[data-lb-video]' ); 215 var lbModeButtons = lb.querySelectorAll( '[data-lb-mode]' ); 216 var lastFocus = null; 217 var initialImages = map[ currentVid ] || []; 218 var initialPattern = names[ currentVid ] || ''; 219 220 if ( lbImg && initialImages[0] && initialImages[0].main ) { 221 lbImg.src = initialImages[0].main; 222 lbImg.alt = initialImages[0].alt || initialPattern || 'Product image'; 223 } 224 225 // ta_fcvg_zoom_marker: hover-zoom on the photograph (desktop pointers) so 226 // customers can inspect the African-print detail before buying â a 227 // confidence-to-purchase aid. Skipped in video mode and for touch/reduced-motion. 228 if ( lbFigure && lbImg && window.matchMedia && window.matchMedia( '(pointer: fine)' ).matches && 229 ! window.matchMedia( '(prefers-reduced-motion: reduce)' ).matches ) { 230 lbFigure.addEventListener( 'mousemove', function ( e ) { 231 if ( lbStage && lbStage.classList.contains( 'is-video' ) ) { return; } 232 var r = lbImg.getBoundingClientRect(); 233 if ( ! r.width || ! r.height ) { return; } 234 var x = Math.max( 0, Math.min( 100, ( ( e.clientX - r.left ) / r.width ) * 100 ) ); 235 var y = Math.max( 0, Math.min( 100, ( ( e.clientY - r.top ) / r.height ) * 100 ) ); 236 lbImg.style.transformOrigin = x + '% ' + y + '%'; 237 lbImg.style.transform = 'scale(2)'; 238 lbFigure.classList.add( 'is-zoomed' ); 239 } ); 240 lbFigure.addEventListener( 'mouseleave', function () { 241 lbImg.style.transform = ''; 242 lbFigure.classList.remove( 'is-zoomed' ); 243 } ); 244 } 245 246 function lbMarkActiveThumb() { 247 var buttons = lbThumbs.querySelectorAll( '.ta-fcvg-lb-thumb' ); 248 for ( var i = 0; i < buttons.length; i++ ) { 249 var active = parseInt( buttons[ i ].getAttribute( 'data-lb-index' ), 10 ) === currentIndex; 250 buttons[ i ].classList.toggle( 'is-active', active ); 251 if ( active ) { 252 buttons[ i ].setAttribute( 'aria-current', 'true' );
253 buttons[ i ].scrollIntoView( { block: 'nearest', inline: 'nearest' } ); 254 } else { 255 buttons[ i ].removeAttribute( 'aria-current' ); 256 } 257 } 258 } 259 260 function lbBuildThumbs() { 261 var images = map[ currentVid ]; 262 var pattern = names[ currentVid ] || 'Selected pattern'; 263 if ( ! images || ! lbThumbs ) { return; } 264 lbThumbs.innerHTML = ''; 265 images.forEach( function ( desc, idx ) { 266 var item = document.createElement( 'div' ); 267 item.className = 'ta-fcvg-lb-thumb-item'; 268 item.setAttribute( 'role', 'listitem' ); 269 var button = document.createElement( 'button' ); 270 button.type = 'button'; 271 button.className = 'ta-fcvg-lb-thumb'; 272 button.setAttribute( 'data-lb-index', idx ); 273 button.setAttribute( 'aria-label', 'View ' + pattern + ' photograph ' + ( idx + 1 ) + ' of ' + images.length ); 274 var thumb = document.createElement( 'img' ); 275 thumb.src = desc.thumb || desc.main; 276 thumb.width = 88; 277 thumb.height = 88; 278 thumb.alt = ''; 279 thumb.decoding = 'async'; 280 button.appendChild( thumb ); 281 item.appendChild( button ); 282 lbThumbs.appendChild( item ); 283 } ); 284 if ( hasVideo ) { 285 var videoItem = document.createElement( 'div' ); 286 videoItem.className = 'ta-fcvg-lb-thumb-item'; 287 videoItem.setAttribute( 'role', 'listitem' ); 288 var videoButton = document.createElement( 'button' ); 289 videoButton.type = 'button'; 290 videoButton.className = 'ta-fcvg-lb-thumb ta-fcvg-lb-video-thumb'; 291 videoButton.setAttribute( 'data-lb-video-thumb', '' ); 292 videoButton.setAttribute( 'aria-label', 'Play video: ' + videoTitle ); 293 if ( videoPoster ) { 294 var videoImage = document.createElement( 'img' ); 295 videoImage.src = videoPoster; 296 videoImage.width = 88; 297 videoImage.height = 88; 298 videoImage.alt = ''; 299 videoImage.decoding = 'async'; 300 videoButton.appendChild( videoImage ); 301 } 302 var videoPlay = document.createElement( 'span' ); 303 videoPlay.className = 'ta-fcvg-video-thumb-play'; 304 videoPlay.setAttribute( 'aria-hidden', 'true' ); 305 videoPlay.textContent = 'â¶'; 306 videoButton.appendChild( videoPlay ); 307 videoItem.appendChild( videoButton ); 308 lbThumbs.appendChild( videoItem ); 309 } 310 } 311 312 function pauseVideo() { 313 if ( ! lbVideo ) { return; } 314 Array.prototype.forEach.call( lbVideo.querySelectorAll( 'video' ), function ( video ) { 315 try { video.pause(); } catch ( error ) {} 316 } ); 317 var fluentPlayer = lbVideo.querySelector( 'media-player' ); 318 if ( fluentPlayer && 'function' === typeof fluentPlayer.pause ) { 319 try { fluentPlayer.pause(); } catch ( error ) {} 320 } 321 var presto = lbVideo.querySelector( 'presto-player' ); 322 if ( presto && 'function' === typeof presto.pause ) { 323 try { presto.pause(); } catch ( error ) {} 324 } 325 } 326 327 function setMode( mode ) { 328 if ( 'video' === mode && ! hasVideo ) { mode = 'photos'; } 329 lbMode = mode; 330 var isVideo = 'video' === mode; 331 if ( lbFigure ) { lbFigure.hidden = isVideo; } 332 if ( lbVideo ) { lbVideo.hidden = ! isVideo; } 333 lbCounter.hidden = isVideo; 334 lbPrev.hidden = isVideo || ! map[ currentVid ] || map[ currentVid ].length < 2; 335 lbNext.hidden = isVideo || ! map[ currentVid ] || map[ currentVid ].length < 2; 336 lbStage.classList.toggle( 'is-video', isVideo ); 337 Array.prototype.forEach.call( lbModeButtons, function ( button ) { 338 var active = button.getAttribute( 'data-lb-mode' ) === mode; 339 button.classList.toggle( 'is-active', active ); 340 button.setAttribute( 'aria-selected', active ? 'true' : 'false' ); 341 button.tabIndex = active ? 0 : -1; 342 } ); 343 var videoThumb = lbThumbs.querySelector( '[data-lb-video-thumb]' ); 344 if ( videoThumb ) { 345 videoThumb.classList.toggle( 'is-active', isVideo ); 346 if ( isVideo ) { videoThumb.setAttribute( 'aria-current', 'true' ); } 347 else { videoThumb.removeAttribute( 'aria-current' ); } 348 } 349 if ( isVideo ) { 350 lbPattern.textContent = videoTitle; 351 lb.setAttribute( 'aria-label', videoTitle + ' player' ); 352 if ( videoSource && lbVideo && ! lbVideo.firstElementChild && videoSource.content ) { 353 lbVideo.appendChild( videoSource.content.cloneNode( true ) ); 354 } 355 // Once the customer chooses Video, refresh metadata at the visible 356 // size so controls and play action have a ready media element. 357 window.requestAnimationFrame( function () { 358 var nativeVideo = lbVideo ? lbVideo.querySelector( 'video' ) : null;
359 if ( nativeVideo && nativeVideo.readyState < 1 ) { 360 nativeVideo.preload = 'metadata'; 361 nativeVideo.load(); 362 } 363 364 // ta_fcvg_autoplay_marker: autoplay muted after an explicit Video action 365 // so the customer immediately sees motion in the 16:9 media frame. 366 window.setTimeout( function () { 367 var fluentPlayer = lbVideo ? lbVideo.querySelector( 'media-player' ) : null; 368 var presto = lbVideo ? lbVideo.querySelector( 'presto-player' ) : null; 369 var sv = ( presto && presto.shadowRoot ) ? presto.shadowRoot.querySelector( 'video' ) : null; 370 try { 371 if ( fluentPlayer && 'function' === typeof fluentPlayer.play ) { 372 var playFluentWhenReady = function () { 373 if ( lb.hidden || 'video' !== lbMode ) { return; } 374 fluentPlayer.muted = true; 375 var playRequest = fluentPlayer.play(); 376 if ( playRequest && 'function' === typeof playRequest.catch ) { playRequest.catch( function () {} ); } 377 }; 378 if ( fluentPlayer.hasAttribute( 'data-can-play' ) ) { playFluentWhenReady(); } 379 else { fluentPlayer.addEventListener( 'can-play', playFluentWhenReady, { once: true } ); } 380 } 381 else if ( sv ) { sv.muted = true; sv.play(); } 382 else if ( nativeVideo ) { nativeVideo.muted = true; nativeVideo.play(); } 383 } catch ( error ) {} 384 }, 260 ); 385 } ); 386 } else { 387 pauseVideo(); 388 lbRender(); 389 } 390 } 391 392 function lbRender() { 393 var images = map[ currentVid ]; 394 if ( ! images || ! images[ currentIndex ] ) { return; } 395 var d = images[ currentIndex ]; 396 var pattern = names[ currentVid ] || 'Selected pattern'; 397 // Crossfade between photographs (premium feel) + clear any zoom. 398 lbImg.style.opacity = '0'; 399 lbImg.style.transform = ''; 400 if ( lbFigure ) { lbFigure.classList.remove( 'is-zoomed' ); } 401 lbImg.onload = function () { lbImg.style.opacity = '1'; }; 402 if ( d.srcset ) { lbImg.srcset = d.srcset; } else { lbImg.removeAttribute( 'srcset' ); } 403 lbImg.sizes = '90vw'; 404 lbImg.alt = d.alt || ''; 405 lbImg.src = d.main; 406 if ( lbImg.complete && lbImg.naturalWidth ) { lbImg.style.opacity = '1'; } 407 lbCounter.textContent = ( currentIndex + 1 ) + ' / ' + images.length; 408 lbPattern.textContent = pattern + ' photographs'; 409 lbCaption.textContent = pattern + ', photograph ' + ( currentIndex + 1 ) + ' of ' + images.length; 410 lbRail.setAttribute( 'aria-label', 'Other photographs of ' + pattern ); 411 lb.setAttribute( 'aria-label', pattern + ' product image viewer' ); 412 lbPrev.hidden = 'video' === lbMode || images.length < 2; 413 lbNext.hidden = 'video' === lbMode || images.length < 2; 414 lbMarkActiveThumb(); 415 416 // Warm the adjacent photographs so arrow/swipe navigation feels immediate. 417 if ( images.length > 1 ) { 418 var next = new Image(); 419 var previous = new Image(); 420 next.src = images[ ( currentIndex + 1 ) % images.length ].main; 421 previous.src = images[ ( currentIndex - 1 + images.length ) % images.length ].main; 422 } 423 } 424 function lbOpen( mode ) { 425 lastFocus = document.activeElement; 426 // Re-appending moves the portal to the end of <body>, above late-loaded 427 // consent/shopping widgets that also use maximum stacking layers. 428 if ( lbUsesBodyPortal ) { document.body.appendChild( lb ); } 429 lb.hidden = false; 430 document.body.classList.add( 'ta-fcvg-lb-open' ); 431 lbBuildThumbs(); 432 lbMode = 'photos'; 433 lbRender(); 434 setMode( mode || 'photos' ); 435 lb.querySelector( '.ta-fcvg-lb-close' ).focus(); 436 document.addEventListener( 'keydown', lbKey, true ); 437 } 438 function lbClose() { 439 pauseVideo(); 440 lb.hidden = true; 441 document.body.classList.remove( 'ta-fcvg-lb-open' ); 442 document.removeEventListener( 'keydown', lbKey, true ); 443 if ( lastFocus && lastFocus.focus ) { lastFocus.focus(); } 444 } 445 function lbNav( delta ) { 446 if ( 'video' === lbMode ) { return; } 447 var images = map[ currentVid ]; 448 if ( ! images ) { return; } 449 currentIndex = ( currentIndex + delta + images.length ) % images.length; 450 setMain( currentIndex ); // keep the page gallery in sync 451 lbRender(); 452 } 453 function lbKey( e ) { 454 if ( e.key === 'Escape' ) { 455 e.preventDefault(); 456 e.stopImmediatePropagation(); 457 lbClose(); 458 } 459 else if ( e.key === 'ArrowRight' ) { 460 e.preventDefault(); 461 e.stopImmediatePropagation(); 462 lbNav( 1 ); 463 } 464 else if ( e.key === 'ArrowLeft' ) { 465 e.preventDefault(); 466 e.stopImmediatePropagation(); 467 lbNav( -1 ); 468 } 469 else if ( e.key === 'Home' ) { 470 if ( 'video' === lbMode ) { return; } 471 e.preventDefault(); 472 currentIndex = 0; 473 setMain( currentIndex ); 474 lbRender(); 475 } 476 else if ( e.key === 'End' ) { 477 if ( 'video' === lbMode ) { return; } 478 e.preventDefault(); 479 var images = map[ currentVid ]; 480 if ( images && images.length ) { 481 currentIndex = images.length - 1; 482 setMain( currentIndex ); 483 lbRender(); 484 } 485 } 486 else if ( e.key === 'Tab' ) { 487 var f = lb.querySelectorAll( 'button:not([hidden]):not([disabled])' ); 488 if ( ! f.length ) { return; } 489 var first = f[ 0 ], last = f[ f.length - 1 ]; 490 if ( e.shiftKey && document.activeElement === first ) { e.preventDefault(); last.focus(); } 491 else if ( ! e.shiftKey && document.activeElement === last ) { e.preventDefault(); first.focus(); } 492 } 493 } 494 lb.addEventListener( 'click', function ( e ) { 495 var close = e.target.closest( '[data-lb-close]' ); 496 var previous = e.target.closest( '[data-lb-prev]' ); 497 var next = e.target.closest( '[data-lb-next]' ); 498 var thumb = e.target.closest( '[data-lb-index]' ); 499 var videoThumb = e.target.closest( '[data-lb-video-thumb]' ); 500 var mode = e.target.closest( '[data-lb-mode]' ); 501 if ( close ) { lbClose(); } 502 else if ( previous ) { lbNav( -1 ); } 503 else if ( next ) { lbNav( 1 ); } 504 else if ( mode ) { setMode( mode.getAttribute( 'data-lb-mode' ) ); } 505 else if ( videoThumb ) { setMode( 'video' ); } 506 else if ( thumb ) { 507 currentIndex = parseInt( thumb.getAttribute( 'data-lb-index' ), 10 ) || 0; 508 setMain( currentIndex ); 509 setMode( 'photos' ); 510 lbRender(); 511 } 512 } ); 513 514 // Mobile/tablet swipe navigation; vertical page gestures are ignored. 515 var swipeX = null; 516 var swipeY = null; 517 lbStage.addEventListener( 'pointerdown', function ( e ) { 518 if ( 'mouse' === e.pointerType ) { return; } 519 swipeX = e.clientX; 520 swipeY = e.clientY; 521 } ); 522 lbStage.addEventListener( 'pointerup', function ( e ) { 523 if ( null === swipeX || null === swipeY ) { return; } 524 var dx = e.clientX - swipeX; 525 var dy = e.clientY - swipeY; 526 swipeX = null; 527 swipeY = null; 528 if ( Math.abs( dx ) >= 48 && Math.abs( dx ) > Math.abs( dy ) ) { 529 lbNav( dx < 0 ? 1 : -1 ); 530 } 531 } ); 532 lbStage.addEventListener( 'pointercancel', function () { 533 swipeX = null; 534 swipeY = null; 535 } ); 536 if ( mainWrap ) { 537 mainWrap.addEventListener( 'click', function () { lbOpen( 'photos' ); } ); 538 } 539 540 // Wire the server-rendered (default) thumbnails for click swapping. 541 if ( thumbsWrap ) { 542 Array.prototype.forEach.call( thumbsWrap.querySelectorAll( '.ta-fcvg-thumb' ), function ( btn ) { 543 btn.addEventListener( 'click', function ( e ) { 544 if ( btn.hasAttribute( 'data-ta-fcvg-video-open' ) ) { 545 e.stopPropagation(); 546 lbOpen( 'video' ); 547 return; 548 }
549 setMain( parseInt( btn.getAttribute( 'data-idx' ), 10 ) || 0 ); 550 } ); 551 } ); 552 thumbsWrap.addEventListener( 'click', function ( e ) { 553 var videoOpen = e.target.closest( '[data-ta-fcvg-video-open]' ); 554 if ( videoOpen ) { lbOpen( 'video' ); } 555 } ); 556 } 557 558 // Resolve a variation id from FluentCart's native event (defensive across shapes). 559 function vidFromEvent( e ) { 560 var d = e && e.detail ? e.detail : {}; 561 return d.variationId || d.variantId || d.id || 562 ( d.variation && ( d.variation.id || d.variation.variation_id ) ) || 563 ( d.variant && ( d.variant.id || d.variant.variation_id ) ) || null; 564 } 565 566 // FluentCart dispatches on `window` (bubbles:false); also keep a document capture 567 // listener as a defensive fallback. 568 [ 'fluentCartSingleProductVariationChanged', 'fluentCart:variationChanged' ].forEach( function ( evt ) { 569 var handler = function ( e ) { 570 var vid = vidFromEvent( e ); 571 if ( vid && map[ vid ] && String( vid ) !== currentVid ) { renderVariation( vid ); } 572 }; 573 window.addEventListener( evt, handler ); 574 document.addEventListener( evt, handler, true ); 575 } ); 576 577 // Sync the initial gallery to FluentCart's product-default variant. 578 var productRoot = root.closest( '[data-fluent-cart-single-product-page]' ); 579 var nativeDefaultEl = productRoot 580 ? productRoot.querySelector( '[data-default-variation-id]' ) 581 : document.querySelector( '[data-default-variation-id]' ); 582 var nativeDefault = nativeDefaultEl ? nativeDefaultEl.getAttribute( 'data-default-variation-id' ) : null; 583 if ( nativeDefault && map[ nativeDefault ] && String( nativeDefault ) !== currentVid ) { 584 renderVariation( nativeDefault ); 585 } 586 } 587 588 function boot( scope ) { 589 scope = scope || document; 590 if ( scope.matches && scope.matches( '.ta-fcvg' ) ) { init( scope ); } 591 Array.prototype.forEach.call( scope.querySelectorAll( '.ta-fcvg' ), init ); 592 upgradeCatalogueProductLinks( scope ); 593 } 594 595 /** 596 * FluentCart 1.6.3 renders variable-product catalogue actions as modal 597 * buttons even though each button already carries the canonical product URL. 598 * That keeps the browser on a filtered /shop/ URL and can expose a blank 599 * loading panel while the modal request completes. Convert only those native 600 * variable-product controls to real same-origin product links. Simple-product 601 * Add To Cart and subscription Buy Now controls remain untouched. 602 */ 603 function canonicalProductUrl( button ) { 604 var raw = button && button.getAttribute( 'data-button-url' ); 605 if ( ! raw ) { return ''; } 606 try { 607 var url = new URL( raw, window.location.href ); 608 if ( url.origin !== window.location.origin || ! /^\/shop\/product\//.test( url.pathname ) ) { return ''; } 609 return url.href; 610 } catch ( error ) { 611 return ''; 612 } 613 } 614 615 function upgradeCatalogueProductLinks( scope ) { 616 scope = scope || document; 617 var selector = '[data-fluent-cart-single-product-card-view-button][data-button-url]'; 618 var buttons = []; 619 if ( scope.matches && scope.matches( selector ) ) { buttons.push( scope ); } 620 Array.prototype.push.apply( buttons, scope.querySelectorAll ? scope.querySelectorAll( selector ) : [] ); 621 buttons.forEach( function ( button ) { 622 var href = canonicalProductUrl( button ); 623 if ( ! href || ! button.parentNode ) { return; } 624 var link = document.createElement( 'a' ); 625 link.href = href; 626 link.className = 'fct-product-view-button ta-fcvg-direct-product-link'; 627 // Keep FluentCart's button palette when replacing its button with an 628 // anchor: Divi's general link selector otherwise overrides the label. 629 // Retain the variable (not a sampled colour) so Appearance still owns it. 630 if ( ! document.body.classList.contains( 'fluent-cart-no-colors' ) ) { 631 link.style.setProperty( 'color', 'var(--fct-card-btn-text-color)' ); 632 } 633 link.setAttribute( 'aria-label', ( button.getAttribute( 'aria-label' ) || 'View product details' ).replace( /^View options/i, 'View product details' ) ); 634 var label = document.createElement( 'span' ); 635 label.className = 'fct-button-text'; 636 label.textContent = 'View product details'; 637 link.appendChild( label ); 638 button.parentNode.replaceChild( link, button ); 639 } ); 640 } 641 642 // Capture the small pre-upgrade race without intercepting cart or checkout
643 // controls. Keyboard activation also dispatches click for the native button. 644 document.addEventListener( 'click', function ( event ) { 645 var button = event.target && event.target.closest 646 ? event.target.closest( '[data-fluent-cart-single-product-card-view-button][data-button-url]' ) 647 : null; 648 var href = canonicalProductUrl( button ); 649 if ( ! href ) { return; } 650 event.preventDefault(); 651 event.stopPropagation(); 652 event.stopImmediatePropagation(); 653 window.location.assign( href ); 654 }, true ); 655 656 /** 657 * FluentCart reuses its single-product renderer inside the catalogue modal, 658 * where the product title is emitted as an H1. The catalogue page already has 659 * its own H1, so make the injected title the dialog's H2 and accessible name. 660 * This is deliberately modal-scoped and leaves canonical PDP headings intact. 661 */ 662 function normalizeQuickViewHeading() { 663 var modal = document.querySelector( '[data-fluent-cart-shop-app-single-product-modal]' ); 664 if ( ! modal ) { return; } 665 var title = modal.querySelector( '.fct-product-title > h1' ); 666 if ( ! title ) { return; } 667 var heading = document.createElement( 'h2' ); 668 Array.prototype.forEach.call( title.attributes, function ( attr ) { 669 heading.setAttribute( attr.name, attr.value ); 670 } ); 671 while ( title.firstChild ) { heading.appendChild( title.firstChild ); } 672 title.parentNode.replaceChild( heading, title ); 673 if ( ! heading.id ) { heading.id = 'fct-product-modal-title'; } 674 modal.removeAttribute( 'aria-label' ); 675 modal.setAttribute( 'aria-labelledby', heading.id ); 676 } 677 if ( document.readyState === 'loading' ) { 678 document.addEventListener( 'DOMContentLoaded', function () { 679 boot( document ); 680 new MutationObserver( function ( records ) { 681 records.forEach( function ( record ) { 682 Array.prototype.forEach.call( record.addedNodes, function ( node ) { 683 if ( node.nodeType === 1 ) { upgradeCatalogueProductLinks( node ); } 684 } ); 685 } ); 686 } ).observe( document.body, { childList: true, subtree: true } ); 687 } ); 688 } else { 689 boot( document ); 690 new MutationObserver( function ( records ) { 691 records.forEach( function ( record ) { 692 Array.prototype.forEach.call( record.addedNodes, function ( node ) { 693 if ( node.nodeType === 1 ) { upgradeCatalogueProductLinks( node ); } 694 } ); 695 } ); 696 } ).observe( document.body, { childList: true, subtree: true } ); 697 } 698 699 window.addEventListener( 'fluentCartSingleProductModalOpened', function () { 700 boot( document ); 701 normalizeQuickViewHeading(); 702 } ); 703}() );
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.