PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.5
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.5
16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 13.8.3 All 506 releases
jetpack / modules / carousel / jetpack-carousel.js

jetpack-carousel.js in Jetpack – WP Security, Backup, Speed, & Growth 16.3-a.5, at modules/carousel/jetpack-carousel.js

2,184 lines 67.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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 phones = 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 inverting
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 nothing
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 } )();
2184