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