PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 15.1.2
Jetpack – WP Security, Backup, Speed, & Growth v15.1.2
12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 13.8.3 13.9.2 14.0.1 14.1.1 14.2.2 14.3.1 14.4.2 14.5.1 14.6.1 14.7.1 14.8.1 14.9.2 15.0.3 15.1.2 15.2.1 15.3.2 15.4.1 15.5.1 15.6.1 15.7.2 15.8.1 15.9.2 16.0.2 16.1.3 16.2-a.5 16.2-a.3 16.1.2 16.2-a.1 16.1.1 16.1 16.1-beta 16.1-beta.2 16.1-beta.3 16.1-a.5 16.1-a.3 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 year ago jetpack-carousel.css 1 year ago jetpack-carousel.js 1 year ago jetpack-carousel.php 1 year ago readme.md 5 years ago
jetpack-carousel.js
1822 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;
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 return text.replace( /<[^>]*>?/gm, '' );
276 }
277
278 return {
279 closest: closest,
280 matches: matches,
281 hide: hide,
282 show: show,
283 fadeIn: fadeIn,
284 fadeOut: fadeOut,
285 scrollToElement: scrollToElement,
286 getJSONAttribute: getJSONAttribute,
287 convertToPlainText: convertToPlainText,
288 stripHTML: stripHTML,
289 emitEvent: emitEvent,
290 isTouch: isTouch,
291 };
292 } )();
293
294 /////////////////////////////////////
295 // Carousel implementation
296 /////////////////////////////////////
297 function init() {
298 var commentInterval;
299 var screenPadding;
300 var originalOverflow;
301 var originalHOverflow;
302 var scrollPos;
303
304 var lastKnownLocationHash = '';
305 var isUserTyping = false;
306
307 var gallerySelector =
308 'div.gallery, div.tiled-gallery, ul.wp-block-gallery, ul.blocks-gallery-grid, ' +
309 'figure.wp-block-gallery.has-nested-images, div.wp-block-jetpack-tiled-gallery, a.single-image-gallery';
310
311 // Selector for items within a gallery or tiled gallery.
312 var galleryItemSelector =
313 '.gallery-item, .tiled-gallery-item, .blocks-gallery-item, ' + ' .tiled-gallery__item';
314
315 // Selector for all items including single images.
316 var itemSelector = galleryItemSelector + ', .wp-block-image';
317
318 var carousel = {};
319
320 var stat =
321 typeof wpcom !== 'undefined' && wpcom.carousel && wpcom.carousel.stat
322 ? wpcom.carousel.stat
323 : util.noop;
324
325 var pageview =
326 typeof wpcom !== 'undefined' && wpcom.carousel && wpcom.carousel.pageview
327 ? wpcom.carousel.pageview
328 : util.noop;
329
330 function handleKeyboardEvent( e ) {
331 if ( ! isUserTyping ) {
332 switch ( e.which ) {
333 case 38: // up
334 e.preventDefault();
335 carousel.overlay.scrollTop -= 100;
336 break;
337 case 40: // down
338 e.preventDefault();
339 carousel.overlay.scrollTop += 100;
340 break;
341 case 39: // right
342 e.preventDefault();
343 swiper.slideNext();
344 break;
345 case 37: // left
346 case 8: // backspace
347 e.preventDefault();
348 swiper.slidePrev();
349 break;
350 case 27: // escape
351 e.preventDefault();
352 closeCarousel();
353 break;
354 default:
355 break;
356 }
357 }
358 }
359
360 function disableKeyboardNavigation() {
361 isUserTyping = true;
362 }
363
364 function enableKeyboardNavigation() {
365 isUserTyping = false;
366 }
367
368 function calculatePadding() {
369 var baseScreenPadding = 110;
370 screenPadding = baseScreenPadding;
371
372 if ( window.innerWidth <= 760 ) {
373 screenPadding = Math.round( ( window.innerWidth / 760 ) * baseScreenPadding );
374
375 if ( screenPadding < 40 && domUtil.isTouch() ) {
376 screenPadding = 0;
377 }
378 }
379 }
380
381 function makeGalleryImageAccessible( img ) {
382 img.role = 'button';
383 img.tabIndex = 0;
384 img.ariaLabel = jetpackCarouselStrings.image_label;
385 }
386
387 function initializeCarousel() {
388 if ( ! carousel.overlay ) {
389 carousel.overlay = document.querySelector( '.jp-carousel-overlay' );
390 carousel.container = carousel.overlay.querySelector( '.jp-carousel-wrap' );
391 carousel.gallery = carousel.container.querySelector( '.jp-carousel' );
392 carousel.info = carousel.overlay.querySelector( '.jp-carousel-info' );
393 carousel.caption = carousel.info.querySelector( '.jp-carousel-caption' );
394 carousel.commentField = carousel.overlay.querySelector(
395 '#jp-carousel-comment-form-comment-field'
396 );
397 carousel.emailField = carousel.overlay.querySelector(
398 '#jp-carousel-comment-form-email-field'
399 );
400 carousel.authorField = carousel.overlay.querySelector(
401 '#jp-carousel-comment-form-author-field'
402 );
403 carousel.urlField = carousel.overlay.querySelector( '#jp-carousel-comment-form-url-field' );
404
405 calculatePadding();
406
407 [
408 carousel.commentField,
409 carousel.emailField,
410 carousel.authorField,
411 carousel.urlField,
412 ].forEach( function ( field ) {
413 if ( field ) {
414 field.addEventListener( 'focus', disableKeyboardNavigation );
415 field.addEventListener( 'blur', enableKeyboardNavigation );
416 }
417 } );
418
419 carousel.overlay.addEventListener( 'click', function ( e ) {
420 var target = e.target;
421 var isTargetCloseHint = !! domUtil.closest( target, '.jp-carousel-close-hint' );
422 var isSmallScreen = !! window.matchMedia( '(max-device-width: 760px)' ).matches;
423 if ( target === carousel.overlay ) {
424 if ( ! isSmallScreen ) {
425 closeCarousel();
426 }
427 } else if ( isTargetCloseHint ) {
428 closeCarousel();
429 } else if ( target.classList.contains( 'jp-carousel-image-download' ) ) {
430 stat( 'download_original_click' );
431 } else if ( target.classList.contains( 'jp-carousel-comment-login' ) ) {
432 handleCommentLoginClick( e );
433 } else if ( domUtil.closest( target, '#jp-carousel-comment-form-container' ) ) {
434 handleCommentFormClick( e );
435 } else if (
436 domUtil.closest( target, '.jp-carousel-photo-icons-container' ) ||
437 target.classList.contains( 'jp-carousel-photo-title' )
438 ) {
439 handleFooterElementClick( e );
440 }
441 } );
442
443 window.addEventListener( 'keydown', handleKeyboardEvent );
444
445 carousel.overlay.addEventListener( 'jp_carousel.afterOpen', function () {
446 enableKeyboardNavigation();
447
448 // Don't show navigation if there's only one image.
449 if ( carousel.slides.length <= 1 ) {
450 return;
451 }
452 // Show dot pagination if slide count is <= 5, otherwise show n/total.
453 if ( carousel.slides.length <= 5 ) {
454 domUtil.show( carousel.info.querySelector( '.jp-swiper-pagination' ) );
455 } else {
456 domUtil.show( carousel.info.querySelector( '.jp-carousel-pagination' ) );
457 }
458 } );
459
460 carousel.overlay.addEventListener( 'jp_carousel.beforeClose', function () {
461 disableKeyboardNavigation();
462
463 // Fixes some themes where closing carousel brings view back to top.
464 document.documentElement.style.removeProperty( 'height' );
465
466 // If we disable the swiper (because there's only one image)
467 // we have to re-enable it here again as Swiper doesn't, for some reason,
468 // show the navigation buttons again after reinitialization.
469 if ( swiper ) {
470 swiper.enable();
471 }
472
473 // Hide pagination.
474 domUtil.hide( carousel.info.querySelector( '.jp-swiper-pagination' ) );
475 domUtil.hide( carousel.info.querySelector( '.jp-carousel-pagination' ) );
476 } );
477
478 carousel.overlay.addEventListener( 'jp_carousel.afterClose', function () {
479 // don't force the browser back when the carousel closes.
480 if ( window.history.pushState ) {
481 history.pushState(
482 '',
483 document.title,
484 window.location.pathname + window.location.search
485 );
486 } else {
487 window.location.href = '';
488 }
489 lastKnownLocationHash = '';
490 carousel.isOpen = false;
491 } );
492
493 // Prevent native browser zooming
494 carousel.overlay.addEventListener( 'touchstart', function ( e ) {
495 if ( e.touches.length > 1 ) {
496 e.preventDefault();
497 }
498 } );
499 }
500 }
501
502 function handleCommentLoginClick() {
503 var slide = carousel.currentSlide;
504 var attachmentId = slide ? slide.attrs.attachmentId : '0';
505
506 window.location.href = jetpackCarouselStrings.login_url + '%23jp-carousel-' + attachmentId;
507 }
508
509 function updatePostResults( msg, isSuccess ) {
510 var results = carousel.overlay.querySelector( '#jp-carousel-comment-post-results' );
511 var elClass = 'jp-carousel-comment-post-' + ( isSuccess ? 'success' : 'error' );
512 results.innerHTML = '<span class="' + elClass + '">' + msg + '</span>';
513 domUtil.hide( carousel.overlay.querySelector( '#jp-carousel-comment-form-spinner' ) );
514 carousel.overlay
515 .querySelector( '#jp-carousel-comment-form' )
516 .classList.remove( 'jp-carousel-is-disabled' );
517 domUtil.show( results );
518 }
519
520 function handleCommentFormClick( e ) {
521 var target = e.target;
522 var data = domUtil.getJSONAttribute( carousel.container, 'data-carousel-extra' ) || {};
523 var attachmentId = carousel.currentSlide.attrs.attachmentId;
524
525 var wrapper = document.querySelector( '#jp-carousel-comment-form-submit-and-info-wrapper' );
526 var spinner = document.querySelector( '#jp-carousel-comment-form-spinner' );
527 // eslint-disable-next-line @wordpress/no-unused-vars-before-return
528 var submit = document.querySelector( '#jp-carousel-comment-form-button-submit' );
529 var form = document.querySelector( '#jp-carousel-comment-form' );
530
531 if (
532 carousel.commentField &&
533 carousel.commentField.getAttribute( 'id' ) === target.getAttribute( 'id' )
534 ) {
535 // For first page load
536 disableKeyboardNavigation();
537 domUtil.show( wrapper );
538 } else if ( domUtil.matches( target, 'input[type="submit"]' ) ) {
539 e.preventDefault();
540 e.stopPropagation();
541
542 domUtil.show( spinner );
543 form.classList.add( 'jp-carousel-is-disabled' );
544
545 var ajaxData = {
546 action: 'post_attachment_comment',
547 nonce: jetpackCarouselStrings.nonce,
548 blog_id: data.blog_id,
549 id: attachmentId,
550 comment: carousel.commentField.value,
551 };
552
553 if ( ! ajaxData.comment.length ) {
554 updatePostResults( jetpackCarouselStrings.no_comment_text, false );
555 return;
556 }
557
558 if ( Number( jetpackCarouselStrings.is_logged_in ) !== 1 ) {
559 ajaxData.email = carousel.emailField.value;
560 ajaxData.author = carousel.authorField.value;
561 ajaxData.url = carousel.urlField.value;
562
563 if ( Number( jetpackCarouselStrings.require_name_email ) === 1 ) {
564 if ( ! ajaxData.email.length || ! ajaxData.email.match( '@' ) ) {
565 updatePostResults( jetpackCarouselStrings.no_comment_email, false );
566 return;
567 } else if ( ! ajaxData.author.length ) {
568 updatePostResults( jetpackCarouselStrings.no_comment_author, false );
569 return;
570 }
571 }
572 }
573
574 var xhr = new XMLHttpRequest();
575 xhr.open( 'POST', jetpackCarouselStrings.ajaxurl, true );
576 xhr.setRequestHeader( 'X-Requested-With', 'XMLHttpRequest' );
577 xhr.setRequestHeader( 'Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8' );
578
579 xhr.onreadystatechange = function () {
580 if (
581 this.readyState === XMLHttpRequest.DONE &&
582 this.status >= 200 &&
583 this.status < 300
584 ) {
585 var response;
586 try {
587 response = JSON.parse( this.response );
588 } catch {
589 updatePostResults( jetpackCarouselStrings.comment_post_error, false );
590 return;
591 }
592 if ( response.comment_status === 'approved' ) {
593 updatePostResults( jetpackCarouselStrings.comment_approved, true );
594 } else if ( response.comment_status === 'unapproved' ) {
595 updatePostResults( jetpackCarouselStrings.comment_unapproved, true );
596 } else {
597 // 'deleted', 'spam', false
598 updatePostResults( jetpackCarouselStrings.comment_post_error, false );
599 }
600 clearCommentTextAreaValue();
601 fetchComments( attachmentId );
602 submit.value = jetpackCarouselStrings.post_comment;
603 domUtil.hide( spinner );
604 form.classList.remove( 'jp-carousel-is-disabled' );
605 } else {
606 // TODO: Add error handling and display here
607 updatePostResults( jetpackCarouselStrings.comment_post_error, false );
608 }
609 };
610
611 var params = [];
612 for ( var item in ajaxData ) {
613 if ( item ) {
614 // Encode each form element into a URI-compatible string.
615 var encoded = encodeURIComponent( item ) + '=' + encodeURIComponent( ajaxData[ item ] );
616 // In x-www-form-urlencoded, spaces should be `+`, not `%20`.
617 params.push( encoded.replace( /%20/g, '+' ) );
618 }
619 }
620 var encodedData = params.join( '&' );
621
622 xhr.send( encodedData );
623 }
624 }
625
626 /**
627 * Handles clicks to icons and other action elements in the icon container.
628 * @param {MouseEvent|TouchEvent|KeyBoardEvent} Event object.
629 */
630 function handleFooterElementClick( e ) {
631 e.preventDefault();
632
633 var target = e.target;
634 var extraInfoContainer = carousel.info.querySelector( '.jp-carousel-info-extra' );
635 var photoMetaContainer = carousel.info.querySelector( '.jp-carousel-image-meta' );
636 var commentsContainer = carousel.info.querySelector( '.jp-carousel-comments-wrapper' );
637 var infoIcon = carousel.info.querySelector( '.jp-carousel-icon-info' );
638 var commentsIcon = carousel.info.querySelector( '.jp-carousel-icon-comments' );
639
640 function handleInfoToggle() {
641 if ( commentsIcon ) {
642 commentsIcon.classList.remove( 'jp-carousel-selected' );
643 }
644 infoIcon.classList.toggle( 'jp-carousel-selected' );
645
646 if ( commentsContainer ) {
647 commentsContainer.classList.remove( 'jp-carousel-show' );
648 }
649 if ( photoMetaContainer ) {
650 photoMetaContainer.classList.toggle( 'jp-carousel-show' );
651 if ( photoMetaContainer.classList.contains( 'jp-carousel-show' ) ) {
652 extraInfoContainer.classList.add( 'jp-carousel-show' );
653 } else {
654 extraInfoContainer.classList.remove( 'jp-carousel-show' );
655 }
656 }
657 }
658
659 function handleCommentToggle() {
660 if ( infoIcon ) {
661 infoIcon.classList.remove( 'jp-carousel-selected' );
662 }
663 commentsIcon.classList.toggle( 'jp-carousel-selected' );
664
665 if ( photoMetaContainer ) {
666 photoMetaContainer.classList.remove( 'jp-carousel-show' );
667 }
668 if ( commentsContainer ) {
669 commentsContainer.classList.toggle( 'jp-carousel-show' );
670 if ( commentsContainer.classList.contains( 'jp-carousel-show' ) ) {
671 extraInfoContainer.classList.add( 'jp-carousel-show' );
672 } else {
673 extraInfoContainer.classList.remove( 'jp-carousel-show' );
674 }
675 }
676 }
677
678 if (
679 domUtil.closest( target, '.jp-carousel-icon-info' ) ||
680 target.classList.contains( 'jp-carousel-photo-title' )
681 ) {
682 if ( photoMetaContainer && photoMetaContainer.classList.contains( 'jp-carousel-show' ) ) {
683 domUtil.scrollToElement( carousel.overlay, carousel.overlay, handleInfoToggle );
684 } else {
685 handleInfoToggle();
686 domUtil.scrollToElement( carousel.info, carousel.overlay );
687 }
688 }
689
690 if ( domUtil.closest( target, '.jp-carousel-icon-comments' ) ) {
691 if ( commentsContainer && commentsContainer.classList.contains( 'jp-carousel-show' ) ) {
692 domUtil.scrollToElement( carousel.overlay, carousel.overlay, handleCommentToggle );
693 } else {
694 handleCommentToggle();
695 domUtil.scrollToElement( carousel.info, carousel.overlay );
696 }
697 }
698 }
699
700 function processSingleImageGallery() {
701 var images = document.querySelectorAll( 'a img[data-attachment-id]' );
702 Array.prototype.forEach.call( images, function ( image ) {
703 var link = image.parentElement;
704 var container = link.parentElement;
705
706 // Skip if image was already added to gallery by shortcode.
707 if ( container.classList.contains( 'gallery-icon' ) ) {
708 return;
709 }
710
711 // Skip if image is part of a gallery.
712 if ( domUtil.closest( container, galleryItemSelector ) ) {
713 return;
714 }
715
716 // Skip if the parent is not actually a link.
717 if ( ! link.hasAttribute( 'href' ) ) {
718 return;
719 }
720
721 var valid = false;
722
723 // If link points to 'Media File' (ignoring GET parameters) and flag is set, allow it.
724 if (
725 link.getAttribute( 'href' ).split( '?' )[ 0 ] ===
726 image.getAttribute( 'data-orig-file' ).split( '?' )[ 0 ] &&
727 Number( jetpackCarouselStrings.single_image_gallery_media_file ) === 1
728 ) {
729 valid = true;
730 }
731
732 // If link points to 'Attachment Page', allow it.
733 if ( link.getAttribute( 'href' ) === image.getAttribute( 'data-permalink' ) ) {
734 valid = true;
735 }
736
737 // Links to 'Custom URL' or 'Media File' when flag is not set are not valid.
738 if ( ! valid ) {
739 return;
740 }
741
742 makeGalleryImageAccessible( image );
743
744 // Make this node a gallery recognizable by event listener above.
745 link.classList.add( 'single-image-gallery' );
746 // blog_id is needed to allow posting comments to correct blog.
747 link.setAttribute(
748 'data-carousel-extra',
749 JSON.stringify( {
750 blog_id: Number( jetpackCarouselStrings.blog_id ),
751 } )
752 );
753 } );
754 }
755
756 function testForData( el ) {
757 return !! ( el && el.getAttribute( 'data-carousel-extra' ) );
758 }
759
760 function openOrSelectSlide( gal, index ) {
761 if ( ! carousel.isOpen ) {
762 // The `open` method selects the correct slide during the initialization.
763 loadSwiper( gal, { startIndex: index } );
764 } else {
765 selectSlideAtIndex( index );
766 // We have to force swiper to slide to the index onHasChange.
767 swiper.slideTo( index + 1 );
768 }
769 }
770
771 function selectSlideAtIndex( index ) {
772 if ( ! index || index < 0 || index > carousel.slides.length ) {
773 index = 0;
774 }
775 carousel.currentSlide = carousel.slides[ index ];
776
777 var current = carousel.currentSlide;
778 var attachmentId = current.attrs.attachmentId;
779
780 // Load current image immediately
781 loadFullImage( carousel.slides[ index ] );
782
783 // Preload adjacent images in background
784 preloadAdjacentImages( index );
785
786 if (
787 Number( jetpackCarouselStrings.display_background_image ) === 1 &&
788 ! carousel.slides[ index ].backgroundImage
789 ) {
790 loadBackgroundImage( carousel.slides[ index ] );
791 }
792
793 domUtil.hide( carousel.caption );
794 updateTitleCaptionAndDesc( {
795 caption: current.attrs.caption,
796 title: current.attrs.title,
797 desc: current.attrs.desc,
798 } );
799
800 var imageMeta = carousel.slides[ index ].attrs.imageMeta;
801 updateExif( imageMeta );
802 updateFullSizeLink( current );
803
804 if ( Number( jetpackCarouselStrings.display_comments ) === 1 ) {
805 testCommentsOpened( carousel.slides[ index ].attrs.commentsOpened );
806 fetchComments( attachmentId );
807 domUtil.hide( carousel.info.querySelector( '#jp-carousel-comment-post-results' ) );
808 }
809
810 // Update pagination in footer.
811 var pagination = carousel.info.querySelector( '.jp-carousel-pagination' );
812 if ( pagination && carousel.slides.length > 5 ) {
813 var currentPage = index + 1;
814 pagination.innerHTML = '<span>' + currentPage + ' / ' + carousel.slides.length + '</span>';
815 }
816
817 // Record pageview in WP Stats, for each new image loaded full-screen.
818 if ( jetpackCarouselStrings.stats ) {
819 new Image().src =
820 document.location.protocol +
821 '//pixel.wp.com/g.gif?' +
822 jetpackCarouselStrings.stats +
823 '&post=' +
824 encodeURIComponent( attachmentId ) +
825 '&rand=' +
826 Math.random();
827 }
828
829 pageview( attachmentId );
830
831 window.location.hash = lastKnownLocationHash = '#jp-carousel-' + attachmentId;
832 }
833
834 function restoreScroll() {
835 window.scrollTo( window.scrollX || window.pageXOffset || 0, scrollPos || 0 );
836 }
837
838 function closeCarousel() {
839 // Make sure to let the page scroll again.
840 document.body.style.overflow = originalOverflow;
841 document.documentElement.style.overflow = originalHOverflow;
842 clearCommentTextAreaValue();
843
844 disableKeyboardNavigation();
845
846 domUtil.emitEvent( carousel.overlay, 'jp_carousel.beforeClose' );
847 restoreScroll();
848 swiper.destroy();
849 carousel.isOpen = false;
850 // Clear slide data for DOM garbage collection.
851 carousel.slides = [];
852 carousel.currentSlide = undefined;
853 carousel.gallery.innerHTML = '';
854
855 domUtil.fadeOut( carousel.overlay, function () {
856 domUtil.emitEvent( carousel.overlay, 'jp_carousel.afterClose' );
857 } );
858 }
859
860 function calculateMaxSlideDimensions() {
861 return {
862 width: window.innerWidth,
863 height: window.innerHeight - 64, //subtract height of bottom info bar,
864 };
865 }
866
867 function sanitizePhotonUrl( url ) {
868 var urlObj;
869 try {
870 urlObj = new URL( url );
871 // eslint-disable-next-line no-unused-vars
872 } catch ( e ) {
873 return url;
874 }
875
876 var whitelistedPhotonArgs = [
877 'quality',
878 'ssl',
879 'filter',
880 'brightness',
881 'contrast',
882 'colorize',
883 'smooth',
884 ];
885
886 // Get all search params
887 var searchParams = Array.from( urlObj.searchParams.entries() );
888
889 // Clear all existing params
890 urlObj.search = '';
891
892 // Only add back whitelisted params
893 searchParams.forEach( ( [ key, value ] ) => {
894 if ( whitelistedPhotonArgs.includes( key ) ) {
895 urlObj.searchParams.append( key, value );
896 }
897 } );
898
899 return urlObj;
900 }
901
902 function selectBestImageUrl( args ) {
903 if ( typeof args !== 'object' ) {
904 args = {};
905 }
906
907 if ( typeof args.origFile === 'undefined' ) {
908 return '';
909 }
910
911 if ( typeof args.origWidth === 'undefined' || typeof args.maxWidth === 'undefined' ) {
912 return args.origFile;
913 }
914
915 if ( typeof args.mediumFile === 'undefined' || typeof args.largeFile === 'undefined' ) {
916 return args.origFile;
917 }
918
919 // Check if the image is being served by Photon (using a regular expression on the hostname).
920
921 var imageLinkParser = document.createElement( 'a' );
922 imageLinkParser.href = args.largeFile;
923
924 var isPhotonUrl = /^i[0-2]\.wp\.com$/i.test( imageLinkParser.hostname );
925
926 var largeSizeParts = getImageSizeParts( args.largeFile, args.origWidth, isPhotonUrl );
927
928 var largeWidth = parseInt( largeSizeParts[ 0 ], 10 );
929 var largeHeight = parseInt( largeSizeParts[ 1 ], 10 );
930
931 args.origMaxWidth = args.maxWidth;
932 args.origMaxHeight = args.maxHeight;
933
934 // Give devices with a higher devicePixelRatio higher-res images (Retina display = 2, Android phones = 1.5, etc)
935 if ( typeof window.devicePixelRatio !== 'undefined' && window.devicePixelRatio > 1 ) {
936 args.maxWidth = args.maxWidth * window.devicePixelRatio;
937 args.maxHeight = args.maxHeight * window.devicePixelRatio;
938 }
939
940 if ( largeWidth >= args.maxWidth || largeHeight >= args.maxHeight ) {
941 return args.largeFile;
942 }
943
944 var mediumSizeParts = getImageSizeParts( args.mediumFile, args.origWidth, isPhotonUrl );
945 var mediumWidth = parseInt( mediumSizeParts[ 0 ], 10 );
946 var mediumHeight = parseInt( mediumSizeParts[ 1 ], 10 );
947
948 if ( mediumWidth >= args.maxWidth || mediumHeight >= args.maxHeight ) {
949 return args.mediumFile;
950 }
951
952 if ( isPhotonUrl ) {
953 // args.origFile doesn't point to a Photon url, so in this case we use args.largeFile
954 // to return the photon url of the original image.
955 if ( args.largeFile.lastIndexOf( '?' ) === -1 ) {
956 return args.largeFile;
957 }
958
959 // Sanitize the URL to remove non-cosmetic changes like resize, fit, etc.
960 var sanitizedUrl = sanitizePhotonUrl( args.largeFile );
961
962 // If we have a really large image load a smaller version
963 // that is closer to the viewable size
964 if ( args.origWidth > args.maxWidth || args.origHeight > args.maxHeight ) {
965 // @2x the max sizes so we get a high enough resolution for zooming.
966 args.origMaxWidth = args.maxWidth * 2;
967 args.origMaxHeight = args.maxHeight * 2;
968 // Add the fit arg to the list of Photon args.
969 sanitizedUrl.searchParams.set( 'fit', args.origMaxWidth + ',' + args.origMaxHeight );
970 }
971
972 // Return a Photon URL image that's better fitted for the viewport.
973 return sanitizedUrl.toString();
974 }
975
976 return args.origFile;
977 }
978
979 function getImageSizeParts( file, origWidth, isPhotonUrl ) {
980 var size = isPhotonUrl
981 ? file.replace( /.*=([\d]+%2C[\d]+).*$/, '$1' )
982 : file.replace( /.*-([\d]+x[\d]+)\..+$/, '$1' );
983
984 var sizeParts;
985 if ( size !== file ) {
986 sizeParts = isPhotonUrl ? size.split( '%2C' ) : size.split( 'x' );
987 } else {
988 sizeParts = [ origWidth, 0 ];
989 }
990
991 // If one of the dimensions is set to 9999, then the actual value of that dimension can't be retrieved from the url.
992 // In that case, we set the value to 0.
993 if ( sizeParts[ 0 ] === '9999' ) {
994 sizeParts[ 0 ] = '0';
995 }
996
997 if ( sizeParts[ 1 ] === '9999' ) {
998 sizeParts[ 1 ] = '0';
999 }
1000
1001 return sizeParts;
1002 }
1003
1004 /**
1005 * Returns a number in a fraction format that represents the shutter speed.
1006 * @param Number speed
1007 * @return String
1008 */
1009 function formatShutterSpeed( speed ) {
1010 var denominator;
1011
1012 // round to one decimal if value > 1s by multiplying it by 10, rounding, then dividing by 10 again
1013 if ( speed >= 1 ) {
1014 return Math.round( speed * 10 ) / 10 + 's';
1015 }
1016
1017 // If the speed is less than one, we find the denominator by inverting
1018 // the number. Since cameras usually use rational numbers as shutter
1019 // speeds, we should get a nice round number. Or close to one in cases
1020 // like 1/30. So we round it.
1021 denominator = Math.round( 1 / speed );
1022
1023 return '1/' + denominator + 's';
1024 }
1025
1026 function parseTitleOrDesc( value ) {
1027 if ( ! value.match( ' ' ) && value.match( '_' ) ) {
1028 return '';
1029 }
1030
1031 return value;
1032 }
1033
1034 function updateTitleCaptionAndDesc( data ) {
1035 var caption = '';
1036 var title = '';
1037 var desc = '';
1038 var captionMainElement;
1039 var captionInfoExtraElement;
1040 var titleElement;
1041 var descriptionElement;
1042
1043 captionMainElement = carousel.overlay.querySelector( '.jp-carousel-photo-caption' );
1044 captionInfoExtraElement = carousel.overlay.querySelector( '.jp-carousel-caption' );
1045
1046 titleElement = carousel.overlay.querySelector( '.jp-carousel-photo-title' );
1047 descriptionElement = carousel.overlay.querySelector( '.jp-carousel-photo-description' );
1048
1049 domUtil.hide( captionMainElement );
1050 domUtil.hide( captionInfoExtraElement );
1051 domUtil.hide( titleElement );
1052 domUtil.hide( descriptionElement );
1053
1054 caption = parseTitleOrDesc( data.caption ) || '';
1055 title = parseTitleOrDesc( data.title ) || '';
1056 desc = parseTitleOrDesc( data.desc ) || '';
1057
1058 if ( caption || title || desc ) {
1059 if ( caption ) {
1060 captionMainElement.innerHTML = caption;
1061 captionInfoExtraElement.innerHTML = caption;
1062
1063 domUtil.show( captionMainElement );
1064 domUtil.show( captionInfoExtraElement );
1065 }
1066
1067 if ( domUtil.stripHTML( caption ) === domUtil.stripHTML( title ) ) {
1068 title = '';
1069 }
1070
1071 if ( domUtil.stripHTML( caption ) === domUtil.stripHTML( desc ) ) {
1072 desc = '';
1073 }
1074
1075 if ( domUtil.stripHTML( title ) === domUtil.stripHTML( desc ) ) {
1076 desc = '';
1077 }
1078
1079 if ( desc ) {
1080 descriptionElement.innerHTML = desc;
1081 domUtil.show( descriptionElement );
1082
1083 if ( ! title && ! caption ) {
1084 captionMainElement.innerHTML = domUtil.stripHTML( desc );
1085 domUtil.show( captionMainElement );
1086 }
1087 }
1088
1089 if ( title ) {
1090 var plainTitle = domUtil.stripHTML( title );
1091 titleElement.innerHTML = plainTitle;
1092
1093 if ( ! caption ) {
1094 captionMainElement.innerHTML = plainTitle;
1095 captionInfoExtraElement.innerHTML = plainTitle;
1096
1097 domUtil.show( captionMainElement );
1098 }
1099
1100 domUtil.show( titleElement );
1101 }
1102 }
1103 }
1104
1105 // updateExif updates the contents of the exif UL (.jp-carousel-image-exif)
1106 function updateExif( meta ) {
1107 if ( ! meta || Number( jetpackCarouselStrings.display_exif ) !== 1 ) {
1108 return false;
1109 }
1110
1111 var ul = carousel.info.querySelector( '.jp-carousel-image-meta ul.jp-carousel-image-exif' );
1112 var html = '';
1113
1114 for ( var key in meta ) {
1115 var val = meta[ key ];
1116 var metaKeys = jetpackCarouselStrings.meta_data || [];
1117
1118 if ( parseFloat( val ) === 0 || ! val.length || metaKeys.indexOf( key ) === -1 ) {
1119 continue;
1120 }
1121
1122 switch ( key ) {
1123 case 'focal_length':
1124 val = val + 'mm';
1125 break;
1126 case 'shutter_speed':
1127 val = formatShutterSpeed( val );
1128 break;
1129 case 'aperture':
1130 val = 'f/' + val;
1131 break;
1132 }
1133
1134 html += '<li><h5>' + jetpackCarouselStrings[ key ] + '</h5>' + val + '</li>';
1135 }
1136
1137 ul.innerHTML = html;
1138 ul.style.removeProperty( 'display' );
1139 }
1140
1141 // Update the contents of the jp-carousel-image-download link
1142 function updateFullSizeLink( currentSlide ) {
1143 if ( ! currentSlide ) {
1144 return false;
1145 }
1146 var original;
1147 var origSize = [ currentSlide.attrs.origWidth, currentSlide.attrs.origHeight ];
1148 var imageLinkParser = document.createElement( 'a' );
1149
1150 imageLinkParser.href = currentSlide.attrs.src.replace( /\?.+$/, '' );
1151
1152 // Is this a Photon URL?
1153 if ( imageLinkParser.hostname.match( /^i[\d]{1}\.wp\.com$/i ) !== null ) {
1154 original = imageLinkParser.href;
1155 } else {
1156 original = currentSlide.attrs.origFile.replace( /\?.+$/, '' );
1157 }
1158
1159 var downloadText = carousel.info.querySelector( '.jp-carousel-download-text' );
1160 var permalink = carousel.info.querySelector( '.jp-carousel-image-download' );
1161
1162 downloadText.innerHTML = util.applyReplacements(
1163 jetpackCarouselStrings.download_original,
1164 origSize
1165 );
1166 permalink.setAttribute( 'href', original );
1167 permalink.style.removeProperty( 'display' );
1168 }
1169
1170 function testCommentsOpened( opened ) {
1171 var commentForm = carousel.info.querySelector( '#jp-carousel-comment-form-container' );
1172 var isOpened = parseInt( opened, 10 ) === 1;
1173
1174 if ( isOpened ) {
1175 domUtil.fadeIn( commentForm );
1176 } else {
1177 domUtil.fadeOut( commentForm );
1178 }
1179 }
1180
1181 function fetchComments( attachmentId, offset ) {
1182 var shouldClear = offset === undefined;
1183 var commentsIndicator = carousel.info.querySelector(
1184 '.jp-carousel-icon-comments .jp-carousel-has-comments-indicator'
1185 );
1186
1187 commentsIndicator.classList.remove( 'jp-carousel-show' );
1188
1189 clearInterval( commentInterval );
1190
1191 if ( ! attachmentId ) {
1192 return;
1193 }
1194
1195 if ( ! offset || offset < 1 ) {
1196 offset = 0;
1197 }
1198
1199 var comments = carousel.info.querySelector( '.jp-carousel-comments' );
1200 var commentsLoading = carousel.info.querySelector( '#jp-carousel-comments-loading' );
1201 domUtil.show( commentsLoading );
1202
1203 if ( shouldClear ) {
1204 domUtil.hide( comments );
1205 comments.innerHTML = '';
1206 }
1207
1208 var xhr = new XMLHttpRequest();
1209 var url =
1210 jetpackCarouselStrings.ajaxurl +
1211 '?action=get_attachment_comments' +
1212 '&nonce=' +
1213 jetpackCarouselStrings.nonce +
1214 '&id=' +
1215 attachmentId +
1216 '&offset=' +
1217 offset;
1218 xhr.open( 'GET', url );
1219 xhr.setRequestHeader( 'X-Requested-With', 'XMLHttpRequest' );
1220
1221 var onError = function () {
1222 domUtil.fadeIn( comments );
1223 domUtil.fadeOut( commentsLoading );
1224 };
1225
1226 xhr.onload = function () {
1227 // Ignore the results if they arrive late and we're now on a different slide.
1228 if (
1229 ! carousel.currentSlide ||
1230 carousel.currentSlide.attrs.attachmentId !== attachmentId
1231 ) {
1232 return;
1233 }
1234
1235 var isSuccess = xhr.status >= 200 && xhr.status < 300;
1236 var data;
1237 try {
1238 data = JSON.parse( xhr.responseText );
1239 } catch {
1240 // Do nothing.
1241 }
1242
1243 if ( ! isSuccess || ! data || ! Array.isArray( data ) ) {
1244 return onError();
1245 }
1246
1247 if ( shouldClear ) {
1248 comments.innerHTML = '';
1249 }
1250
1251 for ( var i = 0; i < data.length; i++ ) {
1252 var entry = data[ i ];
1253 var comment = document.createElement( 'div' );
1254 comment.classList.add( 'jp-carousel-comment' );
1255 comment.setAttribute( 'id', 'jp-carousel-comment-' + entry.id );
1256 comment.innerHTML =
1257 '<div class="comment-gravatar">' +
1258 entry.gravatar_markup +
1259 '</div>' +
1260 '<div class="comment-content">' +
1261 '<div class="comment-author">' +
1262 entry.author_markup +
1263 '</div>' +
1264 '<div class="comment-date">' +
1265 entry.date_gmt +
1266 '</div>' +
1267 entry.content +
1268 '</div>';
1269 comments.appendChild( comment );
1270
1271 // Set the interval to check for a new page of comments.
1272 clearInterval( commentInterval );
1273 commentInterval = setInterval( function () {
1274 if ( carousel.container.scrollTop + 150 > window.innerHeight ) {
1275 fetchComments( attachmentId, offset + 10 );
1276 clearInterval( commentInterval );
1277 }
1278 }, 300 );
1279 }
1280
1281 if ( data.length > 0 ) {
1282 domUtil.show( comments );
1283 commentsIndicator.innerText = data.length;
1284 commentsIndicator.classList.add( 'jp-carousel-show' );
1285 }
1286
1287 domUtil.hide( commentsLoading );
1288 };
1289
1290 xhr.onerror = onError;
1291
1292 xhr.send();
1293 }
1294
1295 function loadFullImage( slide ) {
1296 var el = slide.el;
1297 var attrs = slide.attrs;
1298 var image = el.querySelector( 'img' );
1299
1300 if ( ! image.hasAttribute( 'data-loaded' ) ) {
1301 var hasPreview = !! attrs.previewImage;
1302 var thumbSize = attrs.thumbSize;
1303
1304 if ( ! hasPreview || ( thumbSize && el.offsetWidth > thumbSize.width ) ) {
1305 image.src = attrs.src;
1306 } else {
1307 image.src = attrs.previewImage;
1308 }
1309
1310 image.setAttribute( 'itemprop', 'image' );
1311 image.setAttribute( 'data-loaded', 1 );
1312 }
1313 }
1314
1315 function preloadAdjacentImages( currentIndex ) {
1316 var indicesToPreload = [];
1317 var totalSlides = carousel.slides.length;
1318
1319 // Only preload adjacent images if we have more than one slide (matching loop condition)
1320 if ( totalSlides > 1 ) {
1321 // Previous image (with loop handling)
1322 var prevIndex = currentIndex > 0 ? currentIndex - 1 : totalSlides - 1;
1323 indicesToPreload.push( prevIndex );
1324
1325 // Next image (with loop handling)
1326 var nextIndex = currentIndex < totalSlides - 1 ? currentIndex + 1 : 0;
1327 indicesToPreload.push( nextIndex );
1328 }
1329
1330 indicesToPreload.forEach( function ( index ) {
1331 var slide = carousel.slides[ index ];
1332 if ( slide ) {
1333 // Load in background without showing
1334 loadFullImage( slide );
1335
1336 // Also load background image if enabled
1337 if (
1338 Number( jetpackCarouselStrings.display_background_image ) === 1 &&
1339 ! slide.backgroundImage
1340 ) {
1341 loadBackgroundImage( slide );
1342 }
1343 }
1344 } );
1345 }
1346
1347 function loadBackgroundImage( slide ) {
1348 var currentSlide = slide.el;
1349
1350 if ( swiper && swiper.slides ) {
1351 currentSlide = swiper.slides[ swiper.activeIndex ];
1352 }
1353
1354 var image = slide.attrs.originalElement;
1355 var isLoaded = image.complete && image.naturalHeight !== 0;
1356
1357 if ( isLoaded ) {
1358 applyBackgroundImage( slide, currentSlide, image );
1359 return;
1360 }
1361
1362 image.onload = function () {
1363 applyBackgroundImage( slide, currentSlide, image );
1364 };
1365 }
1366
1367 function applyBackgroundImage( slide, currentSlide, image ) {
1368 var url = util.getBackgroundImage( image );
1369 slide.backgroundImage = url;
1370 currentSlide.style.backgroundImage = 'url(' + url + ')';
1371 currentSlide.style.backgroundSize = 'cover';
1372 }
1373
1374 function clearCommentTextAreaValue() {
1375 if ( carousel.commentField ) {
1376 carousel.commentField.value = '';
1377 }
1378 }
1379
1380 function getOriginalDimensions( el ) {
1381 var size = el.getAttribute( 'data-orig-size' ) || '';
1382
1383 if ( size ) {
1384 var parts = size.split( ',' );
1385 return { width: parseInt( parts[ 0 ], 10 ), height: parseInt( parts[ 1 ], 10 ) };
1386 }
1387 return {
1388 width: el.getAttribute( 'data-original-width' ) || el.getAttribute( 'width' ) || undefined,
1389 height:
1390 el.getAttribute( 'data-original-height' ) || el.getAttribute( 'height' ) || undefined,
1391 };
1392 }
1393
1394 function initCarouselSlides( items, startIndex ) {
1395 carousel.slides = [];
1396
1397 var max = calculateMaxSlideDimensions();
1398
1399 // If the startIndex is not 0 then preload the clicked image first.
1400 if ( startIndex !== 0 && items[ startIndex ].getAttribute( 'data-gallery-src' ) !== null ) {
1401 var img = new Image();
1402 img.src = items[ startIndex ].getAttribute( 'data-gallery-src' );
1403 }
1404
1405 var useInPageThumbnails = !! domUtil.closest( items[ 0 ], '.tiled-gallery.type-rectangular' );
1406
1407 // create the 'slide'
1408 Array.prototype.forEach.call( items, function ( item, i ) {
1409 var permalinkEl = domUtil.closest( item, 'a' );
1410 var origFile = item.getAttribute( 'data-orig-file' ) || item.getAttribute( 'src-orig' );
1411 var attrID =
1412 item.getAttribute( 'data-attachment-id' ) || item.getAttribute( 'data-id' ) || '0';
1413 var caption = document.querySelector(
1414 'img[data-attachment-id="' + attrID + '"] + figcaption'
1415 );
1416
1417 if ( caption ) {
1418 caption = caption.innerHTML;
1419 } else {
1420 caption = item.getAttribute( 'data-image-caption' );
1421 }
1422
1423 var attrs = {
1424 originalElement: item,
1425 attachmentId: attrID,
1426 commentsOpened: item.getAttribute( 'data-comments-opened' ) || '0',
1427 imageMeta: domUtil.getJSONAttribute( item, 'data-image-meta' ) || {},
1428 title: item.getAttribute( 'data-image-title' ) || '',
1429 desc: item.getAttribute( 'data-image-description' ) || '',
1430 mediumFile: item.getAttribute( 'data-medium-file' ) || '',
1431 largeFile: item.getAttribute( 'data-large-file' ) || '',
1432 origFile: origFile || '',
1433 thumbSize: { width: item.naturalWidth, height: item.naturalHeight },
1434 caption: caption || '',
1435 permalink: permalinkEl && permalinkEl.getAttribute( 'href' ),
1436 src: origFile || item.getAttribute( 'src' ) || '',
1437 };
1438
1439 var tiledGalleryItem = domUtil.closest( item, '.tiled-gallery-item' );
1440 var tiledCaptionEl =
1441 tiledGalleryItem && tiledGalleryItem.querySelector( '.tiled-gallery-caption' );
1442 var tiledCaption = tiledCaptionEl && tiledCaptionEl.innerHTML;
1443 if ( tiledCaption ) {
1444 attrs.caption = tiledCaption;
1445 }
1446
1447 var origDimensions = getOriginalDimensions( item );
1448
1449 attrs.origWidth = origDimensions.width || attrs.thumbSize.width;
1450 attrs.origHeight = origDimensions.height || attrs.thumbSize.height;
1451
1452 if ( typeof wpcom !== 'undefined' && wpcom.carousel && wpcom.carousel.generateImgSrc ) {
1453 attrs.src = wpcom.carousel.generateImgSrc( item, max );
1454 } else {
1455 attrs.src = selectBestImageUrl( {
1456 origFile: attrs.src,
1457 origWidth: attrs.origWidth,
1458 origHeight: attrs.origHeight,
1459 maxWidth: max.width,
1460 maxHeight: max.height,
1461 mediumFile: attrs.mediumFile,
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 selectSlideAtIndex( swiper.realIndex );
1611 carousel.overlay.classList.remove( 'jp-carousel-hide-controls' );
1612 } );
1613
1614 swiper.on( 'zoomChange', function ( swiper, scale ) {
1615 if ( scale > 1 ) {
1616 carousel.overlay.classList.add( 'jp-carousel-hide-controls' );
1617 }
1618
1619 if ( scale === 1 ) {
1620 carousel.overlay.classList.remove( 'jp-carousel-hide-controls' );
1621 }
1622 } );
1623
1624 swiper.on( 'doubleTap', function ( swiper ) {
1625 clearTimeout( tapTimeout );
1626 if ( swiper.zoom.scale === 1 ) {
1627 var zoomTimeout = setTimeout( function () {
1628 carousel.overlay.classList.remove( 'jp-carousel-hide-controls' );
1629 clearTimeout( zoomTimeout );
1630 }, 150 );
1631 }
1632 } );
1633
1634 swiper.on( 'tap', function () {
1635 if ( swiper.zoom.scale > 1 ) {
1636 tapTimeout = setTimeout( function () {
1637 carousel.overlay.classList.toggle( 'jp-carousel-hide-controls' );
1638 }, 150 );
1639 }
1640 } );
1641
1642 domUtil.fadeIn( carousel.overlay, function () {
1643 domUtil.emitEvent( carousel.overlay, 'jp_carousel.afterOpen' );
1644 } );
1645 }
1646
1647 // Register the event listeners for starting the gallery
1648 document.body.addEventListener( 'click', handleInteraction );
1649 document.body.addEventListener( 'keydown', handleInteraction );
1650 document.querySelectorAll( galleryItemSelector + 'img' ).forEach( function ( galleryImage ) {
1651 if ( shouldOpenModal( galleryImage ) ) {
1652 makeGalleryImageAccessible( galleryImage );
1653 }
1654 } );
1655
1656 function handleInteraction( e ) {
1657 if ( e.type === 'click' ) {
1658 handleClick( e );
1659 return;
1660 }
1661
1662 if ( e.type === 'keydown' ) {
1663 const parentElement = document.activeElement.parentElement;
1664 const isParentCarouselContainer =
1665 parentElement && parentElement.classList.contains( 'tiled-gallery__item' );
1666
1667 if ( ( e.key === ' ' || e.key === 'Enter' ) && isParentCarouselContainer ) {
1668 handleClick( e );
1669 }
1670 }
1671 }
1672
1673 function shouldOpenModal( el ) {
1674 var parent = el.parentElement;
1675 var grandparent = parent.parentElement;
1676
1677 // If Gallery is made up of individual Image blocks check for custom link before
1678 // loading carousel. The custom link may be the parent or could be a descendant
1679 // of the parent if the image has rounded corners.
1680 var parentHref = null;
1681 if ( grandparent && grandparent.classList.contains( 'wp-block-image' ) ) {
1682 parentHref = parent.getAttribute( 'href' );
1683 } else if (
1684 parent &&
1685 parent.classList.contains( 'wp-block-image' ) &&
1686 parent.querySelector( ':scope > a' )
1687 ) {
1688 parentHref = parent.querySelector( ':scope > a' ).getAttribute( 'href' );
1689 }
1690
1691 // If the link does not point to the attachment or media file then assume Image has
1692 // a custom link so don't load the carousel.
1693 if (
1694 parentHref &&
1695 parentHref.split( '?' )[ 0 ] !== el.getAttribute( 'data-orig-file' ).split( '?' )[ 0 ] &&
1696 parentHref !== el.getAttribute( 'data-permalink' )
1697 ) {
1698 return false;
1699 }
1700
1701 // Do not open the modal if we are looking at a gallery caption from before WP5, which may contain a link.
1702 if ( parent.classList.contains( 'gallery-caption' ) ) {
1703 return false;
1704 }
1705
1706 // Do not open the modal if we are looking at a caption of a gallery block, which may contain a link.
1707 if ( domUtil.matches( parent, 'figcaption' ) ) {
1708 return false;
1709 }
1710
1711 return true;
1712 }
1713
1714 function handleClick( e ) {
1715 var isCompatible =
1716 window.CSS && window.CSS.supports && window.CSS.supports( 'display', 'grid' );
1717
1718 // IE11 support is being dropped in August 2021. The new swiper.js libray is not IE11 compat
1719 // so just default to opening individual image attachment/media pages for IE.
1720 if ( ! isCompatible ) {
1721 return;
1722 }
1723
1724 var target = e.target;
1725 var gallery = domUtil.closest( target, gallerySelector );
1726
1727 if ( gallery ) {
1728 if ( ! testForData( gallery ) ) {
1729 return;
1730 }
1731
1732 if ( ! shouldOpenModal( target ) ) {
1733 return;
1734 }
1735
1736 // Set height to auto.
1737 // Fix some themes where closing carousel brings view back to top.
1738 document.documentElement.style.height = 'auto';
1739
1740 e.preventDefault();
1741
1742 // Stopping propagation in case there are parent elements
1743 // with .gallery or .tiled-gallery class
1744 e.stopPropagation();
1745
1746 var item = domUtil.closest( target, itemSelector );
1747 var index = Array.prototype.indexOf.call( gallery.querySelectorAll( itemSelector ), item );
1748 loadSwiper( gallery, { startIndex: index } );
1749 }
1750 }
1751
1752 // Handle lightbox (single image gallery) for images linking to 'Attachment Page'.
1753 if ( Number( jetpackCarouselStrings.single_image_gallery ) === 1 ) {
1754 processSingleImageGallery();
1755 document.body.addEventListener( 'is.post-load', function () {
1756 processSingleImageGallery();
1757 } );
1758 }
1759
1760 // Makes carousel work on page load and when back button leads to same URL with carousel hash
1761 // (i.e. no actual document.ready trigger).
1762 window.addEventListener( 'hashchange', function () {
1763 var hashRegExp = /jp-carousel-(\d+)/;
1764
1765 if ( ! window.location.hash || ! hashRegExp.test( window.location.hash ) ) {
1766 if ( carousel.isOpen ) {
1767 closeCarousel();
1768 }
1769
1770 return;
1771 }
1772
1773 if ( window.location.hash === lastKnownLocationHash && carousel.isOpen ) {
1774 return;
1775 }
1776
1777 if ( window.location.hash && carousel.gallery && ! carousel.isOpen && history.back ) {
1778 history.back();
1779 return;
1780 }
1781
1782 lastKnownLocationHash = window.location.hash;
1783 var matchList = window.location.hash.match( hashRegExp );
1784 var attachmentId = parseInt( matchList[ 1 ], 10 );
1785 var galleries = document.querySelectorAll( gallerySelector );
1786
1787 // Find the first thumbnail that matches the attachment ID in the location
1788 // hash, then open the gallery that contains it.
1789 for ( var i = 0; i < galleries.length; i++ ) {
1790 var gallery = galleries[ i ];
1791 var selected;
1792
1793 var images = gallery.querySelectorAll( 'img' );
1794 for ( var j = 0; j < images.length; j++ ) {
1795 if (
1796 parseInt( images[ j ].getAttribute( 'data-attachment-id' ), 10 ) === attachmentId ||
1797 parseInt( images[ j ].getAttribute( 'data-id' ), 10 ) === attachmentId
1798 ) {
1799 selected = j;
1800 break;
1801 }
1802 }
1803
1804 if ( selected !== undefined ) {
1805 openOrSelectSlide( gallery, selected );
1806 break;
1807 }
1808 }
1809 } );
1810
1811 if ( window.location.hash ) {
1812 domUtil.emitEvent( window, 'hashchange' );
1813 }
1814 }
1815
1816 if ( document.readyState !== 'loading' ) {
1817 init();
1818 } else {
1819 document.addEventListener( 'DOMContentLoaded', init );
1820 }
1821 } )();
1822