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