PageSourceSearch

https://tobiakiode.com/wp-content/plugins/ta-fluentcart-variation-gallery/assets/gallery.js?ver=0.12.3

js tobiakiode.com collected 2026-10-07 03:48:20 UTC 30,012 bytes, 703 lines download raw bytes

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.