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