PageSourceSearch

https://2africa.org/_static/??-eJyFzNEKwjAMheEXsqYKTr0QnyVro652TUl…4wcW6YZQeGmlNfg1mH+X8fL7nj23cHv/ekFtWNW2Q==

js 2africa.org collected 2026-10-04 05:10:05 UTC 94,001 bytes, 2,638 lines download raw bytes

1( function( $ ) {
2
3    // Clickable Tabs
4    $('.charitable-campaign-nav').on( 'click', 'li.tab_title a', function( e ) {
5
6        e.preventDefault();
7
8        var $preview = $( '.charitable-campaign-preview' ),
9            tab_id = $( this ).parent().data('tab-id'),
10            tab_type = $preview.find( 'li#tab_' + tab_id + '_title').attr( 'data-tab-type' ); // eslint-disable-line
11
12        // clear the active states of the tabs and content areas in the preview area
13        $('.tab-content ul li').removeClass('active');
14        $('nav li.tab_title').removeClass('active');
15
16        // make the clicked on tab and it's content area active
17        $( this ).parent().addClass('active');
18        $('.tab-content ul li#tab_' + tab_id + '_content').addClass('active');
19
20        // now clear any of the left option panel 'tabs' from being active... then add active to the proper one
21        $('.charitable-layout-options-tab .charitable-group').removeClass('active');
22        $(".charitable-layout-options-tab").find('[data-group_id=' + tab_id + ']').addClass('active').removeClass('charitable-closed');
23        $(".charitable-layout-options-tab").find('[data-group_id=' + tab_id + ']').find('.charitable-group-rows').show();
24        $(".charitable-layout-options-tab").find('[data-group_id=' + tab_id + ']').find('.charitable-toggleable-group i').removeClass('.charitable-angle-right').addClass('charitable-angle-down');
25        $( '#layout-options a' ).click();
26
27    } );
28
29    // Clickable Donate Amount
30    $('.charitable-campaign-field').on( 'click', 'ul.charitable-template-donation-amounts li', function( e ) { // eslint-disable-line
31
32        // do the UI inside the donate field itself on the frontend.
33        $( this ).parent().find('li').removeClass('selected');
34        $( this ).addClass('selected');
35        $( this ).find('input[type="radio"]').prop("checked", true);
36
37        if ( $( this ).hasClass('custom-donation-amount' ) ) { // eslint-disable-line no-undef
38
39        } else {
40
41            const donationAmountSelected = $( this ).find( 'input[type="radio"]' ).val(); // example: 10.00
42
43            var donation_amount_radio = null,
44                selected_amount_string = '';
45
46            // update any donate button in the <header> of this campaign form to reflect the selected amount
47            $( this ).closest('.charitable-campaign-container').find('form.campaign-donation input[name="charitable_donation_amount"]').val( donationAmountSelected );
48
49            // check if the donation form exists on this page, or even if it's hidden (modal).
50            if( $('form#charitable-donation-form').length > 0 && $('.charitable-donation-options').length > 0 ) { // eslint-disable-line
51                // find the radio inside of charitable-donation-options that has a value of donationAmountSelected and select that radio button.
52                donation_amount_radio = $('.charitable-donation-options').find('input[type="radio"][value="' + donationAmountSelected + '"]'); // eslint-disable-line
53                donation_amount_radio.prop("checked", true);
54
55                // remove all selected classes from the suggested donation amounts
56                $('form#charitable-donation-form').find('.suggested-donation-amount').removeClass('selected');
57                donation_amount_radio.closest('.suggested-donation-amount').addClass('selected');
58
59                // is the form showing a "your donation amount" field due to session?
60                if( $('form#charitable-donation-form').find('.charitable-your-donation-amount').length > 0 ) { // eslint-disable-line
61                    // first let's get the currency value of the amount that was just selected.
62                    selected_amount_string = donation_amount_radio.closest('.suggested-donation-amount').find('.amount').text();
63                    if ( selected_amount_string.length > 0 ) {
64                        $('form#charitable-donation-form').find('.charitable-your-donation-amount').find('p strong').text( selected_amount_string );
65                    }
66                }
67            }
68
69        }
70    } );
71
72    $('.charitable-campaign-field').on( 'keyup', 'ul.charitable-template-donation-amounts input[name="custom_donation_amount"]', function( e ) { // eslint-disable-line
73
74        $( this ).closest('.charitable-campaign-container').find('form.campaign-donation input[name="charitable_donation_amount"]').val( $( this ).val() );
75
76        var donation_amount_radio = null,
77            custom_donation_textbox = null,
78            custom_donation_amount = '',
79            selected_amount_string = '';
80
81        const donationAmountSelected = $( this ).val(); // example: 10.00
82
83        if( $('form#charitable-donation-form').length > 0
84            && $('form#charitable-donation-form').find('input[name="custom_donation_amount"]').length > 0
85            && $('.charitable-template-donation-amounts').find('input[name="custom_donation_amount"]').length > 0
86        ) { // eslint-disable-line
87            custom_donation_textbox = $('.charitable-template-donation-amounts').find('input[name="custom_donation_amount"]')
88            custom_donation_amount = custom_donation_textbox.val();
89            // if there is an amount populate the field in the main charitable-donation-form.
90            if ( custom_donation_amount.length > 0 ) {
91
92                donation_amount_radio = $('.charitable-donation-options').find('input[type="radio"][value="' + donationAmountSelected + '"]'); // eslint-disable-line
93                donation_amount_radio.prop("checked", true);
94
95                $('form#charitable-donation-form').find('input[name="custom_donation_amount"]').val( custom_donation_amount );
96                // remove all selected classes from the suggested donation amounts
97                $('form#charitable-donation-form').find('.suggested-donation-amount').removeClass('selected');
98                donation_amount_radio.closest('.suggested-donation-amount').addClass('selected');
99                // uncheck all the radio buttons.
100                $('form#charitable-donation-form').find('input[type="radio"]').prop("checked", false);
101            }
102        }
103        // is the form showing a "your donation amount" field due to session?
104        if( $('form#charitable-donation-form').find('.charitable-your-donation-amount').length > 0 ) { // eslint-disable-line
105            // first let's get the currency value of the amount that was just selected.
106            custom_donation_textbox = $('.charitable-template-donation-amounts').find('input[name="custom_donation_amount"]')
107            custom_donation_amount = custom_donation_textbox.val();
108            if ( custom_donation_amount.length > 0 ) {
109                $('form#charitable-donation-form').find('.charitable-your-donation-amount').find('p strong').text( custom_donation_amount );
110            }
111        }
112
113    } );
114
115    // Sharing that requires JS.
116
117        // Grab link from the DOM
118        const mastodonShareButton = document.querySelector('.charitable-mastodon-share');
119
120        if ( mastodonShareButton !== null ) {
121
122            // When a user clicks the link
123            mastodonShareButton.addEventListener('click', (e) => {
124
125                // If the user has already entered their instance and it is in localstorage
126                // write out the link href with the instance and the current page title and URL
127                if(localStorage.getItem('mastodon-instance')) {
128                    mastodonShareButton.href = `
129                    https://${localStorage.getItem('mastodon-instance')}/share?text=${encodeURIComponent(document.title)}%0A${encodeURIComponent(location.href)}`;
130                // otherwise, prompt the user for their instance and save it to localstorage
131                } else {
132                    e.preventDefault();
133                    let instance = window.prompt(
134                    'Please tell me your Mastodon instance'
135                    );
136                    localStorage.setItem('mastodon-instance', instance);
137                }
138
139            });
140
141        }
142
143    // Validate donation amount before submitting campaign donation form
144    // Also handle legacy forms that are in .campaign-summary (not in .charitable-campaign-container)
145    // @version 1.8.10.5 - Added support for legacy campaign forms
146    $(document).on( 'submit', 'form.campaign-donation', function( e ) { // eslint-disable-line
147
148        var $form = $( this );
149        var $container = $form.closest('.charitable-campaign-container');
150
151        // For legacy forms (not in .charitable-campaign-container or with data-legacy-form attribute), allow normal submission
152        // These forms don't have donation amount fields and should submit directly
153        // @version 1.8.10.5
154        if ( $container.length === 0 || $form.data('legacy-form') === 1 ) {
155            // This is a legacy form - allow it to submit normally
156            // Don't prevent default, just return true to allow submission
157            return true;
158        }
159
160        var donationAmount = 0;
161        var minimumDonation = 0;
162        var maximumDonation = 0;
163        var isValid = true;
164        var errorMessage = '';
165
166        // Get minimum and maximum donation amounts from CHARITABLE_VARS
167        if ( typeof CHARITABLE_VARS !== 'undefined' ) {
168            minimumDonation = parseFloat( CHARITABLE_VARS.minimum_donation ) || 0;
169            maximumDonation = parseFloat( CHARITABLE_VARS.maximum_donation ) || 0;
170        }
171
172        // Only validate if there's a minimum or maximum set
173        if ( minimumDonation > 0 || maximumDonation > 0 ) {
174
175            // Get the donation amount from the hidden input
176            var $hiddenAmountInput = $form.find('input[name="charitable_donation_amount"]');
177            var hiddenAmount = $hiddenAmountInput.length > 0 ? $hiddenAmountInput.val() : '';
178
179            // Helper function to parse donation amount (strips currency symbols and commas)
180            var parseDonationAmount = function( value ) {
181                if ( ! value || value === '' ) {
182                    return 0;
183                }
184                // Remove currency symbols, commas, and whitespace, then parse
185                var cleaned = String( value ).replace( /[^\d.-]/g, '' );
186                var parsed = parseFloat( cleaned );
187                return isNaN( parsed ) ? 0 : parsed;
188            };
189
190            // If hidden input has a value, use it (this is set by JavaScript when user selects/enters amount)
191            if ( hiddenAmount && hiddenAmount !== '' && hiddenAmount !== '0' ) {
192                donationAmount = parseDonationAmount( hiddenAmount );
193            } else {
194                // Fallback: check if a suggested amount radio is selected
195                var $selectedRadio = $container.find('ul.charitable-template-donation-amounts input[type="radio"][name="donation_amount"]:checked');
196                if ( $selectedRadio.length > 0 ) {
197                    var radioValue = $selectedRadio.val();
198                    // If it's "custom", check the custom input
199                    if ( radioValue === 'custom' ) {
200                        var $customInput = $container.find('input[name="custom_donation_amount"]');
201                        if ( $customInput.length > 0 ) {
202                            donationAmount = parseDonationAmount( $customInput.val() );
203                        }
204                    } else {
205                        donationAmount = parseDonationAmount( radioValue );
206                    }
207                } else {
208                    // Last fallback: check custom input directly
209                    var $customInput = $container.find('input[name="custom_donation_amount"]');
210                    if ( $customInput.length > 0 && $customInput.val() ) {
211                        donationAmount = parseDonationAmount( $customInput.val() );
212                    }
213                }
214            }
215
216            // Skip validation if using separate_page mode and n
216o amount is provided
217            // Amount will be selected on the donation page, not on the campaign page
218            // @version 1.8.11 - Fixed: Skip validation for separate_page when no amount provided
219            var donationFormDisplay = ( typeof CHARITABLE_VARS !== 'undefined' && CHARITABLE_VARS.donation_form_display ) ? CHARITABLE_VARS.donation_form_display : 'separate_page';
220            if ( donationFormDisplay === 'separate_page' && donationAmount === 0 ) {
221                // No amount provided and using separate page - skip validation, allow form submission
222                return true;
223            }
224
225            // Validate against minimum
226            if ( minimumDonation > 0 && donationAmount < minimumDonation ) {
227                isValid = false;
228                if ( typeof CHARITABLE_VARS !== 'undefined' && CHARITABLE_VARS.error_invalid_amount ) {
229                    errorMessage = CHARITABLE_VARS.error_invalid_amount;
230                } else {
231                    errorMessage = 'You must donate at least ' + minimumDonation.toFixed(2);
232                }
233            }
234
235            // Validate against maximum
236            if ( isValid && maximumDonation > 0 && donationAmount > maximumDonation ) {
237                isValid = false;
238                if ( typeof CHARITABLE_VARS !== 'undefined' && CHARITABLE_VARS.error_max_exceeded ) {
239                    errorMessage = CHARITABLE_VARS.error_max_exceeded;
240                } else {
241                    errorMessage = 'The maximum donation amount permitted is ' + maximumDonation.toFixed(2);
242                }
243            }
244
245            // If validation failed, prevent submission and show error
246            if ( ! isValid ) {
247                e.preventDefault();
248                e.stopPropagation();
249
250                // Remove any existing error messages
251                $container.find('.charitable-campaign-donation-amount-error').remove();
252
253                // Find the donation amounts container to place error below it
254                var $donationAmountsContainer = $container.find('ul.charitable-template-donation-amounts, ul.charitable-preview-donation-amounts');
255                if ( $donationAmountsContainer.length === 0 ) {
256                    // Fallback: find the donate amount field container
257                    $donationAmountsContainer = $container.find('.charitable-campaign-field-donate-amount, .charitable-field-donate-amount');
258                }
259
260                // Create and insert error message
261                var $errorMessage = $('<div class="charitable-campaign-donation-amount-error charitable-field-error-message charitable-notice charitable-form-errors"><ul class="errors"><li>' + errorMessage + '</li></ul></div>');
262
263                if ( $donationAmountsContainer.length > 0 ) {
264                    // Insert after the donation amounts container (or its parent if it's a ul)
265                    var $target = $donationAmountsContainer.parent().length > 0 ? $donationAmountsContainer.parent() : $donationAmountsContainer;
266                    $target.after( $errorMessage );
267                } else {
268                    // Fallback: insert before the form
269                    $form.before( $errorMessage );
270                }
271
272                // Scroll to error message
273                $('html, body').animate({
274                    scrollTop: $errorMessage.offset().top - 100
275                }, 300);
276
277                // Highlight the custom donation input if it exists and has focus
278                var $customInput = $container.find('input[name="custom_donation_amount"]');
279                if ( $customInput.length > 0 ) {
280                    $customInput.addClass('charitable-field-error');
281                }
282
283                return false;
284            } else {
285                // Remove any existing error messages and error classes
286                $container.find('.charitable-campaign-donation-amount-error').remove();
287                $container.find('input[name="custom_donation_amount"]').removeClass('charitable-field-error');
288            }
289        }
290
291        // If validation passed, allow form submission
292        return true;
293    } );
294
295})( jQuery ); // eslint-disable-line
296
297;
298( function( $ ) {
299
300    // upon page load, find out how many divs with ids starting with 'tp-charitable-holder-' there are.
301    var tp_charitable_holder_count = $( 'div[id^="tp-charitable-holder-"]' ).length + 1;
302
303    var tp_charitable_animate = true;
304
305    // Get the initial delay from localized script variables, with fallback to 5 seconds.
306    var initial_delay = ( typeof charitable_donortrust_vars !== 'undefined' && charitable_donortrust_vars.initial_delay ) ? charitable_donortrust_vars.initial_delay : 5;
307
308    // Get the number of repeats from localized script variables, with fallback to 0 (infinite).
309    var number_of_repeats = ( typeof charitable_donortrust_vars !== 'undefined' && charitable_donortrust_vars.number_of_repeats ) ? parseInt(charitable_donortrust_vars.number_of_repeats) : 0;
310
311    // add to the values of the style attribute of each of the divs with ids starting with 'tp-charitable-holder-' the following: 'bottom: -40px;'.
312    $( 'div[id^="tp-charitable-holder-"] .tp-widget-container' ).attr( 'style', function( i, style ) {
313        $(this).css('bottom', '-120px');
314        // $(this).css('opacity', '0');
315    });
316
317    // if the user clicks on .tp-widget-container and if there's a data-url attribute called 'data-link-url', open a new window with the value of the data-url attribute.
318    $('body').on('click', '.tp-widget-container', function(e) {
319        // Don't open link if clicking on the close button
320        if ( $(e.target).closest('.tp-notification-close').length > 0 ) {
321            return;
322        }
323
324        var data_url = $( this ).attr( 'data-link-url' );
325        if ( data_url ) {
326            window.open( data_url, '_blank' );
327        }
328    });
329
330    // helper: get list of dismissed donation ids from cookie (JSON array)
331    function getDismissedDonations() {
332        var raw = Cookies.get('tp_charitable_dismissed');
333        if (!raw) { return []; }
334        try { return JSON.parse(raw); } catch (err) { return []; }
335    }
336
337    // helper: persist list of dismissed donation ids to cookie (7 days)
338    function setDismissedDonations(list) {
339        try {
340            Cookies.set('tp_charitable_dismissed', JSON.stringify(list), { expires: 7, path: '/' });
341        } catch (err) { /* no-op */ }
342    }
343
344    // On click of close (X): prevent navigation, record donation id, and remove only matching badge(s)
345    $('body').on('click', '.tp-notification-close', function( e ) {
346        e.preventDefault();
347
348        // find the nearest holder and its donation identifier
349        var $holder = $(this).closest('div[id^="tp-charitable-holder-"]');
350        var donationId = $holder.attr('data-campaign');
351
352        if (donationId) {
353            var dismissed = getDismissedDonations();
354            if (dismissed.indexOf(donationId) === -1) {
355                dismissed.push(donationId);
356                setDismissedDonations(dismissed);
357            }
358
359            // remove all instances of this donation's badge on the page
360            $('div[id^="tp-charitable-holder-"][data-campaign="' + donationId + '"]').remove();
361        } else {
362            // fallback: remove just this container if id missing
363            $holder.remove();
364        }
365    });
366
367    $(document).ready(function() {
368        // Check if user prefers reduced motion
369        const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
370
371        // If user prefers reduced motion, remove all notification elements and exit early
372        if (prefersReducedMotion) {
373            $('div[id^="tp-charitable-holder-"]').remove();
374            return;
375        }
376
377        function slideDivs() {
378            const divs = $('div[id^="tp-charitable-holder-"] .tp-widget-container');
379            let currentIndex = 0;
380            let completedCycles = 0;
381
382            function processDiv() {
383                if (currentIndex < divs.length) {
384                    const currentDiv = $(divs[currentIndex]);
385                    currentDiv.animate({ bottom: '40px' }, 500, function() {
386                        // After sliding up, delay 3 seconds, then slide down
387                        setTimeout(function() {
388                            currentDiv.animate({ bottom: '-120px' }, 300, function() {
389                                // After sliding down, proceed to the next div
390                                currentIndex++;
391
392                                // Check if we've completed a full cycle
393                                if (currentIndex >= divs.length) {
394                                    currentIndex = 0;
395                                    completedCycles++;
396
397                                    // Check if we've reached the repeat limit
398                                    if (number_of_repeats > 0 && completedCycles >= number_of_repeats) {
399                                        // Stop animation - we've reached the limit
400                                        return;
401                                    }
402                                }
403
404                                if ( tp_charitable_animate === true ) {
405                                    setTimeout(function() {
406                                        processDiv();
407                                    }, 3000);
408                                }
409                            });
410                        }
411                        , 5000);
412                    });
413                }
414            }
415
416            // wait for the configured initial delay before starting the animation
417            setTimeout(function() {
418                processDiv();
419            }
420            , initial_delay * 1000);
421        }
422
423        // Remove any badges already dismissed (per donation id)
424        (function filterDismissedOnLoad() {
425            var dismissed = getDismissedDonations();
426            if (dismissed.length === 0) { return; }
427            $('div[id^="tp-charitable-holder-"]').each(function() {
428                var id = $(this).attr('data-campaign');
429                if (id && dismissed.indexOf(id) !== -1) {
430                    $(this).remove();
431                }
432            });
433        })();
434
435        // Legacy animation gate via cookie: default to true; do not disable on dismiss
436        if ( Cookies.get('tp_charitable_animate') === undefined ) {
437            Cookies.set('tp_charitable_animate', true, { expires: 7, path: '/' });
438            slideDivs();
439        } else {
440            if ( Cookies.get('tp_charitable_animate') === 'false' ) {
441                tp_charitable_animate = false;
442            } else {
443                tp_charitable_animate = true;
444                slideDivs();
445            }
446        }
447    });
448
449
450})( jQuery);
451
452/*! js-cookie v3.0.5 | MIT */
453!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self,function(){var n=e.Cookies,o=e.Cookies=t();o.noConflict=function(){return e.Cookies=n,o}}())}(this,(function(){"use strict";function e(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var o in n)e[o]=n[o]}return e}var t=function t(n,o){function r(t,r,i){if("undefined"!=typeof document){"number"==typeof(i=e({},o,i)).expires&&(i.expires=new Date(Date.now()+864e5*i.expires)),i.expires&&(i.expires=i.expires.toUTCString()),t=encodeURIComponent(t).replace(/%(2[346B]|5E|60|7C)/g,decodeURIComponent).replace(/[()]/g,escape);var c="";for(var u in i)i[u]&&(c+="; "+u,!0!==i[u]&&(c+="="+i[u].split(";")[0]));return document.cookie=t+"="+n.write(r,t)+c}}return Object.create({set:r,get:function(e){if("undefined"!=typeof document&&(!arguments.length||e)){for(var t=document.cookie?document.cookie.split("; "):[],o={},r=0;r<t.length;r++){var i=t[r].split("="),c=i.slice(1).join("=");try{var u=decodeURIComponent(i[0]);if(o[u]=n.read(c,u),e===u)break}catch(e){}}return e?o[e]:o}},remove:function(t,n){r(t,"",e({},n,{expires:-1}))},withAttributes:function(n){return t(this.converter,e({},this.attributes,n))},withConverter:function(n){return t(e({},this.converter,n),this.attributes)}},{attributes:{value:Object.freeze(o)},converter:{value:Object.freeze(n)}})}({read:function(e){return'"'===e[0]&&(e=e.slice(1,-1)),e.replace(/(%[\dA-F]{2})+/gi,decodeURIComponent)},write:function(e){return encodeURIComponent(e).replace(/%(2[346BF]|3[AC-F]|40|5[BDE]|60|7[BCD])/g,decodeURIComponent)}},{path:"/"});return t}));
454;
455/* global wpcom, jetpackCarouselStrings, DocumentTouch */
456/* eslint-disable no-shadow */
457
458( function () {
459	'use strict';
460	var swiper;
461	/////////////////////////////////////
462	// Utility functions
463	/////////////////////////////////////
464	var util = ( function () {
465		var noop = function () {};
466
467		function texturize( text ) {
468			// Ensure we get a string.
469			text = text + '';
470			text = text.replace( /'/g, '&#8217;' ).replace( /&#039;/g, '&#8217;' );
471			text = text
472				.replace( /"/g, '&#8221;' )
473				.replace( /&#034;/g, '&#8221;' )
474				.replace( /&quot;/g, '&#8221;' )
475				.replace( /[\u201D]/g, '&#8221;' );
476			// Untexturize allowed HTML tags params double-quotes.
477			text = text.replace( /([\w]+)=&#[\d]+;(.+?)&#[\d]+;/g, '$1="$2"' );
478			return text.trim();
479		}
480
481		function applyReplacements( text, replacements ) {
482			if ( ! text ) {
483				return;
484			}
485			if ( ! replacements ) {
486				return text;
487			}
488			return text.replace( /{(\d+)}/g, function ( match, number ) {
489				return typeof replacements[ number ] !== 'undefined' ? replacements[ number ] : match;
490			} );
491		}
492
493		function getBackgroundImage( imgEl ) {
494			var canvas = document.createElement( 'canvas' ),
495				context = canvas.getContext && canvas.getContext( '2d' );
496
497			if ( ! imgEl ) {
498				return;
499			}
500
501			context.filter = 'blur(20px) ';
502			context.drawImage( imgEl, 0, 0 );
503			var url = canvas.toDataURL( 'image/png' );
504			canvas = null; // eslint-disable-line no-useless-assignment -- Verify this isn't needed to free memory or something.
505
506			return url;
507		}
508
509		return {
510			noop: noop,
511			texturize: texturize,
512			applyReplacements: applyReplacements,
513			getBackgroundImage: getBackgroundImage,
514		};
515	} )();
516
517	/////////////////////////////////////
518	// DOM-related utility functions
519	/////////////////////////////////////
520	var domUtil = ( function () {
521		// Helper matches function (not a polyfill), compatible with IE 11.
522		function matches( el, sel ) {
523			if ( Element.prototype.matches ) {
524				return el.matches( sel );
525			}
526
527			if ( Element.prototype.msMatchesSelector ) {
528				return el.msMatchesSelector( sel );
529			}
530		}
531
532		// Helper closest parent node function (not a polyfill) based on
533		// https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill
534		function closest( el, sel ) {
535			if ( el.closest ) {
536				return el.closest( sel );
537			}
538
539			var current = el;
540
541			do {
542				if ( matches( current, sel ) ) {
543					return current;
544				}
545				current = current.parentElement || current.parentNode;
546			} while ( current !== null && current.nodeType === 1 );
547
548			return null;
549		}
550
551		function hide( el ) {
552			if ( el ) {
553				el.style.display = 'none';
554			}
555		}
556
557		function show( el ) {
558			if ( el ) {
559				// Everything we show and hide in Carousel is currently a block,
560				// so we can make this really straightforward.
561				el.style.display = 'block';
562			}
563		}
564
565		/**
566		 * CSS-transition fade (compositor). Duration + reduced-motion live in the
567		 * `.jp-carousel-fade` rule. A timer drives the finish, not `transitionend` -- that
568		 * event is skipped in background tabs / `transition: none` / zero duration, so the
569		 * timer is the reliable single source. `callback` fires once, after the fade.
570		 */
571		function fade( el, start, end, callback ) {
572			if ( ! el ) {
573				return callback();
574			}
575
576			// A fade already running on this element must not deliver its callback any more.
577			if ( el.jpCarouselCancelFade ) {
578				el.jpCarouselCancelFade();
579			}
580
581			// Set + commit the start state before attaching the transition, or the fade is swallowed.
582			el.classList.remove( 'jp-carousel-fade' );
583			el.style.removeProperty( 'display' );
584			el.style.opacity = start;
585			el.style.pointerEvents = 'none';
586
587			// Commit the starting opacity, otherwise the browser has nothing to animate from.
588			void el.offsetWidth;
589
590			el.classList.add( 'jp-carousel-fade' );
591			el.style.opacity = end;
592
593			// Read the duration back from the stylesheet so the timer always outlives the transition.
594			var duration = parseFloat( getComputedStyle( el ).transitionDuration ) * 1000 || 0;
595
596			var timer = setTimeout( function () {
597				el.jpCarouselCancelFade = null;
598				el.style.removeProperty( 'pointer-events' );
599				callback();
600			}, duration + 50 );
601
602			el.jpCarouselCancelFade = function () {
603				clearTimeout( timer );
604				el.jpCarouselCancelFade = null;
605			};
606		}
607
608		function fadeIn( el, callback ) {
609			callback = callback || util.noop;
610			fade( el, 0, 1, callback );
611		}
612
613		function fadeOut( el, callback ) {
614			callback = callback || util.noop;
615			fade( el, 1, 0, function () {
616				if ( el ) {
617					el.style.display = 'none';
618				}
619				callback();
620			} );
621		}
622
623		function emitEvent( el, type, detail ) {
624			var e;
625			try {
626				e = new CustomEvent( type, {
627					bubbles: true,
628					cancelable: true,
629					detail: detail || null,
630				} );
631			} catch {
632				e = document.createEvent( 'CustomEvent' );
633				e.initCustomEvent( type, true, true, detail || null );
634			}
635			el.dispatchEvent( e );
636		}
637
638		// From: https://easings.net/#easeInOutQuad
639		function easeInOutQuad( num ) {
640			return num < 0.5 ? 2 * num * num : 1 - Math.pow( -2 * num + 2, 2 ) / 2;
641		}
642
643		function getFooterClearance( container ) {
644			var footer = container.querySelector( '.jp-carousel-info-footer' );
645			var infoArea = container.querySelector( '.jp-carousel-info-extra' );
646			var contentArea = container.querySelector( '.jp-carousel-info-content-wrapper' );
647
648			if ( footer && infoArea && contentArea ) {
649				var styles = window.getComputedStyle( infoArea );
650				var padding = parseInt( styles.paddingTop, 10 ) + parseInt( styles.paddingBottom, 10 );
651				padding = isNaN( padding ) ? 0 : padding;
652				return contentArea.offsetHeight + footer.offsetHeight + padding;
653			}
654			return 0;
655		}
656
657		function isTouch() {
658			return (
659				'ontouchstart' in window || ( window.DocumentTouch && document instanceof DocumentTouch )
660			);
661		}
662
663		function prefersReducedMotion() {
664			return (
665				!! window.matchMedia && window.matchMedia( '(prefers-reduced-motion: reduce)' ).matches
666			);
667		}
668
669		function scrollToElement( el, container, callback ) {
670			if ( ! el || ! container ) {
671				if ( callback ) {
672					return callback();
673				}
674				return;
675			}
676
677			// For iOS Safari compatibility, use JS to set the minimum height.
678			var infoArea = container.querySelector( '.jp-carousel-info-extra' );
679			if ( infoArea ) {
680				// 64px is the same height as `.jp-carousel-info-footer` in the CSS.
681				infoArea.style.minHeight = window.innerHeight - 64 + 'px';
682			}
683
684			var isScrolling = true;
685			var startTime = Date.now();
686			var duration = 300;
687			var originalPosition = container.scrollTop;
688			var targetPosition = Math.max(
689				0,
690				el.offsetTop - Math.max( 0, window.innerHeight - getFooterClearance( container ) )
691			);
692			var distance = targetPosition - container.scrollTop;
693			distance = Math.min( distance, container.scrollHeight - window.innerHeight );
694
695			function stopScroll() {
696				isScrolling = false;
697			}
698
699			function runScroll() {
700				var now = Date.now();
701				var progress = easeInOutQuad( ( now - startTime ) / duration );
702
703				progress = progress > 1 ? 1 : progress;
704				var newVal = progress * distance;
705				container.scrollTop = originalPosition + newVal;
706
707				if ( now <= startTime + duration && isScrolling ) {
708					return requestAnimationFrame( runScroll );
709				}
710				if ( callback ) {
711					callback();
712				}
713				if ( infoArea ) {
714					infoArea.style.minHeight = '';
715				}
716				isScrolling = false;
717				container.removeEventListener( 'wheel', stopScroll );
718			}
719
720			// Allow scroll to be cancelled by user interaction.
721			container.addEventListener( 'wheel', stopScroll );
722			runScroll();
723		}
724
725		function getJSONAttribute( el, attr ) {
726			if ( ! el || ! el.hasAttribute( attr ) ) {
727				return undefined;
728			}
729
730			try {
731				return JSON.parse( el.getAttribute( attr ) );
732			} catch {
733				return undefined;
734			}
735		}
736
737		function convertToPlainText( html ) {
738			var dummy = document.createElement( 'div' );
739			dummy.textContent = html;
740			return dummy.innerHTML;
741		}
742
743		function stripHTML( text ) {
744			var tmp = document.createElement( 'div' );
745			tmp.innerHTML = text.replace( /<[^>]*>?/gm, '' );
746			return tmp.textContent;
747		}
748
749		return {
750			closest: closest,
751			matches: matches,
752			hide: hide,
753			show: show,
754			fadeIn: fadeIn,
755			fadeOut: fadeOut,
756			scrollToElement: scrollToElement,
757			getJSONAttribute: getJSONAttribute,
758			convertToPlainText: convertToPlainText,
759			stripHTML: stripHTML,
760			emitEvent: emitEvent,
761			isTouch: isTouch,
762			prefersReducedMotion: prefersReducedMotion,
763		};
764	} )();
765
766	/////////////////////////////////////
767	// Carousel implementation
768	/////////////////////////////////////
769	function init() {
770		var commentInterval;
771		// Rendered comments per attachment, kept for the lifetime of one overlay session.
772		var commentsCache = {};
773		// Attachment IDs with a comments request in flight, to prevent overlapping fetches.
774		var commentsFetching = {};
775		// Comments the server returns per page; mirrors the `number` arg in get_attachment_comments().
776		var COMMENTS_PER_PAGE = 10;
777		var screenPadding;
778		var originalOverflow;
779		var originalHOverflow;
780		var scrollPos;
781
782		var lastKnownLocationHash = '';
783		var isUserTyping = false;
784
785		var gallerySelector =
786			'div.gallery, div.tiled-gallery, ul.wp-block-gallery, ul.blocks-gallery-grid, ' +
787			'figure.wp-block-gallery.has-nested-images, div.wp-block-jetpack-tiled-gallery, a.single-image-gallery';
788
789		// Selector for items within a gallery or tiled gallery.
790		var galleryItemSelector =
791			'.gallery-item, .tiled-gallery-item, .blocks-gallery-item, ' + ' .tiled-gallery__item';
792
793		// Selector for all items including single images.
794		var itemSelector = galleryItemSelector + ', .wp-block-image';
795
796		var carousel = {};
797
798		var stat =
799			typeof wpcom !== 'undefined' && wpcom.carousel && wpcom.carousel.stat
800				? wpcom.carousel.stat
801				: util.noop;
802
803		var pageview =
804			typeof wpcom !== 'undefined' && wpcom.carousel && wpcom.carousel.pageview
805				? wpcom.carousel.pageview
806				: util.noop;
807
808		function handleKeyboardEvent( e ) {
809			if ( ! isUserTyping ) {
810				switch ( e.which ) {
811					case 38: // up
812						e.preventDefault();
813						carousel.overlay.scrollTop -= 100;
814						break;
815					case 40: // down
816						e.preventDefault();
817						carousel.overlay.scrollTop += 100;
818						break;
819					case 39: // right
820						e.preventDefault();
821						swiper.slideNext();
822						break;
823					case 37: // left
824					case 8: // backspace
825						e.preventDefault();
826						swiper.slidePrev();
827						break;
828					case 27: // escape
829						e.preventDefault();
830						closeCarousel();
831						break;
832					default:
833						break;
834				}
835			}
836		}
837
838		function disableKeyboardNavigation() {
839			isUserTyping = true;
840		}
841
842		function enableKeyboardNavigation() {
843			isUserTyping = false;
844		}
845
846		function calculatePadding() {
847			var baseScreenPadding = 110;
848			screenPadding = baseScreenPadding;
849
850			if ( window.innerWidth <= 760 ) {
851				screenPadding = Math.round( ( window.innerWidth / 760 ) * baseScreenPadding );
852
853				if ( screenPadding < 40 && domUtil.isTouch() ) {
854					screenPadding = 0;
855				}
856			}
857		}
858
859		function makeGalleryImageAccessible( img ) {
860			img.role = 'button';
861			img.tabIndex = 0;
862			img.ariaLabel = jetpackCarouselStrings.image_label;
863		}
864
865		function initializeCarousel() {
866			if ( ! carousel.overlay ) {
867				carousel.overlay = document.querySelector( '.jp-carousel-overlay' );
868				carousel.container = carousel.overlay.querySelector( '.jp-carousel-wrap' );
869				carousel.gallery = carousel.container.querySelector( '.jp-carousel' );
870				carousel.info = carousel.overlay.querySelector( '.jp-carousel-info' );
871				carousel.caption = carousel.info.querySelector( '.jp-carousel-caption' );
872				carousel.commentField = carousel.overlay.querySelector(
873					'#jp-carousel-comment-form-comment-field'
874				);
875				carousel.emailField = carousel.overlay.querySelector(
876					'#jp-carousel-comment-form-email-field'
877				);
878				carousel.authorField = carousel.overlay.querySelector(
879					'#jp-carousel-comment-form-author-field'
880				);
881				carousel.urlField = carousel.overlay.querySelector( '#jp-carousel-comment-form-url-field' );
882
883				calculatePadding();
884
885				[
886					carousel.commentField,
887					carousel.emailField,
888					carousel.authorField,
889					carousel.urlField,
890				].forEach( function ( field ) {
891					if ( field ) {
892						field.addEventListener( 'focus', disableKeyboardNavigation );
893						field.addEventListener( 'blur', enableKeyboardNavigation );
894					}
895				} );
896
897				carousel.overlay.addEventListener( 'click', function ( e ) {
898					var target = e.target;
899					var isTargetCloseHint = !! domUtil.closest( target, '.jp-carousel-close-hint' );
900					var isSmallScreen = !! window.matchMedia( '(max-device-width: 760px)' ).matches;
901					if ( target === carousel.overlay ) {
902						if ( ! isSmallScreen ) {
903							closeCarousel();
904						}
905					} else if ( isTargetCloseHint ) {
906						closeCarousel();
907					} else if ( target.classList.contains( 'jp-carousel-image-download' ) ) {
908						stat( 'download_original_click' );
909					} else if ( target.classList.contains( 'jp-carousel-comment-login' ) ) {
910						handleCommentLoginClick( e );
911					} else if ( domUtil.closest( target, '#jp-carousel-comment-form-container' ) ) {
912						handleCommentFormClick( e );
913					} else if (
914						domUtil.closest( target, '.jp-carousel-photo-icons-container' ) ||
915						target.classList.contains( 'jp-carousel-photo-title' )
916					) {
917						handleFooterElementClick( e );
918					}
919				} );
920
921				window.addEventListener( 'keydown', handleKeyboardEvent );
922
923				carousel.overlay.addEventListener( 'jp_carousel.afterOpen', function () {
924					enableKeyboardNavigation();
925
926					// Don't show navigation if there's only one image.
927					if ( carousel.slides.length <= 1 ) {
928						return;
929					}
930					// Show dot pagination if slide count is <= 5, otherwise show n/total.
931					if ( carousel.slides.length <= 5 ) {
932						domUtil.show( carousel.info.querySelector( '.jp-swiper-pagination' ) );
933					} else {
934						domUtil.show( carousel.info.querySelector( '.jp-carousel-pagination' ) );
935					}
936				} );
937
938				carousel.overlay.addEventListener( 'jp_carousel.beforeClose', function () {
939					disableKeyboardNavigation();
940
941					// Fixes some themes where closing carousel brings view back to top.
942					document.documentElement.style.removeProperty( 'height' );
943
944					// If we disable the swiper (because there's only one image)
945					// we have to re-enable it here again as Swiper doesn't, for some reason,
946					// show the navigation buttons again after reinitialization.
947					if ( swiper ) {
948						swiper.enable();
949					}
950
951					// Hide pagination.
952					domUtil.hide( carousel.info.querySelector( '.jp-swiper-pagination' ) );
953					domUtil.hide( carousel.info.querySelector( '.jp-carousel-pagination' ) );
954				} );
955
956				carousel.overlay.addEventListener( 'jp_carousel.afterClose', function () {
957					// don't force the browser back when the carousel closes.
958					if ( window.history.pushState ) {
959						history.pushState(
960							'',
961							document.title,
962							window.location.pathname + window.location.search
963						);
964					} else {
965						window.location.href = '';
966					}
967					lastKnownLocationHash = '';
968					carousel.isOpen = false;
969				} );
970
971				// Prevent native browser zooming
972				carousel.overlay.addEventListener( 'touchstart', function ( e ) {
973					if ( e.touches.length > 1 ) {
974						e.preventDefault();
975					}
976				} );
977			}
978		}
979
980		function handleCommentLoginClick() {
981			var slide = carousel.currentSlide;
982			var attachmentId = slide ? slide.attrs.attachmentId : '0';
983
984			window.location.href = jetpackCarouselStrings.login_url + '%23jp-carousel-' + attachmentId;
985		}
986
987		function updatePostResults( msg, isSuccess ) {
988			var results = carousel.overlay.querySelector( '#jp-carousel-comment-post-results' );
989			var elClass = 'jp-carousel-comment-post-' + ( isSuccess ? 'success' : 'error' );
990			results.innerHTML = '<span class="' + elClass + '">' + msg + '</span>';
991			domUtil.hide( carousel.overlay.querySelector( '#jp-carousel-comment-form-spinner' ) );
992			carousel.overlay
993				.querySelector( '#jp-carousel-comment-form' )
994				.classList.remove( 'jp-carousel-is-disabled' );
995			domUtil.show( results );
996		}
997
998		function handleCommentFormClick( e ) {
999			var target = e.target;
1000			var data = domUtil.getJSONAttribute( carousel.container, 'data-carousel-extra' ) || {};
1001			var attachmentId = carousel.currentSlide.attrs.attachmentId;
1002
1003			var wrapper = document.querySelector( '#jp-carousel-comment-form-submit-and-info-wrapper' );
1004			var spinner = document.querySelector( '#jp-carousel-comment-form-spinner' );
1005			// eslint-disable-next-line @wordpress/no-unused-vars-before-return
1006			var submit = document.querySelector( '#jp-carousel-comment-form-button-submit' );
1007			var form = document.querySelector( '#jp-carousel-comment-form' );
1008
1009			if (
1010				carousel.commentField &&
1011				carousel.commentField.getAttribute( 'id' ) === target.getAttribute( 'id' )
1012			) {
1013				// For first page load
1014				disableKeyboardNavigation();
1015				domUtil.show( wrapper );
1016			} else if ( domUtil.matches( target, 'input[type="submit"]' ) ) {
1017				e.preventDefault();
1018				e.stopPropagation();
1019
1020				domUtil.show( spinner );
1021				form.classList.add( 'jp-carousel-is-disabled' );
1022
1023				var ajaxData = {
1024					action: 'post_attachment_comment',
1025					nonce: jetpackCarouselStrings.nonce,
1026					blog_id: data.blog_id,
1027					id: attachmentId,
1028					comment: carousel.commentField.value,
1029				};
1030
1031				if ( ! ajaxData.comment.length ) {
1032					updatePostResults( jetpackCarouselStrings.no_comment_text, false );
1033					return;
1034				}
1035
1036				if ( Number( jetpackCarouselStrings.is_logged_in ) !== 1 ) {
1037					ajaxData.email = carousel.emailField.value;
1038					ajaxData.author = carousel.authorField.value;
1039					ajaxData.url = carousel.urlField.value;
1040
1041					if ( Number( jetpackCarouselStrings.require_name_email ) === 1 ) {
1042						if ( ! ajaxData.email.length || ! ajaxData.email.match( '@' ) ) {
1043							updatePostResults( jetpackCarouselStrings.no_comment_email, false );
1044							return;
1045						} else if ( ! ajaxData.author.length ) {
1046							updatePostResults( jetpackCarouselStrings.no_comment_author, false );
1047							return;
1048						}
1049					}
1050				}
1051
1052				var xhr = new XMLHttpRequest();
1053				xhr.open( 'POST', jetpackCarouselStrings.ajaxurl, true );
1054				xhr.setRequestHeader( 'X-Requested-With', 'XMLHttpRequest' );
1055				xhr.setRequestHeader( 'Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8' );
1056
1057				xhr.onreadystatechange = function () {
1058					if (
1059						this.readyState === XMLHttpRequest.DONE &&
1060						this.status >= 200 &&
1061						this.status < 300
1062					) {
1063						var response;
1064						try {
1065							response = JSON.parse( this.response );
1066						} catch {
1067							updatePostResults( jetpackCarouselStrings.comment_post_error, false );
1068							return;
1069						}
1070						if ( response.comment_status === 'approved' ) {
1071							updatePostResults( jetpackCarouselStrings.comment_approved, true );
1072							/*
1073							Reflect the new comment in the badge total straight away. The refetch
1074							below only returns the first page, so on attachments with a full page
1075							of comments it cannot see the increment on its own. Bump the slide the
1076							comment was posted to -- captured as `attachmentId` -- not whichever
1077							slide happens to be current when this async response lands.
1078							*/
1079							var postedSlide = getSlideByAttachmentId( attachmentId );
1080							if ( postedSlide ) {
1081								postedSlide.attrs.commentsCount =
1082									( parseInt( postedSlide.attrs.commentsCount, 10 ) || 0 ) + 1;
1083							}
1084						} else if ( response.comment_status === 'unapproved' ) {
1085							updatePostResults( jetpackCarouselStrings.comment_unapproved, true );
1086						} else {
1087							// 'deleted', 'spam', false
1088							updatePostResults( jetpackCarouselStrings.comment_post_error, false );
1089						}
1090						clearCommentTextAreaValue();
1091						// The new comment invalidates whatever we cached for this attachment.
1092						delete commentsCache[ attachmentId ];
1093						// Force past the in-flight guard so a still-pending first page can't
1094						// suppress this refresh.
1095						fetchComments( attachmentId, undefined, true );
1096						submit.value = jetpackCarouselStrings.post_comment;
1097						domUtil.hide( spinner );
1098						form.classList.remove( 'jp-carousel-is-disabled' );
1099					} else {
1100						// TODO: Add error handling and display here
1101						updatePostResults( jetpackCarouselStrings.comment_post_error, false );
1102					}
1103				};
1104
1105				var params = [];
1106				for ( var item in ajaxData ) {
1107					if ( item ) {
1108						// Encode each form element into a URI-compatible string.
1109						var encoded = encodeURIComponent( item ) + '=' + encodeURIComponent( ajaxData[ item ] );
1110						// In x-www-form-urlencoded, spaces should be `+`, not `%20`.
1111						params.push( encoded.replace( /%20/g, '+' ) );
1112					}
1113				}
1114				var encodedData = params.join( '&' );
1115
1116				xhr.send( encodedData );
1117			}
1118		}
1119
1120		/**
1121		 * Handles clicks to icons and other action elements in the icon container.
1122		 * @param {MouseEvent|TouchEvent|KeyBoardEvent} Event object.
1123		 */
1124		function handleFooterElementClick( e ) {
1125			e.preventDefault();
1126
1127			var target = e.target;
1128			var extraInfoContainer = carousel.info.querySelector( '.jp-carousel-info-extra' );
1129			var photoMetaContainer = carousel.info.querySelector( '.jp-carousel-image-meta' );
1130			var commentsContainer = carousel.info.querySelector( '.jp-carousel-comments-wrapper' );
1131			var infoIcon = carousel.info.querySelector( '.jp-carousel-icon-info' );
1132			var commentsIcon = carousel.info.querySelector( '.jp-carousel-icon-comments' );
1133
1134			function handleInfoToggle() {
1135				if ( commentsIcon ) {
1136					commentsIcon.classList.remove( 'jp-carousel-selected' );
1137				}
1138				infoIcon.classList.toggle( 'jp-carousel-selected' );
1139
1140				if ( commentsContainer ) {
1141					commentsContainer.classList.remove( 'jp-carousel-show' );
1142				}
1143				if ( photoMetaContainer ) {
1144					photoMetaContainer.classList.toggle( 'jp-carousel-show' );
1145					if ( photoMetaContainer.classList.contains( 'jp-carousel-show' ) ) {
1146						extraInfoContainer.classList.add( 'jp-carousel-show' );
1147					} else {
1148						extraInfoContainer.classList.remove( 'jp-carousel-show' );
1149					}
1150				}
1151			}
1152
1153			function handleCommentToggle() {
1154				if ( infoIcon ) {
1155					infoIcon.classList.remove( 'jp-carousel-selected' );
1156				}
1157				commentsIcon.classList.toggle( 'jp-carousel-selected' );
1158
1159				if ( photoMetaContainer ) {
1160					photoMetaContainer.classList.remove( 'jp-carousel-show' );
1161				}
1162				if ( commentsContainer ) {
1163					commentsContainer.classList.toggle( 'jp-carousel-show' );
1164					if ( commentsContainer.classList.contains( 'jp-carousel-show' ) ) {
1165						extraInfoContainer.classList.add( 'jp-carousel-show' );
1166						// The panel is only now on screen, so this is when the comments are worth fetching.
1167						var current = carousel.currentSlide;
1168						if ( current ) {
1169							var currentCache = commentsCache[ current.attrs.attachmentId ];
1170							if ( ! currentCache ) {
1171								fetchComments( current.attrs.attachmentId );
1172							} else if ( currentCache.hasMore ) {
1173								// Cached page is already shown; re-arm paging for the rest.
1174								scheduleNextCommentsPage( current.attrs.attachmentId, currentCache.nextOffset );
1175							}
1176						}
1177					} else {
1178						extraInfoContainer.classList.remove( 'jp-carousel-show' );
1179					}
1180				}
1181			}
1182
1183			if (
1184				domUtil.closest( target, '.jp-carousel-icon-info' ) ||
1185				target.classList.contains( 'jp-carousel-photo-title' )
1186			) {
1187				if ( photoMetaContainer && photoMetaContainer.classList.contains( 'jp-carousel-show' ) ) {
1188					domUtil.scrollToElement( carousel.overlay, carousel.overlay, handleInfoToggle );
1189				} else {
1190					handleInfoToggle();
1191					domUtil.scrollToElement( carousel.info, carousel.overlay );
1192				}
1193			}
1194
1195			if ( domUtil.closest( target, '.jp-carousel-icon-comments' ) ) {
1196				if ( commentsContainer && commentsContainer.classList.contains( 'jp-carousel-show' ) ) {
1197					domUtil.scrollToElement( carousel.overlay, carousel.overlay, handleCommentToggle );
1198				} else {
1199					handleCommentToggle();
1200					domUtil.scrollToElement( carousel.info, carousel.overlay );
1201				}
1202			}
1203		}
1204
1205		function processSingleImageGallery() {
1206			var images = document.querySelectorAll( 'a img[data-attachment-id]' );
1207			Array.prototype.forEach.call( images, function ( image ) {
1208				var link = image.parentElement;
1209				var container = link.parentElement;
1210
1211				// Skip if image was already added to gallery by shortcode.
1212				if ( container.classList.contains( 'gallery-icon' ) ) {
1213					return;
1214				}
1215
1216				// Skip if image is part of a gallery.
1217				if ( domUtil.closest( container, galleryItemSelector ) ) {
1218					return;
1219				}
1220
1221				// Skip if the parent is not actually a link.
1222				if ( ! link.hasAttribute( 'href' ) ) {
1223					return;
1224				}
1225
1226				var valid = false;
1227
1228				// If link points to 'Media File' (ignoring GET parameters) and flag is set, allow it.
1229				if (
1230					link.getAttribute( 'href' ).split( '?' )[ 0 ] ===
1231						image.getAttribute( 'data-orig-file' ).split( '?' )[ 0 ] &&
1232					Number( jetpackCarouselStrings.single_image_gallery_media_file ) === 1
1233				) {
1234					valid = true;
1235				}
1236
1237				// If link points to 'Attachment Page', allow it.
1238				if ( link.getAttribute( 'href' ) === image.getAttribute( 'data-permalink' ) ) {
1239					valid = true;
1240				}
1241
1242				// Links to 'Custom URL' or 'Media File' when flag is not set are not valid.
1243				if ( ! valid ) {
1244					return;
1245				}
1246
1247				makeGalleryImageAccessible( image );
1248
1249				// Make this node a gallery recognizable by event listener above.
1250				link.classList.add( 'single-image-gallery' );
1251				// blog_id is needed to allow posting comments to correct blog.
1252				link.setAttribute(
1253					'data-carousel-extra',
1254					JSON.stringify( {
1255						blog_id: Number( jetpackCarouselStrings.blog_id ),
1256					} )
1257				);
1258			} );
1259		}
1260
1261		function testForData( el ) {
1262			return !! ( el && el.getAttribute( 'data-carousel-extra' ) );
1263		}
1264
1265		function openOrSelectSlide( gal, index ) {
1266			if ( ! carousel.isOpen ) {
1267				// The `open` method selects the correct slide during the initialization.
1268				loadSwiper( gal, { startIndex: index } );
1269			} else {
1270				selectSlideAtIndex( index );
1271				// We have to force swiper to slide to the index onHasChange.
1272				swiper.slideTo( index + 1 );
1273			}
1274		}
1275
1276		function getSlideByAttachmentId( attachmentId ) {
1277			for ( var i = 0; i < carousel.slides.length; i++ ) {
1278				if ( carousel.slides[ i ].attrs.attachmentId === attachmentId ) {
1279					return carousel.slides[ i ];
1280				}
1281			}
1282			return null;
1283		}
1284
1285		function selectSlideAtIndex( index ) {
1286			if ( ! index || index < 0 || index > carousel.slides.length ) {
1287				index = 0;
1288			}
1289			carousel.currentSlide = carousel.slides[ index ];
1290
1291			var current = carousel.currentSlide;
1292			var attachmentId = current.attrs.attachmentId;
1293
1294			// Load current image immediately
1295			loadFullImage( carousel.slides[ index ] );
1296
1297			// Preload adjacent images in background
1298			preloadAdjacentImages( index );
1299
1300			if (
1301				Number( jetpackCarouselStrings.display_background_image ) === 1 &&
1302				! carousel.slides[ index ].backgroundImage
1303			) {
1304				loadBackgroundImage( carousel.slides[ index ] );
1305			}
1306
1307			domUtil.hide( carousel.caption );
1308			updateTitleCaptionAndDesc( {
1309				caption: current.attrs.caption,
1310				title: current.attrs.title,
1311				desc: current.attrs.desc,
1312			} );
1313
1314			var imageMeta = carousel.slides[ index ].attrs.imageMeta;
1315			updateExif( imageMeta );
1316			updateFullSizeLink( current );
1317
1318			if ( Number( jetpackCarouselStrings.display_comments ) === 1 ) {
1319				testCommentsOpened( carousel.slides[ index ].attrs.commentsOpened );
1320				showCommentsForSlide( current );
1321				domUtil.hide( carousel.info.querySelector( '#jp-carousel-comment-post-results' ) );
1322			}
1323
1324			// Update pagination in footer.
1325			var pagination = carousel.info.querySelector( '.jp-carousel-pagination' );
1326			if ( pagination && carousel.slides.length > 5 ) {
1327				var currentPage = index + 1;
1328				pagination.innerHTML = '<span>' + currentPage + ' / ' + carousel.slides.length + '</span>';
1329			}
1330
1331			// Record pageview in WP Stats, for each new image loaded full-screen.
1332			if ( jetpackCarouselStrings.stats && carousel.isOpen ) {
1333				new Image().src =
1334					document.location.protocol +
1335					'//pixel.wp.com/g.gif?' +
1336					jetpackCarouselStrings.stats +
1337					'&post=' +
1338					encodeURIComponent( attachmentId ) +
1339					'&rand=' +
1340					Math.random();
1341			}
1342
1343			if ( carousel.isOpen ) {
1344				pageview( attachmentId );
1345			}
1346
1347			lastKnownLocationHash = '#jp-carousel-' + attachmentId;
1348			window.location.hash = lastKnownLocationHash;
1349		}
1350
1351		function restoreScroll() {
1352			window.scrollTo( window.scrollX || window.pageXOffset || 0, scrollPos || 0 );
1353		}
1354
1355		function closeCarousel() {
1356			// Make sure to let the page scroll again.
1357			document.body.style.overflow = originalOverflow;
1358			document.documentElement.style.overflow = originalHOverflow;
1359			clearCommentTextAreaValue();
1360
1361			disableKeyboardNavigation();
1362
1363			domUtil.emitEvent( carousel.overlay, 'jp_carousel.beforeClose' );
1364			restoreScroll();
1365			carousel.isOpen = false;
1366			swiper.destroy();
1367			// Clear slide data for DOM garbage collection.
1368			carousel.slides = [];
1369			commentsCache = {};
1370			commentsFetching = {};
1371			carousel.currentSlide = undefined;
1372			carousel.gallery.innerHTML = '';
1373
1374			domUtil.fadeOut( carousel.overlay, function () {
1375				domUtil.emitEvent( carousel.overlay, 'jp_carousel.afterClose' );
1376			} );
1377		}
1378
1379		function calculateMaxSlideDimensions() {
1380			return {
1381				width: window.innerWidth,
1382				height: window.innerHeight - 64, //subtract height of bottom info bar,
1383			};
1384		}
1385
1386		function sanitizePhotonUrl( url ) {
1387			var urlObj;
1388			try {
1389				urlObj = new URL( url );
1390				// eslint-disable-next-line no-unused-vars
1391			} catch ( e ) {
1392				return url;
1393			}
1394
1395			var whitelistedPhotonArgs = [
1396				'quality',
1397				'ssl',
1398				'filter',
1399				'brightness',
1400				'contrast',
1401				'colorize',
1402				'smooth',
1403			];
1404
1405			// Get all search params
1406			var searchParams = Array.from( urlObj.searchParams.entries() );
1407
1408			// Clear all existing params
1409			urlObj.search = '';
1410
1411			// Only add back whitelisted params
1412			searchParams.forEach( ( [ key, value ] ) => {
1413				if ( whitelistedPhotonArgs.includes( key ) ) {
1414					urlObj.searchParams.append( key, value );
1415				}
1416			} );
1417
1418			return urlObj;
1419		}
1420
1421		function selectBestImageUrl( args ) {
1422			if ( typeof args !== 'object' ) {
1423				args = {};
1424			}
1425
1426			if ( typeof args.origFile === 'undefined' ) {
1427				return '';
1428			}
1429
1430			if ( typeof args.origWidth === 'undefined' || typeof args.maxWidth === 'undefined' ) {
1431				return args.origFile;
1432			}
1433
1434			// When there's no large file to fall back on (e.g. images that weren't enriched with
1435			// Jetpack's data-large-file attribute), use the original file. A missing attribute is
1436			// read as an empty string, so we can't only guard against `undefined` here: otherwise a
1437			// narrow (portrait, mobile) viewport would return that empty string as the image source,
1438			// leaving the carousel with a blank slide.
1439			if ( ! args.largeFile ) {
1440				return args.origFile;
1441			}
1442
1443			// Check if the image is being served by Photon (using a regular expression on the hostname).
1444
1445			var imageLinkParser = document.createElement( 'a' );
1446			imageLinkParser.href = args.largeFile;
1447
1448			var isPhotonUrl = /^i[0-2]\.wp\.com$/i.test( imageLinkParser.hostname );
1449
1450			var largeSizeParts = getImageSizeParts( args.largeFile, args.origWidth, isPhotonUrl );
1451
1452			var largeWidth = parseInt( largeSizeParts[ 0 ], 10 );
1453			var largeHeight = parseInt( largeSizeParts[ 1 ], 10 );
1454
1455			args.origMaxWidth = args.maxWidth;
1456			args.origMaxHeight = args.maxHeight;
1457
1458			// Give devices with a higher devicePixelRatio higher-res images (Retina display = 2, Android ph
1458ones = 1.5, etc)
1459			if ( typeof window.devicePixelRatio !== 'undefined' && window.devicePixelRatio > 1 ) {
1460				args.maxWidth = args.maxWidth * window.devicePixelRatio;
1461				args.maxHeight = args.maxHeight * window.devicePixelRatio;
1462			}
1463
1464			/*
1465			`maxWidth`/`maxHeight` now describe the device resolution. Anything beyond that is
1466			headroom for zooming, so only the zoom request asks for it. The multiplier is folded
1467			in here -- ahead of the size checks below -- so the zoom rendition keeps its extra
1468			resolution even when the unzoomed one is already covered by `data-large-file`.
1469			Without it, every slide would fetch ~4x the pixels the screen can show.
1470			*/
1471			var headroom = args.zoomHeadroom || 1;
1472			if ( headroom > 1 ) {
1473				args.maxWidth = args.maxWidth * headroom;
1474				args.maxHeight = args.maxHeight * headroom;
1475			}
1476
1477			if ( largeWidth >= args.maxWidth || largeHeight >= args.maxHeight ) {
1478				return args.largeFile;
1479			}
1480
1481			if ( isPhotonUrl ) {
1482				// args.origFile doesn't point to a Photon url, so in this case we use args.largeFile
1483				// to return the photon url of the original image.
1484				if ( args.largeFile.lastIndexOf( '?' ) === -1 ) {
1485					return args.largeFile;
1486				}
1487
1488				// Sanitize the URL to remove non-cosmetic changes like resize, fit, etc.
1489				var sanitizedUrl = sanitizePhotonUrl( args.largeFile );
1490
1491				// If we have a really large image load a smaller version
1492				// that is closer to the viewable size
1493				if ( args.origWidth > args.maxWidth || args.origHeight > args.maxHeight ) {
1494					args.origMaxWidth = args.maxWidth;
1495					args.origMaxHeight = args.maxHeight;
1496					// Add the fit arg to the list of Photon args.
1497					sanitizedUrl.searchParams.set( 'fit', args.origMaxWidth + ',' + args.origMaxHeight );
1498				}
1499
1500				// Return a Photon URL image that's better fitted for the viewport.
1501				return sanitizedUrl.toString();
1502			}
1503
1504			return args.origFile;
1505		}
1506
1507		function getImageSizeParts( file, origWidth, isPhotonUrl ) {
1508			var size = isPhotonUrl
1509				? file.replace( /.*=([\d]+%2C[\d]+).*$/, '$1' )
1510				: file.replace( /.*-([\d]+x[\d]+)\..+$/, '$1' );
1511
1512			var sizeParts;
1513			if ( size !== file ) {
1514				sizeParts = isPhotonUrl ? size.split( '%2C' ) : size.split( 'x' );
1515			} else {
1516				sizeParts = [ origWidth, 0 ];
1517			}
1518
1519			// If one of the dimensions is set to 9999, then the actual value of that dimension can't be retrieved from the url.
1520			// In that case, we set the value to 0.
1521			if ( sizeParts[ 0 ] === '9999' ) {
1522				sizeParts[ 0 ] = '0';
1523			}
1524
1525			if ( sizeParts[ 1 ] === '9999' ) {
1526				sizeParts[ 1 ] = '0';
1527			}
1528
1529			return sizeParts;
1530		}
1531
1532		/**
1533		 * Returns a number in a fraction format that represents the shutter speed.
1534		 * @param Number speed
1535		 * @return String
1536		 */
1537		function formatShutterSpeed( speed ) {
1538			var denominator;
1539
1540			// round to one decimal if value > 1s by multiplying it by 10, rounding, then dividing by 10 again
1541			if ( speed >= 1 ) {
1542				return Math.round( speed * 10 ) / 10 + 's';
1543			}
1544
1545			// If the speed is less than one, we find the denominator by invert
1545ing
1546			// the number. Since cameras usually use rational numbers as shutter
1547			// speeds, we should get a nice round number. Or close to one in cases
1548			// like 1/30. So we round it.
1549			denominator = Math.round( 1 / speed );
1550
1551			return '1/' + denominator + 's';
1552		}
1553
1554		function parseTitleOrDesc( value ) {
1555			if ( ! value.match( ' ' ) && value.match( '_' ) ) {
1556				return '';
1557			}
1558
1559			return value;
1560		}
1561
1562		function updateTitleCaptionAndDesc( data ) {
1563			var caption;
1564			var title;
1565			var desc;
1566			var captionMainElement;
1567			var captionInfoExtraElement;
1568			var titleElement;
1569			var descriptionElement;
1570
1571			captionMainElement = carousel.overlay.querySelector( '.jp-carousel-photo-caption' );
1572			captionInfoExtraElement = carousel.overlay.querySelector( '.jp-carousel-caption' );
1573
1574			titleElement = carousel.overlay.querySelector( '.jp-carousel-photo-title' );
1575			descriptionElement = carousel.overlay.querySelector( '.jp-carousel-photo-description' );
1576
1577			domUtil.hide( captionMainElement );
1578			domUtil.hide( captionInfoExtraElement );
1579			domUtil.hide( titleElement );
1580			domUtil.hide( descriptionElement );
1581
1582			caption = parseTitleOrDesc( data.caption ) || '';
1583			title = parseTitleOrDesc( data.title ) || '';
1584			desc = parseTitleOrDesc( data.desc ) || '';
1585
1586			if ( caption || title || desc ) {
1587				if ( caption ) {
1588					captionMainElement.innerHTML = caption;
1589					captionInfoExtraElement.innerHTML = caption;
1590
1591					domUtil.show( captionMainElement );
1592					domUtil.show( captionInfoExtraElement );
1593				}
1594
1595				if ( domUtil.stripHTML( caption ) === domUtil.stripHTML( title ) ) {
1596					title = '';
1597				}
1598
1599				if ( domUtil.stripHTML( caption ) === domUtil.stripHTML( desc ) ) {
1600					desc = '';
1601				}
1602
1603				if ( domUtil.stripHTML( title ) === domUtil.stripHTML( desc ) ) {
1604					desc = '';
1605				}
1606
1607				if ( desc ) {
1608					descriptionElement.innerHTML = desc;
1609					domUtil.show( descriptionElement );
1610
1611					if ( ! title && ! caption ) {
1612						captionMainElement.textContent = domUtil.stripHTML( desc );
1613						domUtil.show( captionMainElement );
1614					}
1615				}
1616
1617				if ( title ) {
1618					var plainTitle = domUtil.stripHTML( title );
1619					titleElement.textContent = plainTitle;
1620
1621					if ( ! caption ) {
1622						captionMainElement.textContent = plainTitle;
1623						captionInfoExtraElement.textContent = plainTitle;
1624
1625						domUtil.show( captionMainElement );
1626					}
1627
1628					domUtil.show( titleElement );
1629				}
1630			}
1631		}
1632
1633		// updateExif updates the contents of the exif UL (.jp-carousel-image-exif)
1634		function updateExif( meta ) {
1635			if ( ! meta || Number( jetpackCarouselStrings.display_exif ) !== 1 ) {
1636				return false;
1637			}
1638
1639			// Locate the parent container for the metadata.
1640			var metaContainer = carousel.info.querySelector( '.jp-carousel-image-meta' );
1641			if ( ! metaContainer ) {
1642				return false;
1643			}
1644
1645			var html = '';
1646
1647			for ( var key in meta ) {
1648				var val = meta[ key ];
1649				var metaKeys = jetpackCarouselStrings.meta_data || [];
1650
1651				if ( parseFloat( val ) === 0 || ! val.length || metaKeys.indexOf( key ) === -1 ) {
1652					continue;
1653				}
1654
1655				switch ( key ) {
1656					case 'focal_length':
1657						val = val + 'mm';
1658						break;
1659					case 'shutter_speed':
1660						val = formatShutterSpeed( val );
1661						break;
1662					case 'aperture':
1663						val = 'f/' + val;
1664						break;
1665				}
1666
1667				html += '<li><h5>' + jetpackCarouselStrings[ key ] + '</h5>' + val + '</li>';
1668			}
1669
1670			// Handle the UL element dynamically to ensure valid markup.
1671			var ul = metaContainer.querySelector( 'ul.jp-carousel-image-exif' );
1672
1673			if ( html !== '' ) {
1674				// If there is data to display and the UL doesn't exist, create it.
1675				if ( ! ul ) {
1676					ul = document.createElement( 'ul' );
1677					ul.className = 'jp-carousel-image-exif';
1678
1679					// Insert right after the title/caption container if it exists, otherwise prepend
1680					var titleAndCaption = metaContainer.querySelector( '.jp-carousel-title-and-caption' );
1681					if ( titleAndCaption && titleAndCaption.nextSibling ) {
1682						metaContainer.insertBefore( ul, titleAndCaption.nextSibling );
1683					} else {
1684						metaContainer.insertBefore( ul, metaContainer.firstChild );
1685					}
1686				}
1687				ul.innerHTML = html;
1688				ul.style.removeProperty( 'display' );
1689			} else if ( ul ) {
1690				// If the data is empty but the UL exists in the DOM, remove it.
1691				ul.parentNode.removeChild( ul );
1692			}
1693		}
1694
1695		// Update the contents of the jp-carousel-image-download link
1696		function updateFullSizeLink( currentSlide ) {
1697			if ( ! currentSlide ) {
1698				return false;
1699			}
1700			var original;
1701			var origSize = [ currentSlide.attrs.origWidth, currentSlide.attrs.origHeight ];
1702			var imageLinkParser = document.createElement( 'a' );
1703
1704			imageLinkParser.href = currentSlide.attrs.src.replace( /\?.+$/, '' );
1705
1706			// Is this a Photon URL?
1707			if ( imageLinkParser.hostname.match( /^i[\d]{1}\.wp\.com$/i ) !== null ) {
1708				original = imageLinkParser.href;
1709			} else {
1710				original = currentSlide.attrs.origFile.replace( /\?.+$/, '' );
1711			}
1712
1713			var downloadText = carousel.info.querySelector( '.jp-carousel-download-text' );
1714			var permalink = carousel.info.querySelector( '.jp-carousel-image-download' );
1715
1716			downloadText.innerHTML = util.applyReplacements(
1717				jetpackCarouselStrings.download_original,
1718				origSize
1719			);
1720			permalink.setAttribute( 'href', original );
1721			permalink.style.removeProperty( 'display' );
1722		}
1723
1724		function testCommentsOpened( opened ) {
1725			var commentForm = carousel.info.querySelector( '#jp-carousel-comment-form-container' );
1726			var isOpened = parseInt( opened, 10 ) === 1;
1727
1728			if ( isOpened ) {
1729				domUtil.fadeIn( commentForm );
1730			} else {
1731				domUtil.fadeOut( commentForm );
1732			}
1733		}
1734
1735		function isCommentsPanelOpen() {
1736			var wrapper = carousel.info.querySelector( '.jp-carousel-comments-wrapper' );
1737			return !! wrapper && wrapper.classList.contains( 'jp-carousel-show' );
1738		}
1739
1740		function updateCommentsIndicator( count ) {
1741			var indicator = carousel.info.querySelector(
1742				'.jp-carousel-icon-comments .jp-carousel-has-comments-indicator'
1743			);
1744
1745			if ( ! indicator ) {
1746				return;
1747			}
1748
1749			if ( count > 0 ) {
1750				indicator.innerText = count;
1751				indicator.classList.add( 'jp-carousel-show' );
1752			} else {
1753				indicator.classList.remove( 'jp-carousel-show' );
1754			}
1755		}
1756
1757		/**
1758		 * Show what we already know about a slide's comments, and fetch the rest only if the
1759		 * comments panel is actually on screen. The comment list used to be re-fetched from
1760		 * admin-ajax on every single slide change, even though it is hidden by default.
1761		 * @param {object} slide - The slide being shown.
1762		 */
1763		function showCommentsForSlide( slide ) {
1764			var attachmentId = slide.attrs.attachmentId;
1765			var comments = carousel.info.querySelector( '.jp-carousel-comments' );
1766			var cached = commentsCache[ attachmentId ];
1767
1768			clearInterval( commentInterval );
1769			domUtil.hide( carousel.info.querySelector( '#jp-carousel-comments-loading' ) );
1770
1771			if ( cached ) {
1772				comments.innerHTML = cached.html;
1773				updateCommentsIndicator( cached.count );
1774				domUtil.show( comments );
1775				/*
1776				Resume paging if the cached run ended on a full page, so returning to a slide
1777				does not strand the comments past the first page. Only while the panel is on
1778				screen, mirroring the fresh-fetch path below.
1779				*/
1780				if ( cached.hasMore && isCommentsPanelOpen() ) {
1781					scheduleNextCommentsPage( attachmentId, cached.nextOffset );
1782				}
1783				return;
1784			}
1785
1786			comments.innerHTML = '';
1787			domUtil.hide( comments );
1788			// The server tells us the count up front, so the badge costs no request.
1789			updateCommentsIndicator( parseInt( slide.attrs.commentsCount, 10 ) || 0 );
1790
1791			if ( isCommentsPanelOpen() ) {
1792				fetchComments( attachmentId );
1793			}
1794		}
1795
1796		/**
1797		 * Fetch the next page of comments once the reader scrolls near the bottom.
1798		 * @param {string} attachmentId - The attachment whose comments are shown.
1799		 * @param {number} nextOffset   - Offset of the page to load next.
1800		 */
1801		function scheduleNextCommentsPage( attachmentId, nextOffset ) {
1802			clearInterval( commentInterval );
1803			commentInterval = setInterval( function () {
1804				if ( carousel.container.scrollTop + 150 > window.innerHeight ) {
1805					clearInterval( commentInterval );
1806					fetchComments( attachmentId, nextOffset );
1807				}
1808			}, 300 );
1809		}
1810
1811		function fetchComments( attachmentId, offset, force ) {
1812			var shouldClear = offset === undefined;
1813
1814			clearInterval( commentInterval );
1815
1816			if ( ! attachmentId ) {
1817				return;
1818			}
1819
1820			/*
1821			The comments UI is shared and only ever shows the current slide, so a request for
1822			any other slide (e.g. a post-submit refetch after the reader has navigated on) must
1823			not touch it.
1824			*/
1825			if ( ! carousel.currentSlide || carousel.currentSlide.attrs.attachmentId !== attachmentId ) {
1826				return;
1827			}
1828
1829			/*
1830			One request per attachment at a time, so a reopen mid-flight can't double-load.
1831			`force` (a post-submit refresh) supersedes instead: the newest request is recorded
1832			below and any older one's response bails on the identity check.
1833			*/
1834			if ( commentsFetching[ attachmentId ] && ! force ) {
1835				return;
1836			}
1837
1838			if ( ! offset || offset < 1 ) {
1839				offset = 0;
1840			}
1841
1842			var comments = carousel.info.querySelector( '.jp-carousel-comments' );
1843			var commentsLoading = carousel.info.querySelector( '#jp-carousel-comments-loading' );
1844			domUtil.show( commentsLoading );
1845
1846			if ( shouldClear ) {
1847				domUtil.hide( comments );
1848				comments.innerHTML = '';
1849			}
1850
1851			var xhr = new XMLHttpRequest();
1852			// Record this as the live request for the attachment; a later `force` fetch replaces it.
1853			commentsFetching[ attachmentId ] = xhr;
1854
1855			// True while `xhr` is still the attachment's live request (not superseded).
1856			var isLiveRequest = function () {
1857				return commentsFetching[ attachmentId ] === xhr;
1858			};
1859
1860			var url =
1861				jetpackCarouselStrings.ajaxurl +
1862				'?action=get_attachment_comments' +
1863				'&nonce=' +
1864				jetpackCarouselStrings.nonce +
1865				'&id=' +
1866				attachmentId +
1867				'&offset=' +
1868				offset;
1869			xhr.open( 'GET', url );
1870			xhr.setRequestHeader( 'X-Requested-With', 'XMLHttpRequest' );
1871
1872			// Reveal the comments and clear the loading state; callers gate this themselves.
1873			var revealComments = function () {
1874				domUtil.fadeIn( comments );
1875				domUtil.fadeOut( commentsLoading );
1876			};
1877
1878			// Network-level failure (fires without onload); guard it like a late response.
1879			var onError = function () {
1880				// A newer (forced) request has superseded this one; leave the UI to it.
1881				if ( ! isLiveRequest() ) {
1882					return;
1883				}
1884				delete commentsFetching[ attachmentId ];
1885
1886				// Only touch the shared UI if this attachment is still on screen.
1887				if (
1888					! carousel.currentSlide ||
1889					carousel.currentSlide.attrs.attachmentId !== attachmentId
1890				) {
1891					return;
1892				}
1893				revealComments();
1894			};
1895
1896			xhr.onload = function () {
1897				// A newer (forced) request has superseded this one; drop its result.
1898				if ( ! isLiveRequest() ) {
1899					return;
1900				}
1901				// The request is done, whatever the outcome below.
1902				delete commentsFetching[ attachmentId ];
1903
1904				// Ignore the results if they arrive late and we're now on a different slide.
1905				if (
1906					! carousel.currentSlide ||
1907					carousel.currentSlide.attrs.attachmentId !== attachmentId
1908				) {
1909					return;
1910				}
1911
1912				var isSuccess = xhr.status >= 200 && xhr.status < 300;
1913				var data;
1914				try {
1915					data = JSON.parse( xhr.responseText );
1916				} catch {
1917					// Do nothing.
1918				}
1919
1920				if ( ! isSuccess || ! data || ! Array.isArray( data ) ) {
1921					// Already past the identity/current-slide guards above.
1922					revealComments();
1923					return;
1924				}
1925
1926				if ( shouldClear ) {
1927					comments.innerHTML = '';
1928				}
1929
1930				for ( var i = 0; i < data.length; i++ ) {
1931					var entry = data[ i ];
1932					// Skip anything already on screen: a page can be requested twice if the
1933					// reader re-opens a slide while its previous request is still in flight.
1934					if ( comments.querySelector( '#jp-carousel-comment-' + entry.id ) ) {
1935						continue;
1936					}
1937					var comment = document.createElement( 'div' );
1938					comment.classList.add( 'jp-carousel-comment' );
1939					comment.setAttribute( 'id', 'jp-carousel-comment-' + entry.id );
1940					comment.innerHTML =
1941						'<div class="comment-gravatar">' +
1942						entry.gravatar_markup +
1943						'</div>' +
1944						'<div class="comment-content">' +
1945						'<div class="comment-author">' +
1946						entry.author_markup +
1947						'</div>' +
1948						'<div class="comment-date">' +
1949						entry.date_gmt +
1950						'</div>' +
1951						entry.content +
1952						'</div>';
1953					comments.appendChild( comment );
1954				}
1955
1956				/*
1957				A full page back means there may be more; watch for a scroll to the bottom to
1958				load the next one. A short page means we have reached the end.
1959				*/
1960				var hasMore = data.length >= COMMENTS_PER_PAGE;
1961				var nextOffset = offset + COMMENTS_PER_PAGE;
1962				if ( hasMore ) {
1963					scheduleNextCommentsPage( attachmentId, nextOffset );
1964				}
1965
1966				/*
1967				The endpoint returns at most one page, so prefer the server's total where we
1968				have it and only count what we rendered as a fallback.
1969				*/
1970				var rendered = comments.querySelectorAll( '.jp-carousel-comment' ).length;
1971				var count = Math.max(
1972					parseInt( carousel.currentSlide.attrs.commentsCount, 10 ) || 0,
1973					rendered
1974				);
1975
1976				if ( data.length > 0 ) {
1977					domUtil.show( comments );
1978					updateCommentsIndicator( count );
1979				}
1980
1981				/*
1982				Keep what we rendered for as long as the overlay is open, so going back to a
1983				slide costs nothing -- including where to resume paging from.
1984				*/
1985				commentsCache[ attachmentId ] = {
1986					html: comments.innerHTML,
1987					count: count,
1988					hasMore: hasMore,
1989					nextOffset: nextOffset,
1990				};
1991
1992				domUtil.hide( commentsLoading );
1993			};
1994
1995			xhr.onerror = onError;
1996
1997			xhr.send();
1998		}
1999
2000		function loadFullImage( slide ) {
2001			var attrs = slide.attrs;
2002			var image = slide.el.querySelector( 'img' );
2003
2004			if ( image.hasAttribute( 'data-loaded' ) ) {
2005				return;
2006			}
2007
2008			image.setAttribute( 'itemprop', 'image' );
2009			image.setAttribute( 'data-loaded', 1 );
2010
2011			var hasPreview = attrs.previewImage && attrs.previewImage !== attrs.src;
2012
2013			if ( ! hasPreview ) {
2014				// No usable in-page thumbnail (e.g. a lazy-loading plugin swapped the
2015				// gallery src for a placeholder). Load the full-size image straight
2016				// into the visible element so the slide is never left without a src.
2017				image.src = attrs.src;
2018				return;
2019			}
2020
2021			// Show the thumbnail the browser has already decoded for this image in the
2022			// post itself. Without it the slide stays empty until the full-size image
2023			// arrives, which reads as a black screen whenever the reader moves through
2024			// the gallery faster than the images can download.
2025			image.src = attrs.previewImage;
2026			// The thumbnail is much smaller than the slide, so soften the upscale
2027			// until the full-size image replaces it.
2028			image.style.filter = 'blur(8px)';
2029
2030			// Load the full-size image off-DOM, then swap it in over the preview. On
2031			// error the (blurred) preview stays put rather than reverting to blank.
2032			var fullImage = new window.Image();
2033
2034			fullImage.addEventListener(
2035				'load',
2036				function () {
2037					/*
2038					If the visitor zoomed while this was still downloading, a larger rendition
2039					is already in place -- don't downgrade it back to the fit-to-screen src.
2040					*/
2041					if ( ! image.hasAttribute( 'data-zoom-loaded' ) ) {
2042						// Cached by this point, so swapping it in is effectively instant.
2043						image.src = attrs.src;
2044					}
2045					image.style.filter = '';
2046				},
2047				{ once: true }
2048			);
2049
2050			fullImage.addEventListener(
2051				'error',
2052				function () {
2053					image.style.filter = '';
2054				},
2055				{ once: true }
2056			);
2057
2058			fullImage.src = attrs.src;
2059		}
2060
2061		/**
2062		 * Swap in a higher-resolution rendition now that the visitor is zoomed in.
2063		 *
2064		 * The zoomed-out slide only ever loads what the screen can actually show. The browser
2065		 * keeps painting the image it already has until this one decodes, so the picture
2066		 * sharpens rather than blanking.
2067		 * @param {object} slide - The slide being zoomed.
2068		 */
2069		function loadZoomImage( slide ) {
2070			if ( ! slide || ! slide.attrs.zoomSrc || slide.attrs.zoomSrc === slide.attrs.src ) {
2071				return;
2072			}
2073
2074			var image = slide.el.querySelector( 'img' );
2075			if ( ! image || image.hasAttribute( 'data-zoom-loaded' ) ) {
2076				return;
2077			}
2078
2079			image.setAttribute( 'data-zoom-loaded', 1 );
2080			image.src = slide.attrs.zoomSrc;
2081		}
2082
2083		function preloadAdjacentImages( currentIndex ) {
2084			var indicesToPreload = [];
2085			var totalSlides = carousel.slides.length;
2086
2087			// Only preload adjacent images if we have more than one slide (matching loop condition)
2088			if ( totalSlides > 1 ) {
2089				// Previous image (with loop handling)
2090				var prevIndex = currentIndex > 0 ? currentIndex - 1 : totalSlides - 1;
2091				indicesToPreload.push( prevIndex );
2092
2093				// Next image (with loop handling)
2094				var nextIndex = currentIndex < totalSlides - 1 ? currentIndex + 1 : 0;
2095				indicesToPreload.push( nextIndex );
2096			}
2097
2098			indicesToPreload.forEach( function ( index ) {
2099				var slide = carousel.slides[ index ];
2100				if ( slide ) {
2101					// Load in background without showing
2102					loadFullImage( slide );
2103
2104					// Also load background image if enabled
2105					if (
2106						Number( jetpackCarouselStrings.display_background_image ) === 1 &&
2107						! slide.backgroundImage
2108					) {
2109						loadBackgroundImage( slide );
2110					}
2111				}
2112			} );
2113		}
2114
2115		function loadBackgroundImage( slide ) {
2116			var image = slide.attrs.originalElement;
2117
2118			if ( ! image ) {
2119				return;
2120			}
2121
2122			if ( image.complete && image.naturalHeight !== 0 ) {
2123				applyBackgroundImage( slide, image );
2124				return;
2125			}
2126
2127			// The thumbnail in the post may still be loading, or may be lazy-loaded.
2128			// Use an event listener rather than `onload`, which would overwrite any
2129			// handler the page has already attached to its own image. Pair the load
2130			// handler with an error handler so a thumbnail that never loads doesn't
2131			// leave a listener (and its reference to the slide) attached for good.
2132			var onLoad = function () {
2133				image.removeEventListener( 'error', onError );
2134				applyBackgroundImage( slide, image );
2135			};
2136			var onError = function () {
2137				image.removeEventListener( 'load', onLoad );
2138			};
2139			image.addEventListener( 'load', onLoad, { once: true } );
2140			image.addEventListener( 'error', onError, { once: true } );
2141		}
2142
2143		function applyBackgroundImage( slide, image ) {
2144			var url = util.getBackgroundImage( image );
2145
2146			if ( ! url ) {
2147				return;
2148			}
2149
2150			// Always paint onto the slide the image belongs to. Preloading runs this
2151			// for the neighbouring slides too, so painting onto whichever slide happens
2152			// to be active would put the wrong image behind it and leave the slide it
2153			// was meant for with no placeholder at all.
2154			slide.backgroundImage = url;
2155			slide.el.style.backgroundImage = 'url(' + url + ')';
2156			slide.el.style.backgroundSize = 'cover';
2157			slide.el.style.backgroundPosition = 'center';
2158		}
2159
2160		function clearCommentTextAreaValue() {
2161			if ( carousel.commentField ) {
2162				carousel.commentField.value = '';
2163			}
2164		}
2165
2166		function getOriginalDimensions( el ) {
2167			var size = el.getAttribute( 'data-orig-size' ) || '';
2168
2169			if ( size ) {
2170				var parts = size.split( ',' );
2171				return { width: parseInt( parts[ 0 ], 10 ), height: parseInt( parts[ 1 ], 10 ) };
2172			}
2173			return {
2174				width: el.getAttribute( 'data-original-width' ) || el.getAttribute( 'width' ) || undefined,
2175				height:
2176					el.getAttribute( 'data-original-height' ) || el.getAttribute( 'height' ) || undefined,
2177			};
2178		}
2179
2180		function initCarouselSlides( items, startIndex ) {
2181			carousel.slides = [];
2182
2183			var max = calculateMaxSlideDimensions();
2184
2185			// If the startIndex is not 0 then preload the clicked image first.
2186			if ( startIndex !== 0 && items[ startIndex ].getAttribute( 'data-gallery-src' ) !== null ) {
2187				var img = new Image();
2188				img.src = items[ startIndex ].getAttribute( 'data-gallery-src' );
2189			}
2190
2191			// create the 'slide'
2192			Array.prototype.forEach.call( items, function ( item, i ) {
2193				var permalinkEl = domUtil.closest( item, 'a' );
2194				var origFile = item.getAttribute( 'data-orig-file' ) || item.getAttribute( 'src-orig' );
2195				var attrID =
2196					item.getAttribute( 'data-attachment-id' ) || item.getAttribute( 'data-id' ) || '0';
2197				var caption = document.querySelector(
2198					'img[data-attachment-id="' + attrID + '"] + figcaption'
2199				);
2200
2201				if ( caption ) {
2202					caption = caption.innerHTML;
2203				} else {
2204					caption = item.getAttribute( 'data-image-caption' );
2205				}
2206
2207				var attrs = {
2208					originalElement: item,
2209					attachmentId: attrID,
2210					commentsOpened: item.getAttribute( 'data-comments-opened' ) || '0',
2211					commentsCount: item.getAttribute( 'data-comments-count' ) || '0',
2212					imageMeta: domUtil.getJSONAttribute( item, 'data-image-meta' ) || {},
2213					title: item.getAttribute( 'data-image-title' ) || '',
2214					desc: item.getAttribute( 'data-image-description' ) || '',
2215					largeFile: item.getAttribute( 'data-large-file' ) || '',
2216					origFile: origFile || '',
2217					thumbSize: { width: item.naturalWidth, height: item.naturalHeight },
2218					caption: caption || '',
2219					permalink: permalinkEl && permalinkEl.getAttribute( 'href' ),
2220					src: origFile || item.getAttribute( 'src' ) || '',
2221				};
2222
2223				var tiledGalleryItem = domUtil.closest( item, '.tiled-gallery-item' );
2224				var tiledCaptionEl =
2225					tiledGalleryItem && tiledGalleryItem.querySelector( '.tiled-gallery-caption' );
2226				var tiledCaption = tiledCaptionEl && tiledCaptionEl.innerHTML;
2227				if ( tiledCaption ) {
2228					attrs.caption = tiledCaption;
2229				}
2230
2231				var origDimensions = getOriginalDimensions( item );
2232
2233				attrs.origWidth = origDimensions.width || attrs.thumbSize.width;
2234				attrs.origHeight = origDimensions.height || attrs.thumbSize.height;
2235
2236				if ( typeof wpcom !== 'undefined' && wpcom.carousel && wpcom.carousel.generateImgSrc ) {
2237					attrs.src = wpcom.carousel.generateImgSrc( item, max );
2238				} else {
2239					var urlArgs = function ( zoomHeadroom ) {
2240						// A fresh object each time: selectBestImageUrl() mutates what it is given.
2241						return {
2242							origFile: attrs.src,
2243							origWidth: attrs.origWidth,
2244							origHeight: attrs.origHeight,
2245							maxWidth: max.width,
2246							maxHeight: max.height,
2247							largeFile: attrs.largeFile,
2248							zoomHeadroom: zoomHeadroom,
2249						};
2250					};
2251
2252					/*
2253					The zoom rendition is kept aside until the visitor actually zooms. On sources
2254					that cannot serve a larger one it comes back identical to `src`, and n
2254othing
2255					ever happens.
2256					*/
2257					attrs.zoomSrc = selectBestImageUrl( urlArgs( 2 ) );
2258					attrs.src = selectBestImageUrl( urlArgs( 1 ) );
2259				}
2260
2261				// Set the final src.
2262				item.setAttribute( 'data-gallery-src', attrs.src );
2263
2264				if ( attrs.attachmentId !== '0' ) {
2265					attrs.title = util.texturize( attrs.title );
2266					attrs.desc = util.texturize( attrs.desc );
2267					attrs.caption = util.texturize( attrs.caption );
2268
2269					// Initially, the image is a 1x1 transparent gif.
2270					// The preview is shown as a background image on the slide itself.
2271					var image = new Image();
2272
2273					var slideEl = document.createElement( 'div' );
2274					slideEl.classList.add( 'swiper-slide' );
2275					slideEl.setAttribute( 'itemprop', 'associatedMedia' );
2276					slideEl.setAttribute( 'itemscope', '' );
2277					slideEl.setAttribute( 'itemtype', 'https://schema.org/ImageObject' );
2278					var zoomEl = document.createElement( 'div' );
2279					zoomEl.classList.add( 'swiper-zoom-container' );
2280
2281					carousel.gallery.appendChild( slideEl );
2282
2283					slideEl.appendChild( zoomEl );
2284					zoomEl.appendChild( image );
2285					slideEl.setAttribute( 'data-attachment-id', attrs.attachmentId );
2286					slideEl.setAttribute( 'data-permalink', attrs.permalink );
2287					slideEl.setAttribute( 'data-orig-file', attrs.origFile );
2288
2289					// Reuse the thumbnail the browser has already decoded for this image
2290					// in the post. It costs nothing to display and gives the slide
2291					// something to show while the full-size image is downloading.
2292					attrs.previewImage = item.currentSrc || item.getAttribute( 'src' ) || '';
2293
2294					var slide = { el: slideEl, attrs: attrs, index: i };
2295					carousel.slides.push( slide );
2296				}
2297			} );
2298		}
2299
2300		function loadSwiper( gallery, options ) {
2301			if ( ! window.JetpackSwiper ) {
2302				var loader = document.querySelector( '#jp-carousel-loading-overlay' );
2303				domUtil.show( loader );
2304				var jsScript = document.createElement( 'script' );
2305				jsScript.id = 'jetpack-carousel-swiper-js';
2306				jsScript.src = window.jetpackSwiperLibraryPath.url;
2307				jsScript.async = true;
2308				jsScript.onload = function () {
2309					domUtil.hide( loader );
2310					openCarousel( gallery, options );
2311				};
2312				jsScript.onerror = function () {
2313					domUtil.hide( loader );
2314				};
2315				document.head.appendChild( jsScript );
2316				return;
2317			}
2318			openCarousel( gallery, options );
2319		}
2320
2321		function openCarousel( gallery, options ) {
2322			var settings = {
2323				imgSelector:
2324					'.gallery-item [data-attachment-id], .tiled-gallery-item [data-attachment-id], img[data-attachment-id], img[data-id]',
2325				startIndex: 0,
2326			};
2327
2328			var data = domUtil.getJSONAttribute( gallery, 'data-carousel-extra' );
2329			var tapTimeout;
2330
2331			if ( ! data ) {
2332				return; // don't run if the default gallery functions weren't used
2333			}
2334
2335			const images = gallery.querySelectorAll( settings.imgSelector );
2336			if ( ! images.length ) {
2337				return; // don't run if we found no images in the gallery (somehow it has images that aren't in the media library?)
2338			}
2339
2340			initializeCarousel();
2341
2342			if ( carousel.isOpen ) {
2343				return; // don't open if already opened
2344			}
2345			carousel.isOpen = true;
2346
2347			// make sure to stop the page from scrolling behind the carousel overlay, so we don't trigger
2348			// infiniscroll for it when enabled (Reader, theme infiniscroll, etc).
2349			originalOverflow = getComputedStyle( document.body ).overflow;
2350			document.body.style.overflow = 'hidden';
2351			// prevent html from overflowing on some of the new themes.
2352			originalHOverflow = getComputedStyle( document.documentElement ).overflow;
2353			document.documentElement.style.overflow = 'hidden';
2354			scrollPos = window.scrollY || window.pageYOffset || 0;
2355
2356			carousel.container.setAttribute( 'data-carousel-extra', JSON.stringify( data ) );
2357			stat( [ 'open', 'view_image' ] );
2358
2359			// If options exist, lets merge them
2360			// with our default settings
2361			for ( var option in options || {} ) {
2362				settings[ option ] = options[ option ];
2363			}
2364
2365			if ( settings.startIndex === -1 ) {
2366				settings.startIndex = 0; // -1 returned if can't find index, so start from beginning
2367			}
2368
2369			domUtil.emitEvent( carousel.overlay, 'jp_carousel.beforeOpen' );
2370			carousel.gallery.innerHTML = '';
2371			commentsCache = {};
2372			commentsFetching = {};
2373
2374			// Need to set the overlay manually to block or swiper does't initialise properly.
2375			carousel.overlay.style.opacity = 1;
2376			carousel.overlay.style.display = 'block';
2377
2378			initCarouselSlides( images, settings.startIndex );
2379
2380			swiper = new window.JetpackSwiper( '.jp-carousel-swiper-container', {
2381				centeredSlides: true,
2382				/*
2383				Swiper's 300ms default is most of what makes slide-to-slide feel slow. Keep a
2384				visible movement -- the slide is a real affordance on touch -- but a shorter one.
2385				*/
2386				speed: domUtil.prefersReducedMotion() ? 0 : 250,
2387				zoom: true,
2388				loop: carousel.slides.length > 1,
2389				// Turn off interactions and hide navigation arrows if there is only one slide.
2390				enabled: carousel.slides.length > 1,
2391				pagination: {
2392					el: '.jp-swiper-pagination',
2393					clickable: true,
2394				},
2395				navigation: {
2396					nextEl: '.jp-swiper-button-next',
2397					prevEl: '.jp-swiper-button-prev',
2398				},
2399				initialSlide: settings.startIndex,
2400				on: {
2401					init: function () {
2402						selectSlideAtIndex( settings.startIndex );
2403					},
2404				},
2405				preventClicks: false,
2406				preventClicksPropagation: false,
2407				preventInteractionOnTransition: ! domUtil.isTouch(),
2408				threshold: 5,
2409			} );
2410
2411			swiper.on( 'slideChange', function ( swiper ) {
2412				if ( ! carousel.isOpen ) {
2413					return;
2414				}
2415				selectSlideAtIndex( swiper.realIndex );
2416				carousel.overlay.classList.remove( 'jp-carousel-hide-controls' );
2417			} );
2418
2419			swiper.on( 'zoomChange', function ( swiper, scale ) {
2420				if ( scale > 1 ) {
2421					loadZoomImage( carousel.currentSlide );
2422					carousel.overlay.classList.add( 'jp-carousel-hide-controls' );
2423				}
2424
2425				if ( scale === 1 ) {
2426					carousel.overlay.classList.remove( 'jp-carousel-hide-controls' );
2427				}
2428			} );
2429
2430			swiper.on( 'doubleTap', function ( swiper ) {
2431				clearTimeout( tapTimeout );
2432				if ( swiper.zoom.scale === 1 ) {
2433					var zoomTimeout = setTimeout( function () {
2434						carousel.overlay.classList.remove( 'jp-carousel-hide-controls' );
2435						clearTimeout( zoomTimeout );
2436					}, 150 );
2437				}
2438			} );
2439
2440			swiper.on( 'tap', function () {
2441				if ( swiper.zoom.scale > 1 ) {
2442					tapTimeout = setTimeout( function () {
2443						carousel.overlay.classList.toggle( 'jp-carousel-hide-controls' );
2444					}, 150 );
2445				}
2446			} );
2447
2448			domUtil.fadeIn( carousel.overlay, function () {
2449				domUtil.emitEvent( carousel.overlay, 'jp_carousel.afterOpen' );
2450			} );
2451		}
2452
2453		// Register the event listeners for starting the gallery
2454		document.body.addEventListener( 'click', handleInteraction );
2455		document.body.addEventListener( 'keydown', handleInteraction );
2456		document.querySelectorAll( galleryItemSelector + 'img' ).forEach( function ( galleryImage ) {
2457			if ( shouldOpenModal( galleryImage ) ) {
2458				makeGalleryImageAccessible( galleryImage );
2459			}
2460		} );
2461
2462		function handleInteraction( e ) {
2463			if ( e.type === 'click' ) {
2464				handleClick( e );
2465				return;
2466			}
2467
2468			if ( e.type === 'keydown' ) {
2469				const parentElement = document.activeElement.parentElement;
2470				const isParentCarouselContainer =
2471					parentElement && parentElement.classList.contains( 'tiled-gallery__item' );
2472
2473				if ( ( e.key === ' ' || e.key === 'Enter' ) && isParentCarouselContainer ) {
2474					handleClick( e );
2475				}
2476			}
2477		}
2478
2479		function normalizeUrl( url ) {
2480			return ( url || '' ).split( '?' )[ 0 ].replace( /\/$/, '' );
2481		}
2482
2483		function shouldOpenModal( el ) {
2484			if ( el.tagName === 'A' ) {
2485				el = el.querySelector( 'img' ) || el;
2486			}
2487
2488			var parent = el.parentElement;
2489			var grandparent = parent ? parent.parentElement : null;
2490
2491			// If Gallery is made up of individual Image blocks check for custom link before
2492			// loading carousel. The custom link may be the parent or could be a descendant
2493			// of the parent if the image has rounded corners.
2494			var parentHref = null;
2495			if ( grandparent && grandparent.classList.contains( 'wp-block-image' ) ) {
2496				parentHref = parent.getAttribute( 'href' );
2497			} else if (
2498				parent &&
2499				parent.classList.contains( 'wp-block-image' ) &&
2500				parent.querySelector( ':scope > a' )
2501			) {
2502				parentHref = parent.querySelector( ':scope > a' ).getAttribute( 'href' );
2503			}
2504
2505			// If the link does not point to the attachment or media file then assume Image has
2506			// a custom link so don't load the carousel.
2507			if ( parentHref ) {
2508				var cleanHref = normalizeUrl( parentHref );
2509				var cleanOrig = normalizeUrl( el.getAttribute( 'data-orig-file' ) );
2510				var cleanPerm = normalizeUrl( el.getAttribute( 'data-permalink' ) );
2511
2512				if ( cleanHref !== cleanOrig && cleanHref !== cleanPerm ) {
2513					return false;
2514				}
2515			}
2516
2517			// Do not open the modal if we are looking at a gallery caption from before WP5, which may contain a link.
2518			if ( parent && parent.classList.contains( 'gallery-caption' ) ) {
2519				return false;
2520			}
2521
2522			// Do not open the modal if we are looking at a caption of a gallery block, which may contain a link.
2523			if ( domUtil.matches( parent, 'figcaption' ) ) {
2524				return false;
2525			}
2526
2527			return true;
2528		}
2529
2530		function handleClick( e ) {
2531			var isCompatible =
2532				window.CSS && window.CSS.supports && window.CSS.supports( 'display', 'grid' );
2533
2534			// IE11 support is being dropped in August 2021. The new swiper.js libray is not IE11 compat
2535			// so just default to opening individual image attachment/media pages for IE.
2536			if ( ! isCompatible ) {
2537				return;
2538			}
2539
2540			var target = e.target;
2541			var gallery = domUtil.closest( target, gallerySelector );
2542
2543			if ( gallery ) {
2544				if ( ! testForData( gallery ) ) {
2545					return;
2546				}
2547
2548				if ( ! shouldOpenModal( target ) ) {
2549					return;
2550				}
2551
2552				// Set height to auto.
2553				// Fix some themes where closing carousel brings view back to top.
2554				document.documentElement.style.height = 'auto';
2555
2556				e.preventDefault();
2557
2558				// Stopping propagation in case there are parent elements
2559				// with .gallery or .tiled-gallery class
2560				e.stopPropagation();
2561
2562				var item = domUtil.closest( target, itemSelector );
2563				var index = Array.prototype.indexOf.call( gallery.querySelectorAll( itemSelector ), item );
2564				loadSwiper( gallery, { startIndex: index } );
2565			}
2566		}
2567
2568		// Handle lightbox (single image gallery) for images linking to 'Attachment Page'.
2569		if ( Number( jetpackCarouselStrings.single_image_gallery ) === 1 ) {
2570			processSingleImageGallery();
2571			document.body.addEventListener( 'is.post-load', function () {
2572				processSingleImageGallery();
2573			} );
2574		}
2575
2576		// Makes carousel work on page load and when back button leads to same URL with carousel hash
2577		// (i.e. no actual document.ready trigger).
2578		window.addEventListener( 'hashchange', function () {
2579			var hashRegExp = /jp-carousel-(\d+)/;
2580
2581			if ( ! window.location.hash || ! hashRegExp.test( window.location.hash ) ) {
2582				if ( carousel.isOpen ) {
2583					closeCarousel();
2584				}
2585
2586				return;
2587			}
2588
2589			if ( window.location.hash === lastKnownLocationHash && carousel.isOpen ) {
2590				return;
2591			}
2592
2593			if ( window.location.hash && carousel.gallery && ! carousel.isOpen && history.back ) {
2594				history.back();
2595				return;
2596			}
2597
2598			lastKnownLocationHash = window.location.hash;
2599			var matchList = window.location.hash.match( hashRegExp );
2600			var attachmentId = parseInt( matchList[ 1 ], 10 );
2601			var galleries = document.querySelectorAll( gallerySelector );
2602
2603			// Find the first thumbnail that matches the attachment ID in the location
2604			// hash, then open the gallery that contains it.
2605			for ( var i = 0; i < galleries.length; i++ ) {
2606				var gallery = galleries[ i ];
2607				var selected;
2608
2609				var images = gallery.querySelectorAll( 'img' );
2610				for ( var j = 0; j < images.length; j++ ) {
2611					if (
2612						parseInt( images[ j ].getAttribute( 'data-attachment-id' ), 10 ) === attachmentId ||
2613						parseInt( images[ j ].getAttribute( 'data-id' ), 10 ) === attachmentId
2614					) {
2615						selected = j;
2616						break;
2617					}
2618				}
2619
2620				if ( selected !== undefined ) {
2621					openOrSelectSlide( gallery, selected );
2622					break;
2623				}
2624			}
2625		} );
2626
2627		if ( window.location.hash ) {
2628			domUtil.emitEvent( window, 'hashchange' );
2629		}
2630	}
2631
2632	if ( document.readyState !== 'loading' ) {
2633		init();
2634	} else {
2635		document.addEventListener( 'DOMContentLoaded', init );
2636	}
2637} )();
2638;

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.