PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3
Jetpack – WP Security, Backup, Speed, & Growth v16.3
16.3 16.3-beta 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 All 508 releases
← All changes | modules/carousel/jetpack-carousel.js +2183 -0 16.2-beta → 16.3 View file →
@@ -1,0 +1,2183 @@
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 +} )();