PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 16.0-beta
Jetpack – WP Security, Backup, Speed, & Growth v16.0-beta
16.0 16.0-beta 16.0-a.7 16.0-a.5 15.9.1 16.0-a.3 16.0-a.1 15.9 15.9-beta 15.9-a.7 15.9-a.5 15.9-a.3 15.9-a.1 15.8 15.8-beta 15.8-a.7 15.8-a.5 5.2.5 5.3.4 5.4.4 5.5.5 5.6.5 5.7.5 5.8.4 5.9.4 6.0.4 6.1 6.1.1 6.1.2 6.1.3 6.1.4 6.1.5 6.2 6.2.1 6.2.2 6.2.3 6.2.4 6.2.5 6.3 6.3.1 6.3.2 6.3.3 6.3.4 6.3.5 6.3.6 6.3.7 6.4 6.4.1 6.4.2 6.4.3 6.4.4 6.4.5 6.4.6 6.5 6.5.1 6.5.2 6.5.3 6.5.4 6.6 6.6.1 6.6.2 6.6.3 6.6.4 6.6.5 6.7 6.7.1 6.7.2 6.7.3 6.7.4 6.8 6.8.1 6.8.2 6.8.3 6.8.4 6.8.5 6.9 6.9.1 6.9.2 6.9.3 6.9.4 7.0 7.0.1 7.0.2 7.0.3 7.0.4 7.0.5 7.1 7.1.1 7.1.2 7.1.3 7.1.4 7.1.5 7.2 7.2.1 7.2.1.1 7.2.2 7.2.3 7.2.4 7.2.5 7.3 7.3.0.1 7.3.1 7.3.1.1 7.3.2 7.3.3 7.3.4 7.3.5 7.4 7.4.1 7.4.2 7.4.3 7.4.4 7.4.5 7.5 7.5.0.1 7.5.1 7.5.2 7.5.3 7.5.4 7.5.5 7.5.6 7.5.7 7.6 7.6.1 7.6.2 7.6.3 7.6.4 7.7 7.7.1 7.7.2 7.7.3 7.7.4 7.7.5 7.7.6 7.8 7.8.1 7.8.2 7.8.3 7.8.4 7.9 7.9.1 7.9.2 7.9.3 7.9.4 8.0 8.0.1 8.0.2 8.0.3 8.1 8.1.1 8.1.2 8.1.3 8.1.4 8.2 8.2.0.1 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.2.6 8.3 8.3.1 8.3.2 8.3.3 8.4 8.4.1 8.4.2 8.4.3 8.4.4 8.4.5 8.5 8.5.1 8.5.2 8.5.3 8.6 8.6.1 8.6.2 8.6.3 8.6.4 8.7 8.7.0.1 8.7.1 8.7.2 8.7.3 8.7.4 8.8 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.9 8.9.1 8.9.2 8.9.3 8.9.4 9.0 9.0.1 9.0.2 9.0.3 9.0.4 9.0.5 9.1 9.1.1 9.1.2 9.1.3 9.2 9.2.1 9.2.2 9.2.3 9.2.4 9.3 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.4 9.4.1 9.4.2 9.4.3 9.4.4 9.5 9.5.1 9.5.2 9.5.3 9.5.4 9.5.5 9.6 9.6.1 9.6.2 9.6.3 9.6.4 9.7 9.7.1 9.7.2 15.7-beta.2 9.7.3 15.7.1 9.8 15.8-a.1 9.8.1 15.8-a.3 9.8.2 2.0.9 9.8.3 2.1.7 9.9 2.2.10 9.9.1 2.3.10 9.9.2 2.4.7 9.9.3 2.5.5 2.6.6 2.7.5 2.8.5 2.9.6 3.0.6 3.1.5 3.2.5 3.3.6 3.4.6 3.5.6 3.6.4 3.7.5 3.8.5 3.9.10 4.0.7 4.1.4 4.2.5 4.3.5 4.4.5 4.5.3 4.6.3 4.7.4 4.8.5 4.9.3 5.0.3 5.1.4 trunk 10.0 10.0.1 10.0.2 10.1 10.1.1 10.1.2 10.2 10.2.1 10.2.2 10.2.3 10.3 10.3.1 10.3.2 10.4 10.4.1 10.4.2 10.5 10.5.1 10.5.2 10.5.3 10.6 10.6.1 10.6.2 10.7 10.7.1 10.7.2 10.8 10.8.1 10.8.2 10.9 10.9.1 10.9.2 10.9.3 11.0 11.0.1 11.0.2 11.1 11.1.1 11.1.2 11.1.3 11.1.4 11.2 11.2.1 11.2.2 11.3 11.3.1 11.3.2 11.3.3 11.3.4 11.4 11.4.1 11.4.2 11.5 11.5.1 11.5.2 11.5.3 11.6 11.6.1 11.6.2 11.7 11.7.1 11.7.2 11.7.3 11.8 11.8.3 11.8.4 11.8.5 11.8.6 11.9 11.9.1 11.9.2 11.9.3 12.0 12.0.1 12.0.2 12.1 12.1.1 12.1.2 12.2 12.2.1 12.2.2 12.3 12.3.1 12.4 12.4.1 12.5 12.5.1 12.6 12.6.1 12.6.2 12.6.3 12.7 12.7.1 12.7.2 12.8 12.8.1 12.8.2 12.9 12.9.1 12.9.2 12.9.3 12.9.4 13.0 13.0.1 13.1 13.1.1 13.1.2 13.1.3 13.1.4 13.2 13.2.1 13.2.2 13.2.3 13.3 13.3.1 13.3.2 13.4 13.4.1 13.4.2 13.4.3 13.4.4 13.5 13.5.1 13.6 13.6.1 13.7 13.7.1 13.8 13.8.1 13.8.2 13.9 13.9.1 14.0 14.1 14.2 14.2.1 14.3 14.4 14.4.1 14.5 14.6 14.7 14.8 14.9 14.9.1 15.0 15.0.1 15.0.2 15.1 15.1.1 15.2 15.3 15.3.1 15.4 15.5 15.6 15.7 15.7-a.1 15.7-a.3 15.7-a.5 15.7-a.7 15.7-beta
jetpack / modules / carousel / jetpack-carousel.js
jetpack / modules / carousel Last commit date
jetpack-carousel-rtl.css 1 month ago jetpack-carousel.css 2 months ago jetpack-carousel.js 1 month ago jetpack-carousel.php 1 month ago
jetpack-carousel.js
1820 lines
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 function fade( el, start, end, callback ) {
112 if ( ! el ) {
113 return callback();
114 }
115
116 // Prepare for transition.
117 // Ensure the item is in the render tree, in its initial state.
118 el.style.removeProperty( 'display' );
119 el.style.opacity = start;
120 el.style.pointerEvents = 'none';
121
122 var animate = function ( t0, duration ) {
123 var t = performance.now();
124 var diff = t - t0;
125 var ratio = diff / duration;
126
127 if ( ratio < 1 ) {
128 el.style.opacity = start + ( end - start ) * ratio;
129 requestAnimationFrame( () => animate( t0, duration ) );
130 } else {
131 el.style.opacity = end;
132 el.style.removeProperty( 'pointer-events' );
133 callback();
134 }
135 };
136
137 requestAnimationFrame( function () {
138 // Double rAF for browser compatibility.
139 requestAnimationFrame( function () {
140 animate( performance.now(), 200 );
141 } );
142 } );
143 }
144
145 function fadeIn( el, callback ) {
146 callback = callback || util.noop;
147 fade( el, 0, 1, callback );
148 }
149
150 function fadeOut( el, callback ) {
151 callback = callback || util.noop;
152 fade( el, 1, 0, function () {
153 if ( el ) {
154 el.style.display = 'none';
155 }
156 callback();
157 } );
158 }
159
160 function emitEvent( el, type, detail ) {
161 var e;
162 try {
163 e = new CustomEvent( type, {
164 bubbles: true,
165 cancelable: true,
166 detail: detail || null,
167 } );
168 } catch {
169 e = document.createEvent( 'CustomEvent' );
170 e.initCustomEvent( type, true, true, detail || null );
171 }
172 el.dispatchEvent( e );
173 }
174
175 // From: https://easings.net/#easeInOutQuad
176 function easeInOutQuad( num ) {
177 return num < 0.5 ? 2 * num * num : 1 - Math.pow( -2 * num + 2, 2 ) / 2;
178 }
179
180 function getFooterClearance( container ) {
181 var footer = container.querySelector( '.jp-carousel-info-footer' );
182 var infoArea = container.querySelector( '.jp-carousel-info-extra' );
183 var contentArea = container.querySelector( '.jp-carousel-info-content-wrapper' );
184
185 if ( footer && infoArea && contentArea ) {
186 var styles = window.getComputedStyle( infoArea );
187 var padding = parseInt( styles.paddingTop, 10 ) + parseInt( styles.paddingBottom, 10 );
188 padding = isNaN( padding ) ? 0 : padding;
189 return contentArea.offsetHeight + footer.offsetHeight + padding;
190 }
191 return 0;
192 }
193
194 function isTouch() {
195 return (
196 'ontouchstart' in window || ( window.DocumentTouch && document instanceof DocumentTouch )
197 );
198 }
199
200 function scrollToElement( el, container, callback ) {
201 if ( ! el || ! container ) {
202 if ( callback ) {
203 return callback();
204 }
205 return;
206 }
207
208 // For iOS Safari compatibility, use JS to set the minimum height.
209 var infoArea = container.querySelector( '.jp-carousel-info-extra' );
210 if ( infoArea ) {
211 // 64px is the same height as `.jp-carousel-info-footer` in the CSS.
212 infoArea.style.minHeight = window.innerHeight - 64 + 'px';
213 }
214
215 var isScrolling = true;
216 var startTime = Date.now();
217 var duration = 300;
218 var originalPosition = container.scrollTop;
219 var targetPosition = Math.max(
220 0,
221 el.offsetTop - Math.max( 0, window.innerHeight - getFooterClearance( container ) )
222 );
223 var distance = targetPosition - container.scrollTop;
224 distance = Math.min( distance, container.scrollHeight - window.innerHeight );
225
226 function stopScroll() {
227 isScrolling = false;
228 }
229
230 function runScroll() {
231 var now = Date.now();
232 var progress = easeInOutQuad( ( now - startTime ) / duration );
233
234 progress = progress > 1 ? 1 : progress;
235 var newVal = progress * distance;
236 container.scrollTop = originalPosition + newVal;
237
238 if ( now <= startTime + duration && isScrolling ) {
239 return requestAnimationFrame( runScroll );
240 }
241 if ( callback ) {
242 callback();
243 }
244 if ( infoArea ) {
245 infoArea.style.minHeight = '';
246 }
247 isScrolling = false;
248 container.removeEventListener( 'wheel', stopScroll );
249 }
250
251 // Allow scroll to be cancelled by user interaction.
252 container.addEventListener( 'wheel', stopScroll );
253 runScroll();
254 }
255
256 function getJSONAttribute( el, attr ) {
257 if ( ! el || ! el.hasAttribute( attr ) ) {
258 return undefined;
259 }
260
261 try {
262 return JSON.parse( el.getAttribute( attr ) );
263 } catch {
264 return undefined;
265 }
266 }
267
268 function convertToPlainText( html ) {
269 var dummy = document.createElement( 'div' );
270 dummy.textContent = html;
271 return dummy.innerHTML;
272 }
273
274 function stripHTML( text ) {
275 var tmp = document.createElement( 'div' );
276 tmp.innerHTML = text.replace( /<[^>]*>?/gm, '' );
277 return tmp.textContent;
278 }
279
280 return {
281 closest: closest,
282 matches: matches,
283 hide: hide,
284 show: show,
285 fadeIn: fadeIn,
286 fadeOut: fadeOut,
287 scrollToElement: scrollToElement,
288 getJSONAttribute: getJSONAttribute,
289 convertToPlainText: convertToPlainText,
290 stripHTML: stripHTML,
291 emitEvent: emitEvent,
292 isTouch: isTouch,
293 };
294 } )();
295
296 /////////////////////////////////////
297 // Carousel implementation
298 /////////////////////////////////////
299 function init() {
300 var commentInterval;
301 var screenPadding;
302 var originalOverflow;
303 var originalHOverflow;
304 var scrollPos;
305
306 var lastKnownLocationHash = '';
307 var isUserTyping = false;
308
309 var gallerySelector =
310 'div.gallery, div.tiled-gallery, ul.wp-block-gallery, ul.blocks-gallery-grid, ' +
311 'figure.wp-block-gallery.has-nested-images, div.wp-block-jetpack-tiled-gallery, a.single-image-gallery';
312
313 // Selector for items within a gallery or tiled gallery.
314 var galleryItemSelector =
315 '.gallery-item, .tiled-gallery-item, .blocks-gallery-item, ' + ' .tiled-gallery__item';
316
317 // Selector for all items including single images.
318 var itemSelector = galleryItemSelector + ', .wp-block-image';
319
320 var carousel = {};
321
322 var stat =
323 typeof wpcom !== 'undefined' && wpcom.carousel && wpcom.carousel.stat
324 ? wpcom.carousel.stat
325 : util.noop;
326
327 var pageview =
328 typeof wpcom !== 'undefined' && wpcom.carousel && wpcom.carousel.pageview
329 ? wpcom.carousel.pageview
330 : util.noop;
331
332 function handleKeyboardEvent( e ) {
333 if ( ! isUserTyping ) {
334 switch ( e.which ) {
335 case 38: // up
336 e.preventDefault();
337 carousel.overlay.scrollTop -= 100;
338 break;
339 case 40: // down
340 e.preventDefault();
341 carousel.overlay.scrollTop += 100;
342 break;
343 case 39: // right
344 e.preventDefault();
345 swiper.slideNext();
346 break;
347 case 37: // left
348 case 8: // backspace
349 e.preventDefault();
350 swiper.slidePrev();
351 break;
352 case 27: // escape
353 e.preventDefault();
354 closeCarousel();
355 break;
356 default:
357 break;
358 }
359 }
360 }
361
362 function disableKeyboardNavigation() {
363 isUserTyping = true;
364 }
365
366 function enableKeyboardNavigation() {
367 isUserTyping = false;
368 }
369
370 function calculatePadding() {
371 var baseScreenPadding = 110;
372 screenPadding = baseScreenPadding;
373
374 if ( window.innerWidth <= 760 ) {
375 screenPadding = Math.round( ( window.innerWidth / 760 ) * baseScreenPadding );
376
377 if ( screenPadding < 40 && domUtil.isTouch() ) {
378 screenPadding = 0;
379 }
380 }
381 }
382
383 function makeGalleryImageAccessible( img ) {
384 img.role = 'button';
385 img.tabIndex = 0;
386 img.ariaLabel = jetpackCarouselStrings.image_label;
387 }
388
389 function initializeCarousel() {
390 if ( ! carousel.overlay ) {
391 carousel.overlay = document.querySelector( '.jp-carousel-overlay' );
392 carousel.container = carousel.overlay.querySelector( '.jp-carousel-wrap' );
393 carousel.gallery = carousel.container.querySelector( '.jp-carousel' );
394 carousel.info = carousel.overlay.querySelector( '.jp-carousel-info' );
395 carousel.caption = carousel.info.querySelector( '.jp-carousel-caption' );
396 carousel.commentField = carousel.overlay.querySelector(
397 '#jp-carousel-comment-form-comment-field'
398 );
399 carousel.emailField = carousel.overlay.querySelector(
400 '#jp-carousel-comment-form-email-field'
401 );
402 carousel.authorField = carousel.overlay.querySelector(
403 '#jp-carousel-comment-form-author-field'
404 );
405 carousel.urlField = carousel.overlay.querySelector( '#jp-carousel-comment-form-url-field' );
406
407 calculatePadding();
408
409 [
410 carousel.commentField,
411 carousel.emailField,
412 carousel.authorField,
413 carousel.urlField,
414 ].forEach( function ( field ) {
415 if ( field ) {
416 field.addEventListener( 'focus', disableKeyboardNavigation );
417 field.addEventListener( 'blur', enableKeyboardNavigation );
418 }
419 } );
420
421 carousel.overlay.addEventListener( 'click', function ( e ) {
422 var target = e.target;
423 var isTargetCloseHint = !! domUtil.closest( target, '.jp-carousel-close-hint' );
424 var isSmallScreen = !! window.matchMedia( '(max-device-width: 760px)' ).matches;
425 if ( target === carousel.overlay ) {
426 if ( ! isSmallScreen ) {
427 closeCarousel();
428 }
429 } else if ( isTargetCloseHint ) {
430 closeCarousel();
431 } else if ( target.classList.contains( 'jp-carousel-image-download' ) ) {
432 stat( 'download_original_click' );
433 } else if ( target.classList.contains( 'jp-carousel-comment-login' ) ) {
434 handleCommentLoginClick( e );
435 } else if ( domUtil.closest( target, '#jp-carousel-comment-form-container' ) ) {
436 handleCommentFormClick( e );
437 } else if (
438 domUtil.closest( target, '.jp-carousel-photo-icons-container' ) ||
439 target.classList.contains( 'jp-carousel-photo-title' )
440 ) {
441 handleFooterElementClick( e );
442 }
443 } );
444
445 window.addEventListener( 'keydown', handleKeyboardEvent );
446
447 carousel.overlay.addEventListener( 'jp_carousel.afterOpen', function () {
448 enableKeyboardNavigation();
449
450 // Don't show navigation if there's only one image.
451 if ( carousel.slides.length <= 1 ) {
452 return;
453 }
454 // Show dot pagination if slide count is <= 5, otherwise show n/total.
455 if ( carousel.slides.length <= 5 ) {
456 domUtil.show( carousel.info.querySelector( '.jp-swiper-pagination' ) );
457 } else {
458 domUtil.show( carousel.info.querySelector( '.jp-carousel-pagination' ) );
459 }
460 } );
461
462 carousel.overlay.addEventListener( 'jp_carousel.beforeClose', function () {
463 disableKeyboardNavigation();
464
465 // Fixes some themes where closing carousel brings view back to top.
466 document.documentElement.style.removeProperty( 'height' );
467
468 // If we disable the swiper (because there's only one image)
469 // we have to re-enable it here again as Swiper doesn't, for some reason,
470 // show the navigation buttons again after reinitialization.
471 if ( swiper ) {
472 swiper.enable();
473 }
474
475 // Hide pagination.
476 domUtil.hide( carousel.info.querySelector( '.jp-swiper-pagination' ) );
477 domUtil.hide( carousel.info.querySelector( '.jp-carousel-pagination' ) );
478 } );
479
480 carousel.overlay.addEventListener( 'jp_carousel.afterClose', function () {
481 // don't force the browser back when the carousel closes.
482 if ( window.history.pushState ) {
483 history.pushState(
484 '',
485 document.title,
486 window.location.pathname + window.location.search
487 );
488 } else {
489 window.location.href = '';
490 }
491 lastKnownLocationHash = '';
492 carousel.isOpen = false;
493 } );
494
495 // Prevent native browser zooming
496 carousel.overlay.addEventListener( 'touchstart', function ( e ) {
497 if ( e.touches.length > 1 ) {
498 e.preventDefault();
499 }
500 } );
501 }
502 }
503
504 function handleCommentLoginClick() {
505 var slide = carousel.currentSlide;
506 var attachmentId = slide ? slide.attrs.attachmentId : '0';
507
508 window.location.href = jetpackCarouselStrings.login_url + '%23jp-carousel-' + attachmentId;
509 }
510
511 function updatePostResults( msg, isSuccess ) {
512 var results = carousel.overlay.querySelector( '#jp-carousel-comment-post-results' );
513 var elClass = 'jp-carousel-comment-post-' + ( isSuccess ? 'success' : 'error' );
514 results.innerHTML = '<span class="' + elClass + '">' + msg + '</span>';
515 domUtil.hide( carousel.overlay.querySelector( '#jp-carousel-comment-form-spinner' ) );
516 carousel.overlay
517 .querySelector( '#jp-carousel-comment-form' )
518 .classList.remove( 'jp-carousel-is-disabled' );
519 domUtil.show( results );
520 }
521
522 function handleCommentFormClick( e ) {
523 var target = e.target;
524 var data = domUtil.getJSONAttribute( carousel.container, 'data-carousel-extra' ) || {};
525 var attachmentId = carousel.currentSlide.attrs.attachmentId;
526
527 var wrapper = document.querySelector( '#jp-carousel-comment-form-submit-and-info-wrapper' );
528 var spinner = document.querySelector( '#jp-carousel-comment-form-spinner' );
529 // eslint-disable-next-line @wordpress/no-unused-vars-before-return
530 var submit = document.querySelector( '#jp-carousel-comment-form-button-submit' );
531 var form = document.querySelector( '#jp-carousel-comment-form' );
532
533 if (
534 carousel.commentField &&
535 carousel.commentField.getAttribute( 'id' ) === target.getAttribute( 'id' )
536 ) {
537 // For first page load
538 disableKeyboardNavigation();
539 domUtil.show( wrapper );
540 } else if ( domUtil.matches( target, 'input[type="submit"]' ) ) {
541 e.preventDefault();
542 e.stopPropagation();
543
544 domUtil.show( spinner );
545 form.classList.add( 'jp-carousel-is-disabled' );
546
547 var ajaxData = {
548 action: 'post_attachment_comment',
549 nonce: jetpackCarouselStrings.nonce,
550 blog_id: data.blog_id,
551 id: attachmentId,
552 comment: carousel.commentField.value,
553 };
554
555 if ( ! ajaxData.comment.length ) {
556 updatePostResults( jetpackCarouselStrings.no_comment_text, false );
557 return;
558 }
559
560 if ( Number( jetpackCarouselStrings.is_logged_in ) !== 1 ) {
561 ajaxData.email = carousel.emailField.value;
562 ajaxData.author = carousel.authorField.value;
563 ajaxData.url = carousel.urlField.value;
564
565 if ( Number( jetpackCarouselStrings.require_name_email ) === 1 ) {
566 if ( ! ajaxData.email.length || ! ajaxData.email.match( '@' ) ) {
567 updatePostResults( jetpackCarouselStrings.no_comment_email, false );
568 return;
569 } else if ( ! ajaxData.author.length ) {
570 updatePostResults( jetpackCarouselStrings.no_comment_author, false );
571 return;
572 }
573 }
574 }
575
576 var xhr = new XMLHttpRequest();
577 xhr.open( 'POST', jetpackCarouselStrings.ajaxurl, true );
578 xhr.setRequestHeader( 'X-Requested-With', 'XMLHttpRequest' );
579 xhr.setRequestHeader( 'Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8' );
580
581 xhr.onreadystatechange = function () {
582 if (
583 this.readyState === XMLHttpRequest.DONE &&
584 this.status >= 200 &&
585 this.status < 300
586 ) {
587 var response;
588 try {
589 response = JSON.parse( this.response );
590 } catch {
591 updatePostResults( jetpackCarouselStrings.comment_post_error, false );
592 return;
593 }
594 if ( response.comment_status === 'approved' ) {
595 updatePostResults( jetpackCarouselStrings.comment_approved, true );
596 } else if ( response.comment_status === 'unapproved' ) {
597 updatePostResults( jetpackCarouselStrings.comment_unapproved, true );
598 } else {
599 // 'deleted', 'spam', false
600 updatePostResults( jetpackCarouselStrings.comment_post_error, false );
601 }
602 clearCommentTextAreaValue();
603 fetchComments( attachmentId );
604 submit.value = jetpackCarouselStrings.post_comment;
605 domUtil.hide( spinner );
606 form.classList.remove( 'jp-carousel-is-disabled' );
607 } else {
608 // TODO: Add error handling and display here
609 updatePostResults( jetpackCarouselStrings.comment_post_error, false );
610 }
611 };
612
613 var params = [];
614 for ( var item in ajaxData ) {
615 if ( item ) {
616 // Encode each form element into a URI-compatible string.
617 var encoded = encodeURIComponent( item ) + '=' + encodeURIComponent( ajaxData[ item ] );
618 // In x-www-form-urlencoded, spaces should be `+`, not `%20`.
619 params.push( encoded.replace( /%20/g, '+' ) );
620 }
621 }
622 var encodedData = params.join( '&' );
623
624 xhr.send( encodedData );
625 }
626 }
627
628 /**
629 * Handles clicks to icons and other action elements in the icon container.
630 * @param {MouseEvent|TouchEvent|KeyBoardEvent} Event object.
631 */
632 function handleFooterElementClick( e ) {
633 e.preventDefault();
634
635 var target = e.target;
636 var extraInfoContainer = carousel.info.querySelector( '.jp-carousel-info-extra' );
637 var photoMetaContainer = carousel.info.querySelector( '.jp-carousel-image-meta' );
638 var commentsContainer = carousel.info.querySelector( '.jp-carousel-comments-wrapper' );
639 var infoIcon = carousel.info.querySelector( '.jp-carousel-icon-info' );
640 var commentsIcon = carousel.info.querySelector( '.jp-carousel-icon-comments' );
641
642 function handleInfoToggle() {
643 if ( commentsIcon ) {
644 commentsIcon.classList.remove( 'jp-carousel-selected' );
645 }
646 infoIcon.classList.toggle( 'jp-carousel-selected' );
647
648 if ( commentsContainer ) {
649 commentsContainer.classList.remove( 'jp-carousel-show' );
650 }
651 if ( photoMetaContainer ) {
652 photoMetaContainer.classList.toggle( 'jp-carousel-show' );
653 if ( photoMetaContainer.classList.contains( 'jp-carousel-show' ) ) {
654 extraInfoContainer.classList.add( 'jp-carousel-show' );
655 } else {
656 extraInfoContainer.classList.remove( 'jp-carousel-show' );
657 }
658 }
659 }
660
661 function handleCommentToggle() {
662 if ( infoIcon ) {
663 infoIcon.classList.remove( 'jp-carousel-selected' );
664 }
665 commentsIcon.classList.toggle( 'jp-carousel-selected' );
666
667 if ( photoMetaContainer ) {
668 photoMetaContainer.classList.remove( 'jp-carousel-show' );
669 }
670 if ( commentsContainer ) {
671 commentsContainer.classList.toggle( 'jp-carousel-show' );
672 if ( commentsContainer.classList.contains( 'jp-carousel-show' ) ) {
673 extraInfoContainer.classList.add( 'jp-carousel-show' );
674 } else {
675 extraInfoContainer.classList.remove( 'jp-carousel-show' );
676 }
677 }
678 }
679
680 if (
681 domUtil.closest( target, '.jp-carousel-icon-info' ) ||
682 target.classList.contains( 'jp-carousel-photo-title' )
683 ) {
684 if ( photoMetaContainer && photoMetaContainer.classList.contains( 'jp-carousel-show' ) ) {
685 domUtil.scrollToElement( carousel.overlay, carousel.overlay, handleInfoToggle );
686 } else {
687 handleInfoToggle();
688 domUtil.scrollToElement( carousel.info, carousel.overlay );
689 }
690 }
691
692 if ( domUtil.closest( target, '.jp-carousel-icon-comments' ) ) {
693 if ( commentsContainer && commentsContainer.classList.contains( 'jp-carousel-show' ) ) {
694 domUtil.scrollToElement( carousel.overlay, carousel.overlay, handleCommentToggle );
695 } else {
696 handleCommentToggle();
697 domUtil.scrollToElement( carousel.info, carousel.overlay );
698 }
699 }
700 }
701
702 function processSingleImageGallery() {
703 var images = document.querySelectorAll( 'a img[data-attachment-id]' );
704 Array.prototype.forEach.call( images, function ( image ) {
705 var link = image.parentElement;
706 var container = link.parentElement;
707
708 // Skip if image was already added to gallery by shortcode.
709 if ( container.classList.contains( 'gallery-icon' ) ) {
710 return;
711 }
712
713 // Skip if image is part of a gallery.
714 if ( domUtil.closest( container, galleryItemSelector ) ) {
715 return;
716 }
717
718 // Skip if the parent is not actually a link.
719 if ( ! link.hasAttribute( 'href' ) ) {
720 return;
721 }
722
723 var valid = false;
724
725 // If link points to 'Media File' (ignoring GET parameters) and flag is set, allow it.
726 if (
727 link.getAttribute( 'href' ).split( '?' )[ 0 ] ===
728 image.getAttribute( 'data-orig-file' ).split( '?' )[ 0 ] &&
729 Number( jetpackCarouselStrings.single_image_gallery_media_file ) === 1
730 ) {
731 valid = true;
732 }
733
734 // If link points to 'Attachment Page', allow it.
735 if ( link.getAttribute( 'href' ) === image.getAttribute( 'data-permalink' ) ) {
736 valid = true;
737 }
738
739 // Links to 'Custom URL' or 'Media File' when flag is not set are not valid.
740 if ( ! valid ) {
741 return;
742 }
743
744 makeGalleryImageAccessible( image );
745
746 // Make this node a gallery recognizable by event listener above.
747 link.classList.add( 'single-image-gallery' );
748 // blog_id is needed to allow posting comments to correct blog.
749 link.setAttribute(
750 'data-carousel-extra',
751 JSON.stringify( {
752 blog_id: Number( jetpackCarouselStrings.blog_id ),
753 } )
754 );
755 } );
756 }
757
758 function testForData( el ) {
759 return !! ( el && el.getAttribute( 'data-carousel-extra' ) );
760 }
761
762 function openOrSelectSlide( gal, index ) {
763 if ( ! carousel.isOpen ) {
764 // The `open` method selects the correct slide during the initialization.
765 loadSwiper( gal, { startIndex: index } );
766 } else {
767 selectSlideAtIndex( index );
768 // We have to force swiper to slide to the index onHasChange.
769 swiper.slideTo( index + 1 );
770 }
771 }
772
773 function selectSlideAtIndex( index ) {
774 if ( ! index || index < 0 || index > carousel.slides.length ) {
775 index = 0;
776 }
777 carousel.currentSlide = carousel.slides[ index ];
778
779 var current = carousel.currentSlide;
780 var attachmentId = current.attrs.attachmentId;
781
782 // Load current image immediately
783 loadFullImage( carousel.slides[ index ] );
784
785 // Preload adjacent images in background
786 preloadAdjacentImages( index );
787
788 if (
789 Number( jetpackCarouselStrings.display_background_image ) === 1 &&
790 ! carousel.slides[ index ].backgroundImage
791 ) {
792 loadBackgroundImage( carousel.slides[ index ] );
793 }
794
795 domUtil.hide( carousel.caption );
796 updateTitleCaptionAndDesc( {
797 caption: current.attrs.caption,
798 title: current.attrs.title,
799 desc: current.attrs.desc,
800 } );
801
802 var imageMeta = carousel.slides[ index ].attrs.imageMeta;
803 updateExif( imageMeta );
804 updateFullSizeLink( current );
805
806 if ( Number( jetpackCarouselStrings.display_comments ) === 1 ) {
807 testCommentsOpened( carousel.slides[ index ].attrs.commentsOpened );
808 fetchComments( attachmentId );
809 domUtil.hide( carousel.info.querySelector( '#jp-carousel-comment-post-results' ) );
810 }
811
812 // Update pagination in footer.
813 var pagination = carousel.info.querySelector( '.jp-carousel-pagination' );
814 if ( pagination && carousel.slides.length > 5 ) {
815 var currentPage = index + 1;
816 pagination.innerHTML = '<span>' + currentPage + ' / ' + carousel.slides.length + '</span>';
817 }
818
819 // Record pageview in WP Stats, for each new image loaded full-screen.
820 if ( jetpackCarouselStrings.stats && carousel.isOpen ) {
821 new Image().src =
822 document.location.protocol +
823 '//pixel.wp.com/g.gif?' +
824 jetpackCarouselStrings.stats +
825 '&post=' +
826 encodeURIComponent( attachmentId ) +
827 '&rand=' +
828 Math.random();
829 }
830
831 if ( carousel.isOpen ) {
832 pageview( attachmentId );
833 }
834
835 lastKnownLocationHash = '#jp-carousel-' + attachmentId;
836 window.location.hash = lastKnownLocationHash;
837 }
838
839 function restoreScroll() {
840 window.scrollTo( window.scrollX || window.pageXOffset || 0, scrollPos || 0 );
841 }
842
843 function closeCarousel() {
844 // Make sure to let the page scroll again.
845 document.body.style.overflow = originalOverflow;
846 document.documentElement.style.overflow = originalHOverflow;
847 clearCommentTextAreaValue();
848
849 disableKeyboardNavigation();
850
851 domUtil.emitEvent( carousel.overlay, 'jp_carousel.beforeClose' );
852 restoreScroll();
853 carousel.isOpen = false;
854 swiper.destroy();
855 // Clear slide data for DOM garbage collection.
856 carousel.slides = [];
857 carousel.currentSlide = undefined;
858 carousel.gallery.innerHTML = '';
859
860 domUtil.fadeOut( carousel.overlay, function () {
861 domUtil.emitEvent( carousel.overlay, 'jp_carousel.afterClose' );
862 } );
863 }
864
865 function calculateMaxSlideDimensions() {
866 return {
867 width: window.innerWidth,
868 height: window.innerHeight - 64, //subtract height of bottom info bar,
869 };
870 }
871
872 function sanitizePhotonUrl( url ) {
873 var urlObj;
874 try {
875 urlObj = new URL( url );
876 // eslint-disable-next-line no-unused-vars
877 } catch ( e ) {
878 return url;
879 }
880
881 var whitelistedPhotonArgs = [
882 'quality',
883 'ssl',
884 'filter',
885 'brightness',
886 'contrast',
887 'colorize',
888 'smooth',
889 ];
890
891 // Get all search params
892 var searchParams = Array.from( urlObj.searchParams.entries() );
893
894 // Clear all existing params
895 urlObj.search = '';
896
897 // Only add back whitelisted params
898 searchParams.forEach( ( [ key, value ] ) => {
899 if ( whitelistedPhotonArgs.includes( key ) ) {
900 urlObj.searchParams.append( key, value );
901 }
902 } );
903
904 return urlObj;
905 }
906
907 function selectBestImageUrl( args ) {
908 if ( typeof args !== 'object' ) {
909 args = {};
910 }
911
912 if ( typeof args.origFile === 'undefined' ) {
913 return '';
914 }
915
916 if ( typeof args.origWidth === 'undefined' || typeof args.maxWidth === 'undefined' ) {
917 return args.origFile;
918 }
919
920 if ( typeof args.largeFile === 'undefined' ) {
921 return args.origFile;
922 }
923
924 // Check if the image is being served by Photon (using a regular expression on the hostname).
925
926 var imageLinkParser = document.createElement( 'a' );
927 imageLinkParser.href = args.largeFile;
928
929 var isPhotonUrl = /^i[0-2]\.wp\.com$/i.test( imageLinkParser.hostname );
930
931 var largeSizeParts = getImageSizeParts( args.largeFile, args.origWidth, isPhotonUrl );
932
933 var largeWidth = parseInt( largeSizeParts[ 0 ], 10 );
934 var largeHeight = parseInt( largeSizeParts[ 1 ], 10 );
935
936 args.origMaxWidth = args.maxWidth;
937 args.origMaxHeight = args.maxHeight;
938
939 // Give devices with a higher devicePixelRatio higher-res images (Retina display = 2, Android phones = 1.5, etc)
940 if ( typeof window.devicePixelRatio !== 'undefined' && window.devicePixelRatio > 1 ) {
941 args.maxWidth = args.maxWidth * window.devicePixelRatio;
942 args.maxHeight = args.maxHeight * window.devicePixelRatio;
943 }
944
945 if ( largeWidth >= args.maxWidth || largeHeight >= args.maxHeight ) {
946 return args.largeFile;
947 }
948
949 if ( isPhotonUrl ) {
950 // args.origFile doesn't point to a Photon url, so in this case we use args.largeFile
951 // to return the photon url of the original image.
952 if ( args.largeFile.lastIndexOf( '?' ) === -1 ) {
953 return args.largeFile;
954 }
955
956 // Sanitize the URL to remove non-cosmetic changes like resize, fit, etc.
957 var sanitizedUrl = sanitizePhotonUrl( args.largeFile );
958
959 // If we have a really large image load a smaller version
960 // that is closer to the viewable size
961 if ( args.origWidth > args.maxWidth || args.origHeight > args.maxHeight ) {
962 // @2x the max sizes so we get a high enough resolution for zooming.
963 args.origMaxWidth = args.maxWidth * 2;
964 args.origMaxHeight = args.maxHeight * 2;
965 // Add the fit arg to the list of Photon args.
966 sanitizedUrl.searchParams.set( 'fit', args.origMaxWidth + ',' + args.origMaxHeight );
967 }
968
969 // Return a Photon URL image that's better fitted for the viewport.
970 return sanitizedUrl.toString();
971 }
972
973 return args.origFile;
974 }
975
976 function getImageSizeParts( file, origWidth, isPhotonUrl ) {
977 var size = isPhotonUrl
978 ? file.replace( /.*=([\d]+%2C[\d]+).*$/, '$1' )
979 : file.replace( /.*-([\d]+x[\d]+)\..+$/, '$1' );
980
981 var sizeParts;
982 if ( size !== file ) {
983 sizeParts = isPhotonUrl ? size.split( '%2C' ) : size.split( 'x' );
984 } else {
985 sizeParts = [ origWidth, 0 ];
986 }
987
988 // If one of the dimensions is set to 9999, then the actual value of that dimension can't be retrieved from the url.
989 // In that case, we set the value to 0.
990 if ( sizeParts[ 0 ] === '9999' ) {
991 sizeParts[ 0 ] = '0';
992 }
993
994 if ( sizeParts[ 1 ] === '9999' ) {
995 sizeParts[ 1 ] = '0';
996 }
997
998 return sizeParts;
999 }
1000
1001 /**
1002 * Returns a number in a fraction format that represents the shutter speed.
1003 * @param Number speed
1004 * @return String
1005 */
1006 function formatShutterSpeed( speed ) {
1007 var denominator;
1008
1009 // round to one decimal if value > 1s by multiplying it by 10, rounding, then dividing by 10 again
1010 if ( speed >= 1 ) {
1011 return Math.round( speed * 10 ) / 10 + 's';
1012 }
1013
1014 // If the speed is less than one, we find the denominator by inverting
1015 // the number. Since cameras usually use rational numbers as shutter
1016 // speeds, we should get a nice round number. Or close to one in cases
1017 // like 1/30. So we round it.
1018 denominator = Math.round( 1 / speed );
1019
1020 return '1/' + denominator + 's';
1021 }
1022
1023 function parseTitleOrDesc( value ) {
1024 if ( ! value.match( ' ' ) && value.match( '_' ) ) {
1025 return '';
1026 }
1027
1028 return value;
1029 }
1030
1031 function updateTitleCaptionAndDesc( data ) {
1032 var caption;
1033 var title;
1034 var desc;
1035 var captionMainElement;
1036 var captionInfoExtraElement;
1037 var titleElement;
1038 var descriptionElement;
1039
1040 captionMainElement = carousel.overlay.querySelector( '.jp-carousel-photo-caption' );
1041 captionInfoExtraElement = carousel.overlay.querySelector( '.jp-carousel-caption' );
1042
1043 titleElement = carousel.overlay.querySelector( '.jp-carousel-photo-title' );
1044 descriptionElement = carousel.overlay.querySelector( '.jp-carousel-photo-description' );
1045
1046 domUtil.hide( captionMainElement );
1047 domUtil.hide( captionInfoExtraElement );
1048 domUtil.hide( titleElement );
1049 domUtil.hide( descriptionElement );
1050
1051 caption = parseTitleOrDesc( data.caption ) || '';
1052 title = parseTitleOrDesc( data.title ) || '';
1053 desc = parseTitleOrDesc( data.desc ) || '';
1054
1055 if ( caption || title || desc ) {
1056 if ( caption ) {
1057 captionMainElement.innerHTML = caption;
1058 captionInfoExtraElement.innerHTML = caption;
1059
1060 domUtil.show( captionMainElement );
1061 domUtil.show( captionInfoExtraElement );
1062 }
1063
1064 if ( domUtil.stripHTML( caption ) === domUtil.stripHTML( title ) ) {
1065 title = '';
1066 }
1067
1068 if ( domUtil.stripHTML( caption ) === domUtil.stripHTML( desc ) ) {
1069 desc = '';
1070 }
1071
1072 if ( domUtil.stripHTML( title ) === domUtil.stripHTML( desc ) ) {
1073 desc = '';
1074 }
1075
1076 if ( desc ) {
1077 descriptionElement.innerHTML = desc;
1078 domUtil.show( descriptionElement );
1079
1080 if ( ! title && ! caption ) {
1081 captionMainElement.textContent = domUtil.stripHTML( desc );
1082 domUtil.show( captionMainElement );
1083 }
1084 }
1085
1086 if ( title ) {
1087 var plainTitle = domUtil.stripHTML( title );
1088 titleElement.textContent = plainTitle;
1089
1090 if ( ! caption ) {
1091 captionMainElement.textContent = plainTitle;
1092 captionInfoExtraElement.textContent = plainTitle;
1093
1094 domUtil.show( captionMainElement );
1095 }
1096
1097 domUtil.show( titleElement );
1098 }
1099 }
1100 }
1101
1102 // updateExif updates the contents of the exif UL (.jp-carousel-image-exif)
1103 function updateExif( meta ) {
1104 if ( ! meta || Number( jetpackCarouselStrings.display_exif ) !== 1 ) {
1105 return false;
1106 }
1107
1108 var ul = carousel.info.querySelector( '.jp-carousel-image-meta ul.jp-carousel-image-exif' );
1109 var html = '';
1110
1111 for ( var key in meta ) {
1112 var val = meta[ key ];
1113 var metaKeys = jetpackCarouselStrings.meta_data || [];
1114
1115 if ( parseFloat( val ) === 0 || ! val.length || metaKeys.indexOf( key ) === -1 ) {
1116 continue;
1117 }
1118
1119 switch ( key ) {
1120 case 'focal_length':
1121 val = val + 'mm';
1122 break;
1123 case 'shutter_speed':
1124 val = formatShutterSpeed( val );
1125 break;
1126 case 'aperture':
1127 val = 'f/' + val;
1128 break;
1129 }
1130
1131 html += '<li><h5>' + jetpackCarouselStrings[ key ] + '</h5>' + val + '</li>';
1132 }
1133
1134 ul.innerHTML = html;
1135 ul.style.removeProperty( 'display' );
1136 }
1137
1138 // Update the contents of the jp-carousel-image-download link
1139 function updateFullSizeLink( currentSlide ) {
1140 if ( ! currentSlide ) {
1141 return false;
1142 }
1143 var original;
1144 var origSize = [ currentSlide.attrs.origWidth, currentSlide.attrs.origHeight ];
1145 var imageLinkParser = document.createElement( 'a' );
1146
1147 imageLinkParser.href = currentSlide.attrs.src.replace( /\?.+$/, '' );
1148
1149 // Is this a Photon URL?
1150 if ( imageLinkParser.hostname.match( /^i[\d]{1}\.wp\.com$/i ) !== null ) {
1151 original = imageLinkParser.href;
1152 } else {
1153 original = currentSlide.attrs.origFile.replace( /\?.+$/, '' );
1154 }
1155
1156 var downloadText = carousel.info.querySelector( '.jp-carousel-download-text' );
1157 var permalink = carousel.info.querySelector( '.jp-carousel-image-download' );
1158
1159 downloadText.innerHTML = util.applyReplacements(
1160 jetpackCarouselStrings.download_original,
1161 origSize
1162 );
1163 permalink.setAttribute( 'href', original );
1164 permalink.style.removeProperty( 'display' );
1165 }
1166
1167 function testCommentsOpened( opened ) {
1168 var commentForm = carousel.info.querySelector( '#jp-carousel-comment-form-container' );
1169 var isOpened = parseInt( opened, 10 ) === 1;
1170
1171 if ( isOpened ) {
1172 domUtil.fadeIn( commentForm );
1173 } else {
1174 domUtil.fadeOut( commentForm );
1175 }
1176 }
1177
1178 function fetchComments( attachmentId, offset ) {
1179 var shouldClear = offset === undefined;
1180 var commentsIndicator = carousel.info.querySelector(
1181 '.jp-carousel-icon-comments .jp-carousel-has-comments-indicator'
1182 );
1183
1184 commentsIndicator.classList.remove( 'jp-carousel-show' );
1185
1186 clearInterval( commentInterval );
1187
1188 if ( ! attachmentId ) {
1189 return;
1190 }
1191
1192 if ( ! offset || offset < 1 ) {
1193 offset = 0;
1194 }
1195
1196 var comments = carousel.info.querySelector( '.jp-carousel-comments' );
1197 var commentsLoading = carousel.info.querySelector( '#jp-carousel-comments-loading' );
1198 domUtil.show( commentsLoading );
1199
1200 if ( shouldClear ) {
1201 domUtil.hide( comments );
1202 comments.innerHTML = '';
1203 }
1204
1205 var xhr = new XMLHttpRequest();
1206 var url =
1207 jetpackCarouselStrings.ajaxurl +
1208 '?action=get_attachment_comments' +
1209 '&nonce=' +
1210 jetpackCarouselStrings.nonce +
1211 '&id=' +
1212 attachmentId +
1213 '&offset=' +
1214 offset;
1215 xhr.open( 'GET', url );
1216 xhr.setRequestHeader( 'X-Requested-With', 'XMLHttpRequest' );
1217
1218 var onError = function () {
1219 domUtil.fadeIn( comments );
1220 domUtil.fadeOut( commentsLoading );
1221 };
1222
1223 xhr.onload = function () {
1224 // Ignore the results if they arrive late and we're now on a different slide.
1225 if (
1226 ! carousel.currentSlide ||
1227 carousel.currentSlide.attrs.attachmentId !== attachmentId
1228 ) {
1229 return;
1230 }
1231
1232 var isSuccess = xhr.status >= 200 && xhr.status < 300;
1233 var data;
1234 try {
1235 data = JSON.parse( xhr.responseText );
1236 } catch {
1237 // Do nothing.
1238 }
1239
1240 if ( ! isSuccess || ! data || ! Array.isArray( data ) ) {
1241 return onError();
1242 }
1243
1244 if ( shouldClear ) {
1245 comments.innerHTML = '';
1246 }
1247
1248 for ( var i = 0; i < data.length; i++ ) {
1249 var entry = data[ i ];
1250 var comment = document.createElement( 'div' );
1251 comment.classList.add( 'jp-carousel-comment' );
1252 comment.setAttribute( 'id', 'jp-carousel-comment-' + entry.id );
1253 comment.innerHTML =
1254 '<div class="comment-gravatar">' +
1255 entry.gravatar_markup +
1256 '</div>' +
1257 '<div class="comment-content">' +
1258 '<div class="comment-author">' +
1259 entry.author_markup +
1260 '</div>' +
1261 '<div class="comment-date">' +
1262 entry.date_gmt +
1263 '</div>' +
1264 entry.content +
1265 '</div>';
1266 comments.appendChild( comment );
1267
1268 // Set the interval to check for a new page of comments.
1269 clearInterval( commentInterval );
1270 commentInterval = setInterval( function () {
1271 if ( carousel.container.scrollTop + 150 > window.innerHeight ) {
1272 fetchComments( attachmentId, offset + 10 );
1273 clearInterval( commentInterval );
1274 }
1275 }, 300 );
1276 }
1277
1278 if ( data.length > 0 ) {
1279 domUtil.show( comments );
1280 commentsIndicator.innerText = data.length;
1281 commentsIndicator.classList.add( 'jp-carousel-show' );
1282 }
1283
1284 domUtil.hide( commentsLoading );
1285 };
1286
1287 xhr.onerror = onError;
1288
1289 xhr.send();
1290 }
1291
1292 function loadFullImage( slide ) {
1293 var el = slide.el;
1294 var attrs = slide.attrs;
1295 var image = el.querySelector( 'img' );
1296
1297 if ( ! image.hasAttribute( 'data-loaded' ) ) {
1298 var hasPreview = !! attrs.previewImage;
1299 var thumbSize = attrs.thumbSize;
1300
1301 if ( ! hasPreview || ( thumbSize && el.offsetWidth > thumbSize.width ) ) {
1302 image.src = attrs.src;
1303 } else {
1304 image.src = attrs.previewImage;
1305 }
1306
1307 image.setAttribute( 'itemprop', 'image' );
1308 image.setAttribute( 'data-loaded', 1 );
1309 }
1310 }
1311
1312 function preloadAdjacentImages( currentIndex ) {
1313 var indicesToPreload = [];
1314 var totalSlides = carousel.slides.length;
1315
1316 // Only preload adjacent images if we have more than one slide (matching loop condition)
1317 if ( totalSlides > 1 ) {
1318 // Previous image (with loop handling)
1319 var prevIndex = currentIndex > 0 ? currentIndex - 1 : totalSlides - 1;
1320 indicesToPreload.push( prevIndex );
1321
1322 // Next image (with loop handling)
1323 var nextIndex = currentIndex < totalSlides - 1 ? currentIndex + 1 : 0;
1324 indicesToPreload.push( nextIndex );
1325 }
1326
1327 indicesToPreload.forEach( function ( index ) {
1328 var slide = carousel.slides[ index ];
1329 if ( slide ) {
1330 // Load in background without showing
1331 loadFullImage( slide );
1332
1333 // Also load background image if enabled
1334 if (
1335 Number( jetpackCarouselStrings.display_background_image ) === 1 &&
1336 ! slide.backgroundImage
1337 ) {
1338 loadBackgroundImage( slide );
1339 }
1340 }
1341 } );
1342 }
1343
1344 function loadBackgroundImage( slide ) {
1345 var currentSlide = slide.el;
1346
1347 if ( swiper && swiper.slides ) {
1348 currentSlide = swiper.slides[ swiper.activeIndex ];
1349 }
1350
1351 var image = slide.attrs.originalElement;
1352 var isLoaded = image.complete && image.naturalHeight !== 0;
1353
1354 if ( isLoaded ) {
1355 applyBackgroundImage( slide, currentSlide, image );
1356 return;
1357 }
1358
1359 image.onload = function () {
1360 applyBackgroundImage( slide, currentSlide, image );
1361 };
1362 }
1363
1364 function applyBackgroundImage( slide, currentSlide, image ) {
1365 var url = util.getBackgroundImage( image );
1366 slide.backgroundImage = url;
1367 currentSlide.style.backgroundImage = 'url(' + url + ')';
1368 currentSlide.style.backgroundSize = 'cover';
1369 }
1370
1371 function clearCommentTextAreaValue() {
1372 if ( carousel.commentField ) {
1373 carousel.commentField.value = '';
1374 }
1375 }
1376
1377 function getOriginalDimensions( el ) {
1378 var size = el.getAttribute( 'data-orig-size' ) || '';
1379
1380 if ( size ) {
1381 var parts = size.split( ',' );
1382 return { width: parseInt( parts[ 0 ], 10 ), height: parseInt( parts[ 1 ], 10 ) };
1383 }
1384 return {
1385 width: el.getAttribute( 'data-original-width' ) || el.getAttribute( 'width' ) || undefined,
1386 height:
1387 el.getAttribute( 'data-original-height' ) || el.getAttribute( 'height' ) || undefined,
1388 };
1389 }
1390
1391 function initCarouselSlides( items, startIndex ) {
1392 carousel.slides = [];
1393
1394 var max = calculateMaxSlideDimensions();
1395
1396 // If the startIndex is not 0 then preload the clicked image first.
1397 if ( startIndex !== 0 && items[ startIndex ].getAttribute( 'data-gallery-src' ) !== null ) {
1398 var img = new Image();
1399 img.src = items[ startIndex ].getAttribute( 'data-gallery-src' );
1400 }
1401
1402 var useInPageThumbnails = !! domUtil.closest( items[ 0 ], '.tiled-gallery.type-rectangular' );
1403
1404 // create the 'slide'
1405 Array.prototype.forEach.call( items, function ( item, i ) {
1406 var permalinkEl = domUtil.closest( item, 'a' );
1407 var origFile = item.getAttribute( 'data-orig-file' ) || item.getAttribute( 'src-orig' );
1408 var attrID =
1409 item.getAttribute( 'data-attachment-id' ) || item.getAttribute( 'data-id' ) || '0';
1410 var caption = document.querySelector(
1411 'img[data-attachment-id="' + attrID + '"] + figcaption'
1412 );
1413
1414 if ( caption ) {
1415 caption = caption.innerHTML;
1416 } else {
1417 caption = item.getAttribute( 'data-image-caption' );
1418 }
1419
1420 var attrs = {
1421 originalElement: item,
1422 attachmentId: attrID,
1423 commentsOpened: item.getAttribute( 'data-comments-opened' ) || '0',
1424 imageMeta: domUtil.getJSONAttribute( item, 'data-image-meta' ) || {},
1425 title: item.getAttribute( 'data-image-title' ) || '',
1426 desc: item.getAttribute( 'data-image-description' ) || '',
1427 largeFile: item.getAttribute( 'data-large-file' ) || '',
1428 origFile: origFile || '',
1429 thumbSize: { width: item.naturalWidth, height: item.naturalHeight },
1430 caption: caption || '',
1431 permalink: permalinkEl && permalinkEl.getAttribute( 'href' ),
1432 src: origFile || item.getAttribute( 'src' ) || '',
1433 };
1434
1435 var tiledGalleryItem = domUtil.closest( item, '.tiled-gallery-item' );
1436 var tiledCaptionEl =
1437 tiledGalleryItem && tiledGalleryItem.querySelector( '.tiled-gallery-caption' );
1438 var tiledCaption = tiledCaptionEl && tiledCaptionEl.innerHTML;
1439 if ( tiledCaption ) {
1440 attrs.caption = tiledCaption;
1441 }
1442
1443 var origDimensions = getOriginalDimensions( item );
1444
1445 attrs.origWidth = origDimensions.width || attrs.thumbSize.width;
1446 attrs.origHeight = origDimensions.height || attrs.thumbSize.height;
1447
1448 if ( typeof wpcom !== 'undefined' && wpcom.carousel && wpcom.carousel.generateImgSrc ) {
1449 attrs.src = wpcom.carousel.generateImgSrc( item, max );
1450 } else {
1451 attrs.src = selectBestImageUrl( {
1452 origFile: attrs.src,
1453 origWidth: attrs.origWidth,
1454 origHeight: attrs.origHeight,
1455 maxWidth: max.width,
1456 maxHeight: max.height,
1457 largeFile: attrs.largeFile,
1458 } );
1459 }
1460
1461 // Set the final src.
1462 item.setAttribute( 'data-gallery-src', attrs.src );
1463
1464 if ( attrs.attachmentId !== '0' ) {
1465 attrs.title = util.texturize( attrs.title );
1466 attrs.desc = util.texturize( attrs.desc );
1467 attrs.caption = util.texturize( attrs.caption );
1468
1469 // Initially, the image is a 1x1 transparent gif.
1470 // The preview is shown as a background image on the slide itself.
1471 var image = new Image();
1472
1473 var slideEl = document.createElement( 'div' );
1474 slideEl.classList.add( 'swiper-slide' );
1475 slideEl.setAttribute( 'itemprop', 'associatedMedia' );
1476 slideEl.setAttribute( 'itemscope', '' );
1477 slideEl.setAttribute( 'itemtype', 'https://schema.org/ImageObject' );
1478 var zoomEl = document.createElement( 'div' );
1479 zoomEl.classList.add( 'swiper-zoom-container' );
1480
1481 carousel.gallery.appendChild( slideEl );
1482
1483 slideEl.appendChild( zoomEl );
1484 zoomEl.appendChild( image );
1485 slideEl.setAttribute( 'data-attachment-id', attrs.attachmentId );
1486 slideEl.setAttribute( 'data-permalink', attrs.permalink );
1487 slideEl.setAttribute( 'data-orig-file', attrs.origFile );
1488
1489 if ( useInPageThumbnails ) {
1490 // Use the image already loaded in the gallery as a preview.
1491 attrs.previewImage = attrs.src;
1492 }
1493
1494 var slide = { el: slideEl, attrs: attrs, index: i };
1495 carousel.slides.push( slide );
1496 }
1497 } );
1498 }
1499
1500 function loadSwiper( gallery, options ) {
1501 if ( ! window.JetpackSwiper ) {
1502 var loader = document.querySelector( '#jp-carousel-loading-overlay' );
1503 domUtil.show( loader );
1504 var jsScript = document.createElement( 'script' );
1505 jsScript.id = 'jetpack-carousel-swiper-js';
1506 jsScript.src = window.jetpackSwiperLibraryPath.url;
1507 jsScript.async = true;
1508 jsScript.onload = function () {
1509 domUtil.hide( loader );
1510 openCarousel( gallery, options );
1511 };
1512 jsScript.onerror = function () {
1513 domUtil.hide( loader );
1514 };
1515 document.head.appendChild( jsScript );
1516 return;
1517 }
1518 openCarousel( gallery, options );
1519 }
1520
1521 function openCarousel( gallery, options ) {
1522 var settings = {
1523 imgSelector:
1524 '.gallery-item [data-attachment-id], .tiled-gallery-item [data-attachment-id], img[data-attachment-id], img[data-id]',
1525 startIndex: 0,
1526 };
1527
1528 var data = domUtil.getJSONAttribute( gallery, 'data-carousel-extra' );
1529 var tapTimeout;
1530
1531 if ( ! data ) {
1532 return; // don't run if the default gallery functions weren't used
1533 }
1534
1535 const images = gallery.querySelectorAll( settings.imgSelector );
1536 if ( ! images.length ) {
1537 return; // don't run if we found no images in the gallery (somehow it has images that aren't in the media library?)
1538 }
1539
1540 initializeCarousel();
1541
1542 if ( carousel.isOpen ) {
1543 return; // don't open if already opened
1544 }
1545 carousel.isOpen = true;
1546
1547 // make sure to stop the page from scrolling behind the carousel overlay, so we don't trigger
1548 // infiniscroll for it when enabled (Reader, theme infiniscroll, etc).
1549 originalOverflow = getComputedStyle( document.body ).overflow;
1550 document.body.style.overflow = 'hidden';
1551 // prevent html from overflowing on some of the new themes.
1552 originalHOverflow = getComputedStyle( document.documentElement ).overflow;
1553 document.documentElement.style.overflow = 'hidden';
1554 scrollPos = window.scrollY || window.pageYOffset || 0;
1555
1556 carousel.container.setAttribute( 'data-carousel-extra', JSON.stringify( data ) );
1557 stat( [ 'open', 'view_image' ] );
1558
1559 // If options exist, lets merge them
1560 // with our default settings
1561 for ( var option in options || {} ) {
1562 settings[ option ] = options[ option ];
1563 }
1564
1565 if ( settings.startIndex === -1 ) {
1566 settings.startIndex = 0; // -1 returned if can't find index, so start from beginning
1567 }
1568
1569 domUtil.emitEvent( carousel.overlay, 'jp_carousel.beforeOpen' );
1570 carousel.gallery.innerHTML = '';
1571
1572 // Need to set the overlay manually to block or swiper does't initialise properly.
1573 carousel.overlay.style.opacity = 1;
1574 carousel.overlay.style.display = 'block';
1575
1576 initCarouselSlides( images, settings.startIndex );
1577
1578 swiper = new window.JetpackSwiper( '.jp-carousel-swiper-container', {
1579 centeredSlides: true,
1580 zoom: true,
1581 loop: carousel.slides.length > 1,
1582 // Turn off interactions and hide navigation arrows if there is only one slide.
1583 enabled: carousel.slides.length > 1,
1584 pagination: {
1585 el: '.jp-swiper-pagination',
1586 clickable: true,
1587 },
1588 navigation: {
1589 nextEl: '.jp-swiper-button-next',
1590 prevEl: '.jp-swiper-button-prev',
1591 },
1592 initialSlide: settings.startIndex,
1593 on: {
1594 init: function () {
1595 selectSlideAtIndex( settings.startIndex );
1596 },
1597 },
1598 preventClicks: false,
1599 preventClicksPropagation: false,
1600 preventInteractionOnTransition: ! domUtil.isTouch(),
1601 threshold: 5,
1602 } );
1603
1604 swiper.on( 'slideChange', function ( swiper ) {
1605 if ( ! carousel.isOpen ) {
1606 return;
1607 }
1608 selectSlideAtIndex( swiper.realIndex );
1609 carousel.overlay.classList.remove( 'jp-carousel-hide-controls' );
1610 } );
1611
1612 swiper.on( 'zoomChange', function ( swiper, scale ) {
1613 if ( scale > 1 ) {
1614 carousel.overlay.classList.add( 'jp-carousel-hide-controls' );
1615 }
1616
1617 if ( scale === 1 ) {
1618 carousel.overlay.classList.remove( 'jp-carousel-hide-controls' );
1619 }
1620 } );
1621
1622 swiper.on( 'doubleTap', function ( swiper ) {
1623 clearTimeout( tapTimeout );
1624 if ( swiper.zoom.scale === 1 ) {
1625 var zoomTimeout = setTimeout( function () {
1626 carousel.overlay.classList.remove( 'jp-carousel-hide-controls' );
1627 clearTimeout( zoomTimeout );
1628 }, 150 );
1629 }
1630 } );
1631
1632 swiper.on( 'tap', function () {
1633 if ( swiper.zoom.scale > 1 ) {
1634 tapTimeout = setTimeout( function () {
1635 carousel.overlay.classList.toggle( 'jp-carousel-hide-controls' );
1636 }, 150 );
1637 }
1638 } );
1639
1640 domUtil.fadeIn( carousel.overlay, function () {
1641 domUtil.emitEvent( carousel.overlay, 'jp_carousel.afterOpen' );
1642 } );
1643 }
1644
1645 // Register the event listeners for starting the gallery
1646 document.body.addEventListener( 'click', handleInteraction );
1647 document.body.addEventListener( 'keydown', handleInteraction );
1648 document.querySelectorAll( galleryItemSelector + 'img' ).forEach( function ( galleryImage ) {
1649 if ( shouldOpenModal( galleryImage ) ) {
1650 makeGalleryImageAccessible( galleryImage );
1651 }
1652 } );
1653
1654 function handleInteraction( e ) {
1655 if ( e.type === 'click' ) {
1656 handleClick( e );
1657 return;
1658 }
1659
1660 if ( e.type === 'keydown' ) {
1661 const parentElement = document.activeElement.parentElement;
1662 const isParentCarouselContainer =
1663 parentElement && parentElement.classList.contains( 'tiled-gallery__item' );
1664
1665 if ( ( e.key === ' ' || e.key === 'Enter' ) && isParentCarouselContainer ) {
1666 handleClick( e );
1667 }
1668 }
1669 }
1670
1671 function shouldOpenModal( el ) {
1672 var parent = el.parentElement;
1673 var grandparent = parent.parentElement;
1674
1675 // If Gallery is made up of individual Image blocks check for custom link before
1676 // loading carousel. The custom link may be the parent or could be a descendant
1677 // of the parent if the image has rounded corners.
1678 var parentHref = null;
1679 if ( grandparent && grandparent.classList.contains( 'wp-block-image' ) ) {
1680 parentHref = parent.getAttribute( 'href' );
1681 } else if (
1682 parent &&
1683 parent.classList.contains( 'wp-block-image' ) &&
1684 parent.querySelector( ':scope > a' )
1685 ) {
1686 parentHref = parent.querySelector( ':scope > a' ).getAttribute( 'href' );
1687 }
1688
1689 // If the link does not point to the attachment or media file then assume Image has
1690 // a custom link so don't load the carousel.
1691 if (
1692 parentHref &&
1693 parentHref.split( '?' )[ 0 ] !== el.getAttribute( 'data-orig-file' ).split( '?' )[ 0 ] &&
1694 parentHref !== el.getAttribute( 'data-permalink' )
1695 ) {
1696 return false;
1697 }
1698
1699 // Do not open the modal if we are looking at a gallery caption from before WP5, which may contain a link.
1700 if ( parent.classList.contains( 'gallery-caption' ) ) {
1701 return false;
1702 }
1703
1704 // Do not open the modal if we are looking at a caption of a gallery block, which may contain a link.
1705 if ( domUtil.matches( parent, 'figcaption' ) ) {
1706 return false;
1707 }
1708
1709 return true;
1710 }
1711
1712 function handleClick( e ) {
1713 var isCompatible =
1714 window.CSS && window.CSS.supports && window.CSS.supports( 'display', 'grid' );
1715
1716 // IE11 support is being dropped in August 2021. The new swiper.js libray is not IE11 compat
1717 // so just default to opening individual image attachment/media pages for IE.
1718 if ( ! isCompatible ) {
1719 return;
1720 }
1721
1722 var target = e.target;
1723 var gallery = domUtil.closest( target, gallerySelector );
1724
1725 if ( gallery ) {
1726 if ( ! testForData( gallery ) ) {
1727 return;
1728 }
1729
1730 if ( ! shouldOpenModal( target ) ) {
1731 return;
1732 }
1733
1734 // Set height to auto.
1735 // Fix some themes where closing carousel brings view back to top.
1736 document.documentElement.style.height = 'auto';
1737
1738 e.preventDefault();
1739
1740 // Stopping propagation in case there are parent elements
1741 // with .gallery or .tiled-gallery class
1742 e.stopPropagation();
1743
1744 var item = domUtil.closest( target, itemSelector );
1745 var index = Array.prototype.indexOf.call( gallery.querySelectorAll( itemSelector ), item );
1746 loadSwiper( gallery, { startIndex: index } );
1747 }
1748 }
1749
1750 // Handle lightbox (single image gallery) for images linking to 'Attachment Page'.
1751 if ( Number( jetpackCarouselStrings.single_image_gallery ) === 1 ) {
1752 processSingleImageGallery();
1753 document.body.addEventListener( 'is.post-load', function () {
1754 processSingleImageGallery();
1755 } );
1756 }
1757
1758 // Makes carousel work on page load and when back button leads to same URL with carousel hash
1759 // (i.e. no actual document.ready trigger).
1760 window.addEventListener( 'hashchange', function () {
1761 var hashRegExp = /jp-carousel-(\d+)/;
1762
1763 if ( ! window.location.hash || ! hashRegExp.test( window.location.hash ) ) {
1764 if ( carousel.isOpen ) {
1765 closeCarousel();
1766 }
1767
1768 return;
1769 }
1770
1771 if ( window.location.hash === lastKnownLocationHash && carousel.isOpen ) {
1772 return;
1773 }
1774
1775 if ( window.location.hash && carousel.gallery && ! carousel.isOpen && history.back ) {
1776 history.back();
1777 return;
1778 }
1779
1780 lastKnownLocationHash = window.location.hash;
1781 var matchList = window.location.hash.match( hashRegExp );
1782 var attachmentId = parseInt( matchList[ 1 ], 10 );
1783 var galleries = document.querySelectorAll( gallerySelector );
1784
1785 // Find the first thumbnail that matches the attachment ID in the location
1786 // hash, then open the gallery that contains it.
1787 for ( var i = 0; i < galleries.length; i++ ) {
1788 var gallery = galleries[ i ];
1789 var selected;
1790
1791 var images = gallery.querySelectorAll( 'img' );
1792 for ( var j = 0; j < images.length; j++ ) {
1793 if (
1794 parseInt( images[ j ].getAttribute( 'data-attachment-id' ), 10 ) === attachmentId ||
1795 parseInt( images[ j ].getAttribute( 'data-id' ), 10 ) === attachmentId
1796 ) {
1797 selected = j;
1798 break;
1799 }
1800 }
1801
1802 if ( selected !== undefined ) {
1803 openOrSelectSlide( gallery, selected );
1804 break;
1805 }
1806 }
1807 } );
1808
1809 if ( window.location.hash ) {
1810 domUtil.emitEvent( window, 'hashchange' );
1811 }
1812 }
1813
1814 if ( document.readyState !== 'loading' ) {
1815 init();
1816 } else {
1817 document.addEventListener( 'DOMContentLoaded', init );
1818 }
1819 } )();
1820