PageSourceSearch

https://www.silex.co.uk/wp-content/plugins/woocommerce/assets/js/frontend/single-product.js?ver=11.1.2

js silex.co.uk collected 2026-10-04 13:29:55 UTC 22,586 bytes, 743 lines download raw bytes

1/*global wc_single_product_params, PhotoSwipe, PhotoSwipeUI_Default */
2jQuery( function( $ ) {
3
4	// wc_single_product_params is required to continue.
5	if ( typeof wc_single_product_params === 'undefined' ) {
6		return false;
7	}
8
9	$( 'body' )
10		// Tabs
11		.on( 'init', '.wc-tabs-wrapper, .woocommerce-tabs', function() {
12			$( this ).find( '.wc-tab, .woocommerce-tabs .panel:not(.panel .panel)' ).hide();
13
14			var hash  = window.location.hash;
15			var url   = window.location.href;
16			var $tabs = $( this ).find( '.wc-tabs, ul.tabs' ).first();
17
18			if ( hash.toLowerCase().indexOf( 'comment-' ) >= 0 || hash === '#reviews' || hash === '#tab-reviews' ) {
19				$tabs.find( 'li.reviews_tab a' ).trigger( 'click' );
20			} else if ( url.indexOf( 'comment-page-' ) > 0 || url.indexOf( 'cpage=' ) > 0 ) {
21				$tabs.find( 'li.reviews_tab a' ).trigger( 'click' );
22			} else if ( hash === '#tab-additional_information' ) {
23				$tabs.find( 'li.additional_information_tab a' ).trigger( 'click' );
24			} else {
25				$tabs.find( 'li:first a' ).trigger( 'click' );
26			}
27		} )
28		.on( 'click', '.wc-tabs li a, ul.tabs li a', function( e ) {
29			e.preventDefault();
30			var $tab          = $( this );
31			var $tabs_wrapper = $tab.closest( '.wc-tabs-wrapper, .woocommerce-tabs' );
32			var $tabs         = $tabs_wrapper.find( '.wc-tabs, ul.tabs' );
33
34			$tabs.find( 'li' ).removeClass( 'active' );
35			$tabs
36				.find( 'a[role="tab"]' )
37				.attr( 'aria-selected', 'false' )
38				.attr( 'tabindex', '-1' );
39			$tabs_wrapper.find( '.wc-tab, .panel:not(.panel .panel)' ).hide();
40
41			$tab.closest( 'li' ).addClass( 'active' );
42			$tab
43				.attr( 'aria-selected', 'true' )
44				.attr( 'tabindex', '0' );
45			$tabs_wrapper.find( '#' + $tab.attr( 'href' ).split( '#' )[1] ).show();
46		} )
47		.on( 'keydown', '.wc-tabs li a, ul.tabs li a', function( e ) {
48			var isRTL     = document.documentElement.dir === 'rtl';
49			var direction = e.key;
50			var next      = isRTL ? 'ArrowLeft' : 'ArrowRight';
51			var prev      = isRTL ? 'ArrowRight' : 'ArrowLeft';
52			var down      = 'ArrowDown';
53			var up        = 'ArrowUp';
54			var home	  = 'Home';
55			var end		  = 'End';
56
57			if ( ! [ next, prev, down, up, end, home ].includes( direction ) ) {
58				return;
59			}
60
61			var $tab          = $( this );
62			var $tabs_wrapper = $tab.closest( '.wc-tabs-wrapper, .woocommerce-tabs' );
63			var $tabsList     = $tabs_wrapper.find( '.wc-tabs, ul.tabs' );
64			var $tabs         = $tabsList.find( 'a[role="tab"]' );
65			var endIndex	  = $tabs.length - 1;
66			var tabIndex      = $tabs.index( $tab );
67			var targetIndex   = direction === prev || direction === up ? tabIndex - 1 : tabIndex + 1;
68			var orientation   = 'horizontal';
69
70			/**
71			 * We don't know if the tabs are going to be vertical or horizontal,
72			 * so let's try to detect the orientation depending on the position of the tabs.
73			*/
74			if ( $tabs.length >= 2 ) {
75				var firstTab = $tabs[0].getBoundingClientRect();
76				var secondTab = $tabs[1].getBoundingClientRect();
77
78				var orientation = Math.abs( secondTab.top - firstTab.top ) > Math.abs( secondTab.left - firstTab.left )
79					? 'vertical'
80					: 'horizontal';
81			}
82
83			/**
84			 * If the tabs are vertical, we don't need to detect left/right keys
85			 * If the tabs are horizontal, we don't need to detect up/down keys
86			*/
87			if (
88				( orientation === 'vertical' && ( direction === prev || direction === next ) ) ||
89				( orientation === 'horizontal' && ( direction === up || direction === down ) )
90			) {
91				return;
92			}
93
94			e.preventDefault();
95
96			if (
97				( direction === prev && tabIndex === 0 && orientation === 'horizontal' ) ||
98				( direction === up && tabIndex === 0 && orientation === 'vertical' ) ||
99				direction === end
100			) {
101				targetIndex = endIndex;
102			} else if (
103				( next === direction && tabIndex === endIndex && orientation === 'horizontal' ) ||
104				( down === direction && tabIndex === endIndex && orientation === 'vertical' ) ||
105				direction === home
106			) {
107				targetIndex = 0;
108			}
109
110			$tabs.eq( targetIndex ).trigger( 'focus' );
111		} )
112		// Review link
113		.on( 'click', 'a.woocommerce-review-link', function() {
114			$( '.reviews_tab a' ).trigger( 'click' );
115			return true;
116		} )
117		// Star ratings for comments
118		.on( 'init', '#rating', function() {
119			$( this )
120				.hide()
121				.before(
122					'<p class="stars">\
123						<span role="group" aria-labelledby="comment-form-rating-label">\
124							<a role="radio" tabindex="0" aria-checked="false" class="star-1" href="#">
124' +
125								wc_single_product_params.i18n_rating_options[0] +
126							'</a>\
127							<a role="radio" tabindex="-1" aria-checked="false" class="star-2" href="#">' +
128								wc_single_product_params.i18n_rating_options[1] +
129							'</a>\
130							<a role="radio" tabindex="-1" aria-checked="false" class="star-3" href="#">' +
131								wc_single_product_params.i18n_rating_options[2] +
132							'</a>\
133							<a role="radio" tabindex="-1" aria-checked="false" class="star-4" href="#">' +
134								wc_single_product_params.i18n_rating_options[3] +
135							'</a>\
136							<a role="radio" tabindex="-1" aria-checked="false" class="star-5" href="#">' +
137								wc_single_product_params.i18n_rating_options[4] +
138							'</a>\
139						</span>\
140					</p>'
141				);
142		} )
143		.on( 'click', '#respond p.stars a', function() {
144			var $star   	= $( this ),
145				starPos     = $star.closest( 'p.stars' ).find( 'a' ).index( $star ) + 1,
146				$rating 	= $( this ).closest( '#respond' ).find( '#rating' ),
147				$container 	= $( this ).closest( '.stars' );
148
149			$rating.val( starPos );
150			$star.siblings( 'a' )
151				.removeClass( 'active' )
152				.attr( 'aria-checked', 'false' )
153				.attr( 'tabindex', '-1' );
154			$star
155				.addClass( 'active' )
156				.attr( 'aria-checked', 'true' )
157				.attr( 'tabindex', '0' );
158			$container.addClass( 'selected' );
159
160			return false;
161		} )
162		.on( 'click', '#respond #submit', function() {
163			var $rating = $( this ).closest( '#respond' ).find( '#rating' ),
164				rating  = $rating.val();
165
166			if ( $rating.length > 0 && ! rating && wc_single_product_params.review_rating_required === 'yes' ) {
167				window.alert( wc_single_product_params.i18n_required_rating_text );
168
169				return false;
170			}
171		} )
172		/**
173		 * Handle keyup events for tabs, tabs li a, and respond p.stars a.
174		 * The stopPropagation is used to prevent the keyup event from being triggered on the flexslider.
175		 */
176		.on( 'keyup', '.wc-tabs li a, ul.tabs li a, #respond p.stars a', function( e ) {
177			var direction = e.key;
178			var next = [ 'ArrowRight', 'ArrowDown' ];
179			var prev = [ 'ArrowLeft', 'ArrowUp' ];
180			var allDirections = next.concat( prev );
181
182			if ( ! allDirections.includes( direction ) ) {
183				return;
184			}
185
186			e.preventDefault();
187			e.stopPropagation();
188
189			if ( next.includes( direction ) ) {
190				$( this ).next().trigger( 'focus' ).click();
191
192				return;
193			}
194
195			$( this ).prev().trigger( 'focus' ).click();
196		} );
197
198	// Init Tabs and Star Ratings
199	$( '.wc-tabs-wrapper, .woocommerce-tabs, #rating' ).trigger( 'init' );
200
201	var productGalleryElement;
202
203	/**
204	 * Product gallery class.
205	 */
206	var ProductGallery = function( $target, args ) {
207		this.$target = $target;
208		this.$images = $( '.woocommerce-product-gallery__image', $target );
209
210		// No images? Abort.
211		if ( 0 === this.$images.length ) {
212			this.$target.css( 'opacity', 1 );
213			return;
214		}
215
216		// Make this object available.
217		$target.data( 'product_gallery', this );
218
219		// Pick functionality to initialize...
220		this.flexslider_enabled = 'function' === typeof $.fn.flexslider && wc_single_product_params.flexslider_enabled;
221		this.zoom_enabled       = 'function' === typeof $.fn.zoom && wc_single_product_params.zoom_enabled;
222		this.photoswipe_enabled = typeof PhotoSwipe !== 'undefined' && wc_single_product_params.photoswipe_enabled;
223
224		// ...also taking args into account.
225		if ( args ) {
226			this.flexslider_enabled = false === args.flexslider_enabled ? false : this.flexslider_enabled;
227			this.zoom_enabled       = false === args.zoom_enabled ? false : this.zoom_enabled;
228			this.photoswipe_enabled = false === args.photoswipe_enabled ? false : this.photoswipe_enabled;
229		}
230
231		// ...and what is in the gallery.
232		if ( 1 === this.$images.length ) {
233			this.flexslider_enabled = false;
234		}
235
236		// Bind functions to this.
237		this.initFlexslider       = this.initFlexslider.bind( this );
238		this.initZoom             = this.initZoom.bind( this );
239		this.initZoomForTarget    = this.initZoomForTarget.bind( this );
240		this.initPhotoswipe       = this.initPhotoswipe.bind( this );
241		this.initVideoThumbnailPreviews = this.initVideoThumbnailPreviews.bind( this );
242		this.syncVideoPlayback    = this.syncVideoPlayback.bind( this );
243		this.getPhotoswipeVideoHtml = this.getPhotoswipeVideoHtml.bind( this );
244		this.syncPhotoswipeVideoPlayback = this.syncPhotoswipeVideoPlayback.bind( this );
245		this.onResetSlidePosition = this.onResetSlidePosition.bind( this );
246		this.getGalleryItems      = this.getGalleryItems.bind( this );
247		this.openPhotoswipe       = this.openPhotoswipe.bind( this );
248		this.trapFocusPhotoswipe  = this.trapFocusPhotoswipe.bind( this );
249		this.handlePswpTrapFocus  = this.handlePswpTrapFocus.bind( this );
250
251		if ( this.flexslider_enabled ) {
252			this.initFlexslider( args.flexslider );
253			$target.on( 'woocommerce_gallery_reset_slide_position', this.onResetSlidePosition );
254		} else {
255			this.$target.css( 'opacity', 1 );
256			this.syncVideoPlayback( 0 );
257		}
258
259		if ( this.zoom_enabled ) {
260			this.initZoom();
261			$target.on( 'woocommerce_gallery_init_zoom', this.initZoom );
262		}
263
264		if ( this.photoswipe_enabled ) {
265			this.initPhotoswipe();
266		}
267	};
268
269	/**
270	 * Initialize flexSlider.
271	 */
272	ProductGallery.prototype.initFlexslider = function( args ) {
273		var $target = this.$target,
274			gallery = this;
275
276		var options = $.extend( {
277			selector: '.woocommerce-product-gallery__wrapper > .woocommerce-product-gallery__image',
278			start: function( slider ) {
279				$target.css( 'opacity', 1 );
280				gallery.syncVideoPlayback( slider.currentSlide );
281				gallery.syncVideoThumbnailPreviewOpacity();
282			},
283			after: function( slider ) {
284				gallery.initZoomForTarget( gallery.$images.eq( slider.currentSlide ) );
285				gallery.syncVideoPlayback( slider.currentSlide );
286				gallery.syncVideoThumbnailPreviewOpacity();
287			}
288		}, args );
289
290		$target.flexslider( options );
291		gallery.initVideoThumbnailPreviews();
292		$target
293			.off( 'click.wcProductGalleryVideoThumbs', '.flex-control-thumbs img' )
294			.on( 'click.wcProductGalleryVideoThumbs', '.flex-control-thumbs img', function() {
295				window.setTimeout( function() {
296					gallery.syncVideoPlayback();
297					gallery.syncVideoThumbnailPreviewOpacity();
298				}, 0 );
299			} );
300
301		// Trigger resize after main image loads to ensure correct gallery size.
302		$( '.woocommerce-product-gallery__wrapper .woocommerce-product-gallery__image:eq(0) .wp-post-image' ).one( 'load', function() {
303			var $image = $( this );
304
305			if ( $image ) {
306				setTimeout( function() {
307					var setHeight = $image.closest( '.woocommerce-product-gallery__image' ).height();
308					var $viewport = $image.closest( '.flex-viewport' );
309
310					if ( setHeight && $viewport ) {
311						$viewport.height( setHeight );
312					}
313				}, 100 );
314			}
315		} ).each( function() {
316			if ( this.complete ) {
317				$( this ).trigger( 'load' );
318			}
319		} );
320	};
321
322	/**
323	 * Play only the active gallery video.
324	 */
325	ProductGallery.prototype.syncVideoPlayback = function( slideIndex ) {
326		const $activeSlide = 'number' === typeof slideIndex
327			? this.$images.eq( slideIndex )
328			: this.$target.find( '.flex-active-slide' ).first();
329
330		this.$images.find( 'video.wp-post-video' ).each( function() {
331			if ( $activeSlide.has( this ).length ) {
332				const playPromise = this.play();
333
334				if ( playPromise && playPromise.catch ) {
335					playPromise.catch( function() {
336						return undefined;
337					} );
338				}
339			} else {
340				this.pause();
341			}
342		} );
343	};
344
345	/**
346	 * Show browser-rendered previews for video thumbnails without posters.
347	 */
348	ProductGallery.prototype.initVideoThumbnailPreviews = function() {
349		const $thumbs = this.$target.find( '.flex-control-thumbs img' );
350
351		if ( ! $thumbs.length ) {
352			return;
353		}
354
355		this.$images.each( function( index, slide ) {
356			const videoSrc = $( slide ).attr( 'data-thumb-video-src' );
357			const $thumb = $thumbs.eq( index );
358			const $thumbItem = $thumb.closest( 'li' );
359
360			if ( ! videoSrc || ! $thumb.length || $thumb.siblings( 'video' ).length ) {
361				return;
362			}
363
364			const video = $( '<video />', {
365				class: 'woocommerce-product-gallery__video-thumbnail-preview',
366				src: videoSrc,
367				preload: 'metadata',
368				muted: 'muted',
369				playsinline: 'playsinline',
370				'aria-hidden': 'true',
371			} ).css( {
372				position: 'absolute',
373				top: 0,
374				right: 0,
375				bottom: 0,
376				left: 0,
377				width: '100%',
378				height: '100%',
379				objectFit: 'cover',
380				opacity: 0.5,
381				pointerEvents: 'none',
382			} );
383
384			$thumbItem
385				.addClass( 'woocommerce-product-gallery__video-thumbnail' )
386				.css( 'position', 'relative' );
387			$thumb
388				.addClass( 'woocommerce-product-gallery__video-thumbnail-placeholder' )
389				.css( 'opacity', 0 )
390				.after( video );
391		} );
392
393		this.syncVideoThumbnailPreviewOpacity();
394	};
395
396	/**
397	 * Sync video thumbnail preview opacity with FlexSlider active state.
398	 */
399	ProductGallery.prototype.syncVideoThumbnailPreviewOpacity = function() {
400		this.$target
401			.find( '.flex-control-thumbs li.woocommerce-product-gallery__video-thumbnail' )
402			.each( function() {
403				const $thumbItem = $( this );
404				const isActive = $thumbItem.find( 'img.flex-active' ).length > 0;
405
406				$thumbItem
407					.find( '.woocommerce-product-gallery__video-thumbnail-preview' )
408					.css( 'opacity', isActive ? 1 : 0.5 );
409			} );
410	};
411
412	/**
413	 * Init zoom.
414	 */
415	ProductGallery.prototype.initZoom = function() {
416		if (document.readyState === 'complete') {
417			this.initZoomForTarget(this.$images.first());
418		} else {
419			$(window).on('load', () => {
420				this.initZoomForTarget(this.$images.first());
421			});
422		}
423	};
424
425	/**
426	 * Init zoom.
427	 */
428	ProductGallery.prototype.initZoomForTarget = function( zoomTarget ) {
429		if ( ! this.zoom_enabled ) {
430			return false;
431		}
432
433		var galleryWidth = this.$target.width(),
434			zoomEnabled  = false;
435
436		$( zoomTarget ).each( function( index, target ) {
437			var image = $( target ).find( 'img' );
438
439			if ( image.data( 'large_image_width' ) > galleryWidth ) {
440				zoomEnabled = true;
441				return false;
442			}
443		} );
444
445		// But only zoom if the img is larger than its container.
446		if ( zoomEnabled ) {
447			var zoom_options = $.extend( {
448				touch: false,
449				callback: function() {
450					var zoomImg = this;
451
452					setTimeout( function() {
453						zoomImg.removeAttribute( 'role' );
454						zoomImg.setAttribute( 'alt', '' );
455						zoomImg.setAttribute( 'aria-hidden', 'true' );
456					}, 100 );
457				}
458			}, wc_single_product_params.zoom_options );
459
460			if ( 'ontouchstart' in document.documentElement ) {
461				zoom_options.on = 'click';
462			}
463
464			zoomTarget.trigger( 'zoom.destroy' );
465			zoomTarget.zoom( zoom_options );
466
467			setTimeout( function() {
468				if ( zoomTarget.find(':hover').length ) {
469					zoomTarget.trigger( 'mouseover' );
470				}
471			}, 100 );
472		}
473	};
474
475	/**
476	 * Init PhotoSwipe.
477	 */
478	ProductGallery.prototype.initPhotoswipe = function() {
479		if ( this.zoom_enabled && this.$images.length > 0 ) {
480			this.$target.prepend(
481				'<a href="#" role="button" class="woocommerce-product-gallery__trigger" aria-haspopup="dialog" ' +
482				'aria-controls="photoswipe-fullscreen-dialog" aria-label="' +
483				wc_single_product_params.i18n_product_gallery_trigger_text + '">' +
484					'<span aria-hidden="true">🔍</span>' +
485				'</a>'
486			);
487			this.$target.on( 'click', '.woocommerce-product-gallery__trigger', this.openPhotoswipe );
488			this.$target.on( 'keydown', '.woocommerce-product-gallery__trigger', ( e ) => {
489				if ( e.key === ' ' ) {
490					this.openPhotoswipe( e );
491				}
492			} );
493			this.$target.on( 'click', '.woocommerce-product-gallery__image a', this.openPhotoswipe );
494		} else {
495			this.$target.on( 'click', '.woocommerce-product-gallery__image a', this.openPhotoswipe );
496		}
497	};
498
499	/**
500	 * Reset slide position to 0.
501	 */
502	ProductGallery.prototype.onResetSlidePosition = function() {
503		this.$target.flexslider( 0 );
504	};
505
506	/**
507	 * Get product gallery media items.
508	 */
509	ProductGallery.prototype.getGalleryItems = function() {
510		const $slides = this.$images;
511		const items = [];
512		const gallery = this;
513
514		if ( $slides.length > 0 ) {
515			$slides.each( function( i, el ) {
516				const media = $( el )
517					.find( 'img[data-large_image], video[data-large_image]' )
518					.first();
519
520				if ( media.length ) {
521					const large_image_src = media.attr( 'data-large_image' );
522					const large_image_w = media.attr( 'data-large_image_width' );
523					const large_image_h = media.attr( 'data-large_image_height' );
524					const alt = media.attr( 'alt' ) || media.attr( 'aria-label' );
525					const title = media.attr( 'data-caption' )
526						? media.attr( 'data-caption' )
527						: media.attr( 'title' );
528					let item;
529
530					if ( media.is( 'video' ) ) {
531						item = {
532							html: gallery.getPhotoswipeVideoHtml( media ),
533							w: large_image_w,
534							h: large_image_h,
535							title,
536							video: true,
537						};
538					} else {
539						item = {
540							alt,
541							src: large_image_src,
542							w: large_image_w,
543							h: large_image_h,
544							title,
545						};
546					}
547
548					items.push( item );
549				}
550			} );
551		}
552
553		return items;
554	};
555
556	/**
557	 * Get PhotoSwipe video slide HTML.
558	 */
559	ProductGallery.prototype.getPhotoswipeVideoHtml = function( media ) {
560		const video = media.get( 0 );
561		const src = video.currentSrc || media.attr( 'src' );
562		const $video = $( '<video />
562', {
563			class: 'woocommerce-product-gallery__photoswipe-video',
564			src,
565			preload: 'metadata',
566			muted: 'muted',
567			loop: 'loop',
568			playsinline: 'playsinline',
569			style: 'display:block;max-width:100%;max-height:100%;width:auto;height:auto;object-fit:contain;',
570			'aria-label': media.attr( 'aria-label' ) || '',
571		} );
572		const poster = media.attr( 'poster' );
573
574		if ( poster ) {
575			$video.attr( 'poster', poster );
576		}
577
578		return $( '<div />', {
579			class: 'woocommerce-product-gallery__photoswipe-video-wrapper',
580			style: 'display:flex;align-items:center;justify-content:center;width:100%;height:100%;',
581		} )
582			.append( $video )
583			.prop( 'outerHTML' );
584	};
585
586	/**
587	 * Play only the current PhotoSwipe video.
588	 */
589	ProductGallery.prototype.syncPhotoswipeVideoPlayback = function( photoswipe ) {
590		const activeContainer =
591			photoswipe.currItem && photoswipe.currItem.container;
592
593		$( photoswipe.template )
594			.find( 'video.woocommerce-product-gallery__photoswipe-video' )
595			.each( function() {
596				if ( activeContainer && activeContainer.contains( this ) ) {
597					const playPromise = this.play();
598
599					if ( playPromise && playPromise.catch ) {
600						playPromise.catch( function() {
601							return undefined;
602						} );
603					}
604				} else {
605					this.pause();
606				}
607			} );
608	};
609
610	/**
611	 * Open photoswipe modal.
612	 */
613	ProductGallery.prototype.openPhotoswipe = function( e ) {
614		e.preventDefault();
615
616		var pswpElement = $( '.pswp' )[0],
617			items         = this.getGalleryItems(),
618			eventTarget   = $( e.target ),
619			currentTarget = e.currentTarget,
620			flexslider    = this.flexslider_enabled ? this.$target.data( 'flexslider' ) : false,
621			self          = this,
622			clicked,
623			index;
624
625		if ( 0 < eventTarget.closest( '.woocommerce-product-gallery__trigger' ).length ) {
626			clicked = this.$target.find( '.flex-active-slide' );
627		} else {
628			clicked = eventTarget.closest( '.woocommerce-product-gallery__image' );
629		}
630
631		index = $( clicked ).index();
632
633		if ( flexslider && 'number' === typeof flexslider.currentSlide ) {
634			index = flexslider.currentSlide;
635		}
636
637		var options = $.extend( {
638			index: index,
639			addCaptionHTMLFn: function( item, captionEl ) {
640				if ( ! item.title ) {
641					captionEl.children[0].textContent = '';
642					return false;
643				}
644				captionEl.children[0].textContent = item.title;
645				return true;
646			},
647			timeToIdle: 0, // Ensure the gallery controls are always visible to avoid keyboard navigation issues.
648		}, wc_single_product_params.photoswipe_options );
649
650		// Initializes and opens PhotoSwipe.
651		var photoswipe = new PhotoSwipe( pswpElement, PhotoSwipeUI_Default, items, options );
652
653		photoswipe.listen( 'afterInit', function() {
654			self.trapFocusPhotoswipe( true );
655			self.syncPhotoswipeVideoPlayback( photoswipe );
656		});
657
658		photoswipe.listen( 'afterChange', function() {
659			self.syncPhotoswipeVideoPlayback( photoswipe );
660		});
661
662		photoswipe.listen( 'close', function() {
663			$( photoswipe.template )
664				.find( 'video.woocommerce-product-gallery__photoswipe-video' )
665				.each( function() {
666					this.pause();
667				} );
668			self.trapFocusPhotoswipe( false );
669			currentTarget.focus();
670		});
671
672		photoswipe.init();
673	};
674
675	/**
676	 * Control focus in photoswipe modal.
677	 *
678	 * @param {boolean} trapFocus - Whether to trap focus or not.
679	 */
680	ProductGallery.prototype.trapFocusPhotoswipe = function( trapFocus ) {
681		var pswp = document.querySelector( '.pswp' );
682
683		if ( ! pswp ) {
684			return;
685		}
686
687		if ( trapFocus ) {
688			pswp.addEventListener( 'keydown', this.handlePswpTrapFocus );
689		} else {
690			pswp.removeEventListener( 'keydown', this.handlePswpTrapFocus );
691		}
692	};
693
694	/**
695	 * Handle keydown event in photoswipe modal.
696	 */
697	ProductGallery.prototype.handlePswpTrapFocus = function( e ) {
698		var allFocusablesEls      = e.currentTarget.querySelectorAll( 'button:not([disabled])' );
699		var filteredFocusablesEls = Array.from( allFocusablesEls ).filter( function( btn ) {
700			return btn.style.display !== 'none' && window.getComputedStyle( btn ).display !== 'none';
701		} );
702
703		if ( 1 >= filteredFocusablesEls.length ) {
704			return;
705		}
706
707		var firstTabStop = filteredFocusablesEls[0];
708		var lastTabStop  = filteredFocusablesEls[filteredFocusablesEls.length - 1];
709
710		if ( e.key === 'Tab' ) {
711			if ( e.shiftKey ) {
712				if ( document.activeElement === firstTabStop ) {
713					e.preventDefault();
714					lastTabStop.focus();
715				}
716			} else if ( document.activeElement === lastTabStop ) {
717				e.preventDefault();
718				firstTabStop.focus();
719			}
720		}
721	};
722
723	/**
724	 * Function to call wc_product_gallery on jquery selector.
725	 */
726	$.fn.wc_product_gallery = function( args ) {
727		new ProductGallery( this, args || wc_single_product_params );
728		return this;
729	};
730
731	/*
732	 * Initialize all galleries on page.
733	 */
734	$( '.woocommerce-product-gallery' ).each( function() {
735
736		$( this ).trigger( 'wc-product-gallery-before-init', [ this, wc_single_product_params ] );
737
738		productGalleryElement = $( this ).wc_product_gallery( wc_single_product_params );
739
740		$( this ).trigger( 'wc-product-gallery-after-init', [ this, wc_single_product_params ] );
741
742	} );
743} );

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.