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