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, '’' ).replace( /'/g, '’' ); 471 text = text 472 .replace( /"/g, '”' ) 473 .replace( /"/g, '”' ) 474 .replace( /"/g, '”' ) 475 .replace( /[\u201D]/g, '”' ); 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.