PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 16.1-a.1
Jetpack – WP Security, Backup, Speed, & Growth v16.1-a.1
16.0.1 16.1-a.1 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 3 days ago jetpack-carousel.php 1 month ago
jetpack-carousel.js
1835 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 // When there's no large file to fall back on (e.g. images that weren't enriched with
921 // Jetpack's data-large-file attribute), use the original file. A missing attribute is
922 // read as an empty string, so we can't only guard against `undefined` here: otherwise a
923 // narrow (portrait, mobile) viewport would return that empty string as the image source,
924 // leaving the carousel with a blank slide.
925 if ( ! args.largeFile ) {
926 return args.origFile;
927 }
928
929 // Check if the image is being served by Photon (using a regular expression on the hostname).
930
931 var imageLinkParser = document.createElement( 'a' );
932 imageLinkParser.href = args.largeFile;
933
934 var isPhotonUrl = /^i[0-2]\.wp\.com$/i.test( imageLinkParser.hostname );
935
936 var largeSizeParts = getImageSizeParts( args.largeFile, args.origWidth, isPhotonUrl );
937
938 var largeWidth = parseInt( largeSizeParts[ 0 ], 10 );
939 var largeHeight = parseInt( largeSizeParts[ 1 ], 10 );
940
941 args.origMaxWidth = args.maxWidth;
942 args.origMaxHeight = args.maxHeight;
943
944 // Give devices with a higher devicePixelRatio higher-res images (Retina display = 2, Android phones = 1.5, etc)
945 if ( typeof window.devicePixelRatio !== 'undefined' && window.devicePixelRatio > 1 ) {
946 args.maxWidth = args.maxWidth * window.devicePixelRatio;
947 args.maxHeight = args.maxHeight * window.devicePixelRatio;
948 }
949
950 if ( largeWidth >= args.maxWidth || largeHeight >= args.maxHeight ) {
951 return args.largeFile;
952 }
953
954 if ( isPhotonUrl ) {
955 // args.origFile doesn't point to a Photon url, so in this case we use args.largeFile
956 // to return the photon url of the original image.
957 if ( args.largeFile.lastIndexOf( '?' ) === -1 ) {
958 return args.largeFile;
959 }
960
961 // Sanitize the URL to remove non-cosmetic changes like resize, fit, etc.
962 var sanitizedUrl = sanitizePhotonUrl( args.largeFile );
963
964 // If we have a really large image load a smaller version
965 // that is closer to the viewable size
966 if ( args.origWidth > args.maxWidth || args.origHeight > args.maxHeight ) {
967 // @2x the max sizes so we get a high enough resolution for zooming.
968 args.origMaxWidth = args.maxWidth * 2;
969 args.origMaxHeight = args.maxHeight * 2;
970 // Add the fit arg to the list of Photon args.
971 sanitizedUrl.searchParams.set( 'fit', args.origMaxWidth + ',' + args.origMaxHeight );
972 }
973
974 // Return a Photon URL image that's better fitted for the viewport.
975 return sanitizedUrl.toString();
976 }
977
978 return args.origFile;
979 }
980
981 function getImageSizeParts( file, origWidth, isPhotonUrl ) {
982 var size = isPhotonUrl
983 ? file.replace( /.*=([\d]+%2C[\d]+).*$/, '$1' )
984 : file.replace( /.*-([\d]+x[\d]+)\..+$/, '$1' );
985
986 var sizeParts;
987 if ( size !== file ) {
988 sizeParts = isPhotonUrl ? size.split( '%2C' ) : size.split( 'x' );
989 } else {
990 sizeParts = [ origWidth, 0 ];
991 }
992
993 // If one of the dimensions is set to 9999, then the actual value of that dimension can't be retrieved from the url.
994 // In that case, we set the value to 0.
995 if ( sizeParts[ 0 ] === '9999' ) {
996 sizeParts[ 0 ] = '0';
997 }
998
999 if ( sizeParts[ 1 ] === '9999' ) {
1000 sizeParts[ 1 ] = '0';
1001 }
1002
1003 return sizeParts;
1004 }
1005
1006 /**
1007 * Returns a number in a fraction format that represents the shutter speed.
1008 * @param Number speed
1009 * @return String
1010 */
1011 function formatShutterSpeed( speed ) {
1012 var denominator;
1013
1014 // round to one decimal if value > 1s by multiplying it by 10, rounding, then dividing by 10 again
1015 if ( speed >= 1 ) {
1016 return Math.round( speed * 10 ) / 10 + 's';
1017 }
1018
1019 // If the speed is less than one, we find the denominator by inverting
1020 // the number. Since cameras usually use rational numbers as shutter
1021 // speeds, we should get a nice round number. Or close to one in cases
1022 // like 1/30. So we round it.
1023 denominator = Math.round( 1 / speed );
1024
1025 return '1/' + denominator + 's';
1026 }
1027
1028 function parseTitleOrDesc( value ) {
1029 if ( ! value.match( ' ' ) && value.match( '_' ) ) {
1030 return '';
1031 }
1032
1033 return value;
1034 }
1035
1036 function updateTitleCaptionAndDesc( data ) {
1037 var caption;
1038 var title;
1039 var desc;
1040 var captionMainElement;
1041 var captionInfoExtraElement;
1042 var titleElement;
1043 var descriptionElement;
1044
1045 captionMainElement = carousel.overlay.querySelector( '.jp-carousel-photo-caption' );
1046 captionInfoExtraElement = carousel.overlay.querySelector( '.jp-carousel-caption' );
1047
1048 titleElement = carousel.overlay.querySelector( '.jp-carousel-photo-title' );
1049 descriptionElement = carousel.overlay.querySelector( '.jp-carousel-photo-description' );
1050
1051 domUtil.hide( captionMainElement );
1052 domUtil.hide( captionInfoExtraElement );
1053 domUtil.hide( titleElement );
1054 domUtil.hide( descriptionElement );
1055
1056 caption = parseTitleOrDesc( data.caption ) || '';
1057 title = parseTitleOrDesc( data.title ) || '';
1058 desc = parseTitleOrDesc( data.desc ) || '';
1059
1060 if ( caption || title || desc ) {
1061 if ( caption ) {
1062 captionMainElement.innerHTML = caption;
1063 captionInfoExtraElement.innerHTML = caption;
1064
1065 domUtil.show( captionMainElement );
1066 domUtil.show( captionInfoExtraElement );
1067 }
1068
1069 if ( domUtil.stripHTML( caption ) === domUtil.stripHTML( title ) ) {
1070 title = '';
1071 }
1072
1073 if ( domUtil.stripHTML( caption ) === domUtil.stripHTML( desc ) ) {
1074 desc = '';
1075 }
1076
1077 if ( domUtil.stripHTML( title ) === domUtil.stripHTML( desc ) ) {
1078 desc = '';
1079 }
1080
1081 if ( desc ) {
1082 descriptionElement.innerHTML = desc;
1083 domUtil.show( descriptionElement );
1084
1085 if ( ! title && ! caption ) {
1086 captionMainElement.textContent = domUtil.stripHTML( desc );
1087 domUtil.show( captionMainElement );
1088 }
1089 }
1090
1091 if ( title ) {
1092 var plainTitle = domUtil.stripHTML( title );
1093 titleElement.textContent = plainTitle;
1094
1095 if ( ! caption ) {
1096 captionMainElement.textContent = plainTitle;
1097 captionInfoExtraElement.textContent = plainTitle;
1098
1099 domUtil.show( captionMainElement );
1100 }
1101
1102 domUtil.show( titleElement );
1103 }
1104 }
1105 }
1106
1107 // updateExif updates the contents of the exif UL (.jp-carousel-image-exif)
1108 function updateExif( meta ) {
1109 if ( ! meta || Number( jetpackCarouselStrings.display_exif ) !== 1 ) {
1110 return false;
1111 }
1112
1113 var ul = carousel.info.querySelector( '.jp-carousel-image-meta ul.jp-carousel-image-exif' );
1114 var html = '';
1115
1116 for ( var key in meta ) {
1117 var val = meta[ key ];
1118 var metaKeys = jetpackCarouselStrings.meta_data || [];
1119
1120 if ( parseFloat( val ) === 0 || ! val.length || metaKeys.indexOf( key ) === -1 ) {
1121 continue;
1122 }
1123
1124 switch ( key ) {
1125 case 'focal_length':
1126 val = val + 'mm';
1127 break;
1128 case 'shutter_speed':
1129 val = formatShutterSpeed( val );
1130 break;
1131 case 'aperture':
1132 val = 'f/' + val;
1133 break;
1134 }
1135
1136 html += '<li><h5>' + jetpackCarouselStrings[ key ] + '</h5>' + val + '</li>';
1137 }
1138
1139 ul.innerHTML = html;
1140 ul.style.removeProperty( 'display' );
1141 }
1142
1143 // Update the contents of the jp-carousel-image-download link
1144 function updateFullSizeLink( currentSlide ) {
1145 if ( ! currentSlide ) {
1146 return false;
1147 }
1148 var original;
1149 var origSize = [ currentSlide.attrs.origWidth, currentSlide.attrs.origHeight ];
1150 var imageLinkParser = document.createElement( 'a' );
1151
1152 imageLinkParser.href = currentSlide.attrs.src.replace( /\?.+$/, '' );
1153
1154 // Is this a Photon URL?
1155 if ( imageLinkParser.hostname.match( /^i[\d]{1}\.wp\.com$/i ) !== null ) {
1156 original = imageLinkParser.href;
1157 } else {
1158 original = currentSlide.attrs.origFile.replace( /\?.+$/, '' );
1159 }
1160
1161 var downloadText = carousel.info.querySelector( '.jp-carousel-download-text' );
1162 var permalink = carousel.info.querySelector( '.jp-carousel-image-download' );
1163
1164 downloadText.innerHTML = util.applyReplacements(
1165 jetpackCarouselStrings.download_original,
1166 origSize
1167 );
1168 permalink.setAttribute( 'href', original );
1169 permalink.style.removeProperty( 'display' );
1170 }
1171
1172 function testCommentsOpened( opened ) {
1173 var commentForm = carousel.info.querySelector( '#jp-carousel-comment-form-container' );
1174 var isOpened = parseInt( opened, 10 ) === 1;
1175
1176 if ( isOpened ) {
1177 domUtil.fadeIn( commentForm );
1178 } else {
1179 domUtil.fadeOut( commentForm );
1180 }
1181 }
1182
1183 function fetchComments( attachmentId, offset ) {
1184 var shouldClear = offset === undefined;
1185 var commentsIndicator = carousel.info.querySelector(
1186 '.jp-carousel-icon-comments .jp-carousel-has-comments-indicator'
1187 );
1188
1189 commentsIndicator.classList.remove( 'jp-carousel-show' );
1190
1191 clearInterval( commentInterval );
1192
1193 if ( ! attachmentId ) {
1194 return;
1195 }
1196
1197 if ( ! offset || offset < 1 ) {
1198 offset = 0;
1199 }
1200
1201 var comments = carousel.info.querySelector( '.jp-carousel-comments' );
1202 var commentsLoading = carousel.info.querySelector( '#jp-carousel-comments-loading' );
1203 domUtil.show( commentsLoading );
1204
1205 if ( shouldClear ) {
1206 domUtil.hide( comments );
1207 comments.innerHTML = '';
1208 }
1209
1210 var xhr = new XMLHttpRequest();
1211 var url =
1212 jetpackCarouselStrings.ajaxurl +
1213 '?action=get_attachment_comments' +
1214 '&nonce=' +
1215 jetpackCarouselStrings.nonce +
1216 '&id=' +
1217 attachmentId +
1218 '&offset=' +
1219 offset;
1220 xhr.open( 'GET', url );
1221 xhr.setRequestHeader( 'X-Requested-With', 'XMLHttpRequest' );
1222
1223 var onError = function () {
1224 domUtil.fadeIn( comments );
1225 domUtil.fadeOut( commentsLoading );
1226 };
1227
1228 xhr.onload = function () {
1229 // Ignore the results if they arrive late and we're now on a different slide.
1230 if (
1231 ! carousel.currentSlide ||
1232 carousel.currentSlide.attrs.attachmentId !== attachmentId
1233 ) {
1234 return;
1235 }
1236
1237 var isSuccess = xhr.status >= 200 && xhr.status < 300;
1238 var data;
1239 try {
1240 data = JSON.parse( xhr.responseText );
1241 } catch {
1242 // Do nothing.
1243 }
1244
1245 if ( ! isSuccess || ! data || ! Array.isArray( data ) ) {
1246 return onError();
1247 }
1248
1249 if ( shouldClear ) {
1250 comments.innerHTML = '';
1251 }
1252
1253 for ( var i = 0; i < data.length; i++ ) {
1254 var entry = data[ i ];
1255 var comment = document.createElement( 'div' );
1256 comment.classList.add( 'jp-carousel-comment' );
1257 comment.setAttribute( 'id', 'jp-carousel-comment-' + entry.id );
1258 comment.innerHTML =
1259 '<div class="comment-gravatar">' +
1260 entry.gravatar_markup +
1261 '</div>' +
1262 '<div class="comment-content">' +
1263 '<div class="comment-author">' +
1264 entry.author_markup +
1265 '</div>' +
1266 '<div class="comment-date">' +
1267 entry.date_gmt +
1268 '</div>' +
1269 entry.content +
1270 '</div>';
1271 comments.appendChild( comment );
1272
1273 // Set the interval to check for a new page of comments.
1274 clearInterval( commentInterval );
1275 commentInterval = setInterval( function () {
1276 if ( carousel.container.scrollTop + 150 > window.innerHeight ) {
1277 fetchComments( attachmentId, offset + 10 );
1278 clearInterval( commentInterval );
1279 }
1280 }, 300 );
1281 }
1282
1283 if ( data.length > 0 ) {
1284 domUtil.show( comments );
1285 commentsIndicator.innerText = data.length;
1286 commentsIndicator.classList.add( 'jp-carousel-show' );
1287 }
1288
1289 domUtil.hide( commentsLoading );
1290 };
1291
1292 xhr.onerror = onError;
1293
1294 xhr.send();
1295 }
1296
1297 function loadFullImage( slide ) {
1298 var el = slide.el;
1299 var attrs = slide.attrs;
1300 var image = el.querySelector( 'img' );
1301
1302 if ( ! image.hasAttribute( 'data-loaded' ) ) {
1303 var hasPreview = !! attrs.previewImage;
1304 var thumbSize = attrs.thumbSize;
1305
1306 if ( ! hasPreview || ( thumbSize && el.offsetWidth > thumbSize.width ) ) {
1307 image.src = attrs.src;
1308 } else {
1309 image.src = attrs.previewImage;
1310 }
1311
1312 image.setAttribute( 'itemprop', 'image' );
1313 image.setAttribute( 'data-loaded', 1 );
1314 }
1315 }
1316
1317 function preloadAdjacentImages( currentIndex ) {
1318 var indicesToPreload = [];
1319 var totalSlides = carousel.slides.length;
1320
1321 // Only preload adjacent images if we have more than one slide (matching loop condition)
1322 if ( totalSlides > 1 ) {
1323 // Previous image (with loop handling)
1324 var prevIndex = currentIndex > 0 ? currentIndex - 1 : totalSlides - 1;
1325 indicesToPreload.push( prevIndex );
1326
1327 // Next image (with loop handling)
1328 var nextIndex = currentIndex < totalSlides - 1 ? currentIndex + 1 : 0;
1329 indicesToPreload.push( nextIndex );
1330 }
1331
1332 indicesToPreload.forEach( function ( index ) {
1333 var slide = carousel.slides[ index ];
1334 if ( slide ) {
1335 // Load in background without showing
1336 loadFullImage( slide );
1337
1338 // Also load background image if enabled
1339 if (
1340 Number( jetpackCarouselStrings.display_background_image ) === 1 &&
1341 ! slide.backgroundImage
1342 ) {
1343 loadBackgroundImage( slide );
1344 }
1345 }
1346 } );
1347 }
1348
1349 function loadBackgroundImage( slide ) {
1350 var currentSlide = slide.el;
1351
1352 if ( swiper && swiper.slides ) {
1353 currentSlide = swiper.slides[ swiper.activeIndex ];
1354 }
1355
1356 var image = slide.attrs.originalElement;
1357 var isLoaded = image.complete && image.naturalHeight !== 0;
1358
1359 if ( isLoaded ) {
1360 applyBackgroundImage( slide, currentSlide, image );
1361 return;
1362 }
1363
1364 image.onload = function () {
1365 applyBackgroundImage( slide, currentSlide, image );
1366 };
1367 }
1368
1369 function applyBackgroundImage( slide, currentSlide, image ) {
1370 var url = util.getBackgroundImage( image );
1371 slide.backgroundImage = url;
1372 currentSlide.style.backgroundImage = 'url(' + url + ')';
1373 currentSlide.style.backgroundSize = 'cover';
1374 }
1375
1376 function clearCommentTextAreaValue() {
1377 if ( carousel.commentField ) {
1378 carousel.commentField.value = '';
1379 }
1380 }
1381
1382 function getOriginalDimensions( el ) {
1383 var size = el.getAttribute( 'data-orig-size' ) || '';
1384
1385 if ( size ) {
1386 var parts = size.split( ',' );
1387 return { width: parseInt( parts[ 0 ], 10 ), height: parseInt( parts[ 1 ], 10 ) };
1388 }
1389 return {
1390 width: el.getAttribute( 'data-original-width' ) || el.getAttribute( 'width' ) || undefined,
1391 height:
1392 el.getAttribute( 'data-original-height' ) || el.getAttribute( 'height' ) || undefined,
1393 };
1394 }
1395
1396 function initCarouselSlides( items, startIndex ) {
1397 carousel.slides = [];
1398
1399 var max = calculateMaxSlideDimensions();
1400
1401 // If the startIndex is not 0 then preload the clicked image first.
1402 if ( startIndex !== 0 && items[ startIndex ].getAttribute( 'data-gallery-src' ) !== null ) {
1403 var img = new Image();
1404 img.src = items[ startIndex ].getAttribute( 'data-gallery-src' );
1405 }
1406
1407 var useInPageThumbnails = !! domUtil.closest( items[ 0 ], '.tiled-gallery.type-rectangular' );
1408
1409 // create the 'slide'
1410 Array.prototype.forEach.call( items, function ( item, i ) {
1411 var permalinkEl = domUtil.closest( item, 'a' );
1412 var origFile = item.getAttribute( 'data-orig-file' ) || item.getAttribute( 'src-orig' );
1413 var attrID =
1414 item.getAttribute( 'data-attachment-id' ) || item.getAttribute( 'data-id' ) || '0';
1415 var caption = document.querySelector(
1416 'img[data-attachment-id="' + attrID + '"] + figcaption'
1417 );
1418
1419 if ( caption ) {
1420 caption = caption.innerHTML;
1421 } else {
1422 caption = item.getAttribute( 'data-image-caption' );
1423 }
1424
1425 var attrs = {
1426 originalElement: item,
1427 attachmentId: attrID,
1428 commentsOpened: item.getAttribute( 'data-comments-opened' ) || '0',
1429 imageMeta: domUtil.getJSONAttribute( item, 'data-image-meta' ) || {},
1430 title: item.getAttribute( 'data-image-title' ) || '',
1431 desc: item.getAttribute( 'data-image-description' ) || '',
1432 largeFile: item.getAttribute( 'data-large-file' ) || '',
1433 origFile: origFile || '',
1434 thumbSize: { width: item.naturalWidth, height: item.naturalHeight },
1435 caption: caption || '',
1436 permalink: permalinkEl && permalinkEl.getAttribute( 'href' ),
1437 src: origFile || item.getAttribute( 'src' ) || '',
1438 };
1439
1440 var tiledGalleryItem = domUtil.closest( item, '.tiled-gallery-item' );
1441 var tiledCaptionEl =
1442 tiledGalleryItem && tiledGalleryItem.querySelector( '.tiled-gallery-caption' );
1443 var tiledCaption = tiledCaptionEl && tiledCaptionEl.innerHTML;
1444 if ( tiledCaption ) {
1445 attrs.caption = tiledCaption;
1446 }
1447
1448 var origDimensions = getOriginalDimensions( item );
1449
1450 attrs.origWidth = origDimensions.width || attrs.thumbSize.width;
1451 attrs.origHeight = origDimensions.height || attrs.thumbSize.height;
1452
1453 if ( typeof wpcom !== 'undefined' && wpcom.carousel && wpcom.carousel.generateImgSrc ) {
1454 attrs.src = wpcom.carousel.generateImgSrc( item, max );
1455 } else {
1456 attrs.src = selectBestImageUrl( {
1457 origFile: attrs.src,
1458 origWidth: attrs.origWidth,
1459 origHeight: attrs.origHeight,
1460 maxWidth: max.width,
1461 maxHeight: max.height,
1462 largeFile: attrs.largeFile,
1463 } );
1464 }
1465
1466 // Set the final src.
1467 item.setAttribute( 'data-gallery-src', attrs.src );
1468
1469 if ( attrs.attachmentId !== '0' ) {
1470 attrs.title = util.texturize( attrs.title );
1471 attrs.desc = util.texturize( attrs.desc );
1472 attrs.caption = util.texturize( attrs.caption );
1473
1474 // Initially, the image is a 1x1 transparent gif.
1475 // The preview is shown as a background image on the slide itself.
1476 var image = new Image();
1477
1478 var slideEl = document.createElement( 'div' );
1479 slideEl.classList.add( 'swiper-slide' );
1480 slideEl.setAttribute( 'itemprop', 'associatedMedia' );
1481 slideEl.setAttribute( 'itemscope', '' );
1482 slideEl.setAttribute( 'itemtype', 'https://schema.org/ImageObject' );
1483 var zoomEl = document.createElement( 'div' );
1484 zoomEl.classList.add( 'swiper-zoom-container' );
1485
1486 carousel.gallery.appendChild( slideEl );
1487
1488 slideEl.appendChild( zoomEl );
1489 zoomEl.appendChild( image );
1490 slideEl.setAttribute( 'data-attachment-id', attrs.attachmentId );
1491 slideEl.setAttribute( 'data-permalink', attrs.permalink );
1492 slideEl.setAttribute( 'data-orig-file', attrs.origFile );
1493
1494 if ( useInPageThumbnails ) {
1495 // Use the image already loaded in the gallery as a preview.
1496 attrs.previewImage = attrs.src;
1497 }
1498
1499 var slide = { el: slideEl, attrs: attrs, index: i };
1500 carousel.slides.push( slide );
1501 }
1502 } );
1503 }
1504
1505 function loadSwiper( gallery, options ) {
1506 if ( ! window.JetpackSwiper ) {
1507 var loader = document.querySelector( '#jp-carousel-loading-overlay' );
1508 domUtil.show( loader );
1509 var jsScript = document.createElement( 'script' );
1510 jsScript.id = 'jetpack-carousel-swiper-js';
1511 jsScript.src = window.jetpackSwiperLibraryPath.url;
1512 jsScript.async = true;
1513 jsScript.onload = function () {
1514 domUtil.hide( loader );
1515 openCarousel( gallery, options );
1516 };
1517 jsScript.onerror = function () {
1518 domUtil.hide( loader );
1519 };
1520 document.head.appendChild( jsScript );
1521 return;
1522 }
1523 openCarousel( gallery, options );
1524 }
1525
1526 function openCarousel( gallery, options ) {
1527 var settings = {
1528 imgSelector:
1529 '.gallery-item [data-attachment-id], .tiled-gallery-item [data-attachment-id], img[data-attachment-id], img[data-id]',
1530 startIndex: 0,
1531 };
1532
1533 var data = domUtil.getJSONAttribute( gallery, 'data-carousel-extra' );
1534 var tapTimeout;
1535
1536 if ( ! data ) {
1537 return; // don't run if the default gallery functions weren't used
1538 }
1539
1540 const images = gallery.querySelectorAll( settings.imgSelector );
1541 if ( ! images.length ) {
1542 return; // don't run if we found no images in the gallery (somehow it has images that aren't in the media library?)
1543 }
1544
1545 initializeCarousel();
1546
1547 if ( carousel.isOpen ) {
1548 return; // don't open if already opened
1549 }
1550 carousel.isOpen = true;
1551
1552 // make sure to stop the page from scrolling behind the carousel overlay, so we don't trigger
1553 // infiniscroll for it when enabled (Reader, theme infiniscroll, etc).
1554 originalOverflow = getComputedStyle( document.body ).overflow;
1555 document.body.style.overflow = 'hidden';
1556 // prevent html from overflowing on some of the new themes.
1557 originalHOverflow = getComputedStyle( document.documentElement ).overflow;
1558 document.documentElement.style.overflow = 'hidden';
1559 scrollPos = window.scrollY || window.pageYOffset || 0;
1560
1561 carousel.container.setAttribute( 'data-carousel-extra', JSON.stringify( data ) );
1562 stat( [ 'open', 'view_image' ] );
1563
1564 // If options exist, lets merge them
1565 // with our default settings
1566 for ( var option in options || {} ) {
1567 settings[ option ] = options[ option ];
1568 }
1569
1570 if ( settings.startIndex === -1 ) {
1571 settings.startIndex = 0; // -1 returned if can't find index, so start from beginning
1572 }
1573
1574 domUtil.emitEvent( carousel.overlay, 'jp_carousel.beforeOpen' );
1575 carousel.gallery.innerHTML = '';
1576
1577 // Need to set the overlay manually to block or swiper does't initialise properly.
1578 carousel.overlay.style.opacity = 1;
1579 carousel.overlay.style.display = 'block';
1580
1581 initCarouselSlides( images, settings.startIndex );
1582
1583 swiper = new window.JetpackSwiper( '.jp-carousel-swiper-container', {
1584 centeredSlides: true,
1585 zoom: true,
1586 loop: carousel.slides.length > 1,
1587 // Turn off interactions and hide navigation arrows if there is only one slide.
1588 enabled: carousel.slides.length > 1,
1589 pagination: {
1590 el: '.jp-swiper-pagination',
1591 clickable: true,
1592 },
1593 navigation: {
1594 nextEl: '.jp-swiper-button-next',
1595 prevEl: '.jp-swiper-button-prev',
1596 },
1597 initialSlide: settings.startIndex,
1598 on: {
1599 init: function () {
1600 selectSlideAtIndex( settings.startIndex );
1601 },
1602 },
1603 preventClicks: false,
1604 preventClicksPropagation: false,
1605 preventInteractionOnTransition: ! domUtil.isTouch(),
1606 threshold: 5,
1607 } );
1608
1609 swiper.on( 'slideChange', function ( swiper ) {
1610 if ( ! carousel.isOpen ) {
1611 return;
1612 }
1613 selectSlideAtIndex( swiper.realIndex );
1614 carousel.overlay.classList.remove( 'jp-carousel-hide-controls' );
1615 } );
1616
1617 swiper.on( 'zoomChange', function ( swiper, scale ) {
1618 if ( scale > 1 ) {
1619 carousel.overlay.classList.add( 'jp-carousel-hide-controls' );
1620 }
1621
1622 if ( scale === 1 ) {
1623 carousel.overlay.classList.remove( 'jp-carousel-hide-controls' );
1624 }
1625 } );
1626
1627 swiper.on( 'doubleTap', function ( swiper ) {
1628 clearTimeout( tapTimeout );
1629 if ( swiper.zoom.scale === 1 ) {
1630 var zoomTimeout = setTimeout( function () {
1631 carousel.overlay.classList.remove( 'jp-carousel-hide-controls' );
1632 clearTimeout( zoomTimeout );
1633 }, 150 );
1634 }
1635 } );
1636
1637 swiper.on( 'tap', function () {
1638 if ( swiper.zoom.scale > 1 ) {
1639 tapTimeout = setTimeout( function () {
1640 carousel.overlay.classList.toggle( 'jp-carousel-hide-controls' );
1641 }, 150 );
1642 }
1643 } );
1644
1645 domUtil.fadeIn( carousel.overlay, function () {
1646 domUtil.emitEvent( carousel.overlay, 'jp_carousel.afterOpen' );
1647 } );
1648 }
1649
1650 // Register the event listeners for starting the gallery
1651 document.body.addEventListener( 'click', handleInteraction );
1652 document.body.addEventListener( 'keydown', handleInteraction );
1653 document.querySelectorAll( galleryItemSelector + 'img' ).forEach( function ( galleryImage ) {
1654 if ( shouldOpenModal( galleryImage ) ) {
1655 makeGalleryImageAccessible( galleryImage );
1656 }
1657 } );
1658
1659 function handleInteraction( e ) {
1660 if ( e.type === 'click' ) {
1661 handleClick( e );
1662 return;
1663 }
1664
1665 if ( e.type === 'keydown' ) {
1666 const parentElement = document.activeElement.parentElement;
1667 const isParentCarouselContainer =
1668 parentElement && parentElement.classList.contains( 'tiled-gallery__item' );
1669
1670 if ( ( e.key === ' ' || e.key === 'Enter' ) && isParentCarouselContainer ) {
1671 handleClick( e );
1672 }
1673 }
1674 }
1675
1676 function normalizeUrl( url ) {
1677 return ( url || '' ).split( '?' )[ 0 ].replace( /\/$/, '' );
1678 }
1679
1680 function shouldOpenModal( el ) {
1681 if ( el.tagName === 'A' ) {
1682 el = el.querySelector( 'img' ) || el;
1683 }
1684
1685 var parent = el.parentElement;
1686 var grandparent = parent ? parent.parentElement : null;
1687
1688 // If Gallery is made up of individual Image blocks check for custom link before
1689 // loading carousel. The custom link may be the parent or could be a descendant
1690 // of the parent if the image has rounded corners.
1691 var parentHref = null;
1692 if ( grandparent && grandparent.classList.contains( 'wp-block-image' ) ) {
1693 parentHref = parent.getAttribute( 'href' );
1694 } else if (
1695 parent &&
1696 parent.classList.contains( 'wp-block-image' ) &&
1697 parent.querySelector( ':scope > a' )
1698 ) {
1699 parentHref = parent.querySelector( ':scope > a' ).getAttribute( 'href' );
1700 }
1701
1702 // If the link does not point to the attachment or media file then assume Image has
1703 // a custom link so don't load the carousel.
1704 if ( parentHref ) {
1705 var cleanHref = normalizeUrl( parentHref );
1706 var cleanOrig = normalizeUrl( el.getAttribute( 'data-orig-file' ) );
1707 var cleanPerm = normalizeUrl( el.getAttribute( 'data-permalink' ) );
1708
1709 if ( cleanHref !== cleanOrig && cleanHref !== cleanPerm ) {
1710 return false;
1711 }
1712 }
1713
1714 // Do not open the modal if we are looking at a gallery caption from before WP5, which may contain a link.
1715 if ( parent && parent.classList.contains( 'gallery-caption' ) ) {
1716 return false;
1717 }
1718
1719 // Do not open the modal if we are looking at a caption of a gallery block, which may contain a link.
1720 if ( domUtil.matches( parent, 'figcaption' ) ) {
1721 return false;
1722 }
1723
1724 return true;
1725 }
1726
1727 function handleClick( e ) {
1728 var isCompatible =
1729 window.CSS && window.CSS.supports && window.CSS.supports( 'display', 'grid' );
1730
1731 // IE11 support is being dropped in August 2021. The new swiper.js libray is not IE11 compat
1732 // so just default to opening individual image attachment/media pages for IE.
1733 if ( ! isCompatible ) {
1734 return;
1735 }
1736
1737 var target = e.target;
1738 var gallery = domUtil.closest( target, gallerySelector );
1739
1740 if ( gallery ) {
1741 if ( ! testForData( gallery ) ) {
1742 return;
1743 }
1744
1745 if ( ! shouldOpenModal( target ) ) {
1746 return;
1747 }
1748
1749 // Set height to auto.
1750 // Fix some themes where closing carousel brings view back to top.
1751 document.documentElement.style.height = 'auto';
1752
1753 e.preventDefault();
1754
1755 // Stopping propagation in case there are parent elements
1756 // with .gallery or .tiled-gallery class
1757 e.stopPropagation();
1758
1759 var item = domUtil.closest( target, itemSelector );
1760 var index = Array.prototype.indexOf.call( gallery.querySelectorAll( itemSelector ), item );
1761 loadSwiper( gallery, { startIndex: index } );
1762 }
1763 }
1764
1765 // Handle lightbox (single image gallery) for images linking to 'Attachment Page'.
1766 if ( Number( jetpackCarouselStrings.single_image_gallery ) === 1 ) {
1767 processSingleImageGallery();
1768 document.body.addEventListener( 'is.post-load', function () {
1769 processSingleImageGallery();
1770 } );
1771 }
1772
1773 // Makes carousel work on page load and when back button leads to same URL with carousel hash
1774 // (i.e. no actual document.ready trigger).
1775 window.addEventListener( 'hashchange', function () {
1776 var hashRegExp = /jp-carousel-(\d+)/;
1777
1778 if ( ! window.location.hash || ! hashRegExp.test( window.location.hash ) ) {
1779 if ( carousel.isOpen ) {
1780 closeCarousel();
1781 }
1782
1783 return;
1784 }
1785
1786 if ( window.location.hash === lastKnownLocationHash && carousel.isOpen ) {
1787 return;
1788 }
1789
1790 if ( window.location.hash && carousel.gallery && ! carousel.isOpen && history.back ) {
1791 history.back();
1792 return;
1793 }
1794
1795 lastKnownLocationHash = window.location.hash;
1796 var matchList = window.location.hash.match( hashRegExp );
1797 var attachmentId = parseInt( matchList[ 1 ], 10 );
1798 var galleries = document.querySelectorAll( gallerySelector );
1799
1800 // Find the first thumbnail that matches the attachment ID in the location
1801 // hash, then open the gallery that contains it.
1802 for ( var i = 0; i < galleries.length; i++ ) {
1803 var gallery = galleries[ i ];
1804 var selected;
1805
1806 var images = gallery.querySelectorAll( 'img' );
1807 for ( var j = 0; j < images.length; j++ ) {
1808 if (
1809 parseInt( images[ j ].getAttribute( 'data-attachment-id' ), 10 ) === attachmentId ||
1810 parseInt( images[ j ].getAttribute( 'data-id' ), 10 ) === attachmentId
1811 ) {
1812 selected = j;
1813 break;
1814 }
1815 }
1816
1817 if ( selected !== undefined ) {
1818 openOrSelectSlide( gallery, selected );
1819 break;
1820 }
1821 }
1822 } );
1823
1824 if ( window.location.hash ) {
1825 domUtil.emitEvent( window, 'hashchange' );
1826 }
1827 }
1828
1829 if ( document.readyState !== 'loading' ) {
1830 init();
1831 } else {
1832 document.addEventListener( 'DOMContentLoaded', init );
1833 }
1834 } )();
1835