PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 14.1.1
Jetpack – WP Security, Backup, Speed, & Growth v14.1.1
12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 13.8.3 13.9.2 14.0.1 14.1.1 14.2.2 14.3.1 14.4.2 14.5.1 14.6.1 14.7.1 14.8.1 14.9.2 15.0.3 15.1.2 15.2.1 15.3.2 15.4.1 15.5.1 15.6.1 15.7.2 15.8.1 15.9.2 16.0.2 16.1.3 16.2-a.5 16.2-a.3 16.1.2 16.2-a.1 16.1.1 16.1 16.1-beta 16.1-beta.2 16.1-beta.3 16.1-a.5 16.1-a.3 16.0.1 16.1-a.1 16.0 16.0-beta 16.0-a.7 16.0-a.5 15.9.1 16.0-a.3 16.0-a.1 15.9 15.9-beta 15.9-a.7 15.9-a.5 15.9-a.3 15.9-a.1 15.8 15.8-beta 15.8-a.7 15.8-a.5 5.2.5 5.3.4 5.4.4 5.5.5 5.6.5 5.7.5 5.8.4 5.9.4 6.0.4 6.1 6.1.1 6.1.2 6.1.3 6.1.4 6.1.5 6.2 6.2.1 6.2.2 6.2.3 6.2.4 6.2.5 6.3 6.3.1 6.3.2 6.3.3 6.3.4 6.3.5 6.3.6 6.3.7 6.4 6.4.1 6.4.2 6.4.3 6.4.4 6.4.5 6.4.6 6.5 6.5.1 6.5.2 6.5.3 6.5.4 6.6 6.6.1 6.6.2 6.6.3 6.6.4 6.6.5 6.7 6.7.1 6.7.2 6.7.3 6.7.4 6.8 6.8.1 6.8.2 6.8.3 6.8.4 6.8.5 6.9 6.9.1 6.9.2 6.9.3 6.9.4 7.0 7.0.1 7.0.2 7.0.3 7.0.4 7.0.5 7.1 7.1.1 7.1.2 7.1.3 7.1.4 7.1.5 7.2 7.2.1 7.2.1.1 7.2.2 7.2.3 7.2.4 7.2.5 7.3 7.3.0.1 7.3.1 7.3.1.1 7.3.2 7.3.3 7.3.4 7.3.5 7.4 7.4.1 7.4.2 7.4.3 7.4.4 7.4.5 7.5 7.5.0.1 7.5.1 7.5.2 7.5.3 7.5.4 7.5.5 7.5.6 7.5.7 7.6 7.6.1 7.6.2 7.6.3 7.6.4 7.7 7.7.1 7.7.2 7.7.3 7.7.4 7.7.5 7.7.6 7.8 7.8.1 7.8.2 7.8.3 7.8.4 7.9 7.9.1 7.9.2 7.9.3 7.9.4 8.0 8.0.1 8.0.2 8.0.3 8.1 8.1.1 8.1.2 8.1.3 8.1.4 8.2 8.2.0.1 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.2.6 8.3 8.3.1 8.3.2 8.3.3 8.4 8.4.1 8.4.2 8.4.3 8.4.4 8.4.5 8.5 8.5.1 8.5.2 8.5.3 8.6 8.6.1 8.6.2 8.6.3 8.6.4 8.7 8.7.0.1 8.7.1 8.7.2 8.7.3 8.7.4 8.8 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.9 8.9.1 8.9.2 8.9.3 8.9.4 9.0 9.0.1 9.0.2 9.0.3 9.0.4 9.0.5 9.1 9.1.1 9.1.2 9.1.3 9.2 9.2.1 9.2.2 9.2.3 9.2.4 9.3 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.4 9.4.1 9.4.2 9.4.3 9.4.4 9.5 9.5.1 9.5.2 9.5.3 9.5.4 9.5.5 9.6 9.6.1 9.6.2 9.6.3 9.6.4 9.7 9.7.1 9.7.2 15.7-beta.2 9.7.3 15.7.1 9.8 15.8-a.1 9.8.1 15.8-a.3 9.8.2 2.0.9 9.8.3 2.1.7 9.9 2.2.10 9.9.1 2.3.10 9.9.2 2.4.7 9.9.3 2.5.5 2.6.6 2.7.5 2.8.5 2.9.6 3.0.6 3.1.5 3.2.5 3.3.6 3.4.6 3.5.6 3.6.4 3.7.5 3.8.5 3.9.10 4.0.7 4.1.4 4.2.5 4.3.5 4.4.5 4.5.3 4.6.3 4.7.4 4.8.5 4.9.3 5.0.3 5.1.4 trunk 10.0 10.0.1 10.0.2 10.1 10.1.1 10.1.2 10.2 10.2.1 10.2.2 10.2.3 10.3 10.3.1 10.3.2 10.4 10.4.1 10.4.2 10.5 10.5.1 10.5.2 10.5.3 10.6 10.6.1 10.6.2 10.7 10.7.1 10.7.2 10.8 10.8.1 10.8.2 10.9 10.9.1 10.9.2 10.9.3 11.0 11.0.1 11.0.2 11.1 11.1.1 11.1.2 11.1.3 11.1.4 11.2 11.2.1 11.2.2 11.3 11.3.1 11.3.2 11.3.3 11.3.4 11.4 11.4.1 11.4.2 11.5 11.5.1 11.5.2 11.5.3 11.6 11.6.1 11.6.2 11.7 11.7.1 11.7.2 11.7.3 11.8 11.8.3 11.8.4 11.8.5 11.8.6 11.9 11.9.1 11.9.2 11.9.3 12.0 12.0.1 12.0.2 12.1 12.1.1 12.1.2 12.2 12.2.1 12.2.2 12.3 12.3.1 12.4 12.4.1 12.5 12.5.1 12.6 12.6.1 12.6.2 12.6.3 12.7 12.7.1 12.7.2 12.8 12.8.1 12.8.2 12.9 12.9.1 12.9.2 12.9.3 12.9.4 13.0 13.0.1 13.1 13.1.1 13.1.2 13.1.3 13.1.4 13.2 13.2.1 13.2.2 13.2.3 13.3 13.3.1 13.3.2 13.4 13.4.1 13.4.2 13.4.3 13.4.4 13.5 13.5.1 13.6 13.6.1 13.7 13.7.1 13.8 13.8.1 13.8.2 13.9 13.9.1 14.0 14.1 14.2 14.2.1 14.3 14.4 14.4.1 14.5 14.6 14.7 14.8 14.9 14.9.1 15.0 15.0.1 15.0.2 15.1 15.1.1 15.2 15.3 15.3.1 15.4 15.5 15.6 15.7 15.7-a.1 15.7-a.3 15.7-a.5 15.7-a.7 15.7-beta
jetpack / modules / carousel / jetpack-carousel.js
jetpack / modules / carousel Last commit date
jetpack-carousel-rtl.css 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
1763 lines
1 /* global wpcom, jetpackCarouselStrings, DocumentTouch */
2
3 ( function () {
4 'use strict';
5 var swiper;
6 /////////////////////////////////////
7 // Utility functions
8 /////////////////////////////////////
9 var util = ( function () {
10 var noop = function () {};
11
12 function texturize( text ) {
13 // Ensure we get a string.
14 text = text + '';
15 text = text.replace( /'/g, '’' ).replace( /'/g, '’' );
16 text = text
17 .replace( /"/g, '”' )
18 .replace( /"/g, '”' )
19 .replace( /"/g, '”' )
20 .replace( /[\u201D]/g, '”' );
21 // Untexturize allowed HTML tags params double-quotes.
22 text = text.replace( /([\w]+)=&#[\d]+;(.+?)&#[\d]+;/g, '$1="$2"' );
23 return text.trim();
24 }
25
26 function applyReplacements( text, replacements ) {
27 if ( ! text ) {
28 return;
29 }
30 if ( ! replacements ) {
31 return text;
32 }
33 return text.replace( /{(\d+)}/g, function ( match, number ) {
34 return typeof replacements[ number ] !== 'undefined' ? replacements[ number ] : match;
35 } );
36 }
37
38 function getBackgroundImage( imgEl ) {
39 var canvas = document.createElement( 'canvas' ),
40 context = canvas.getContext && canvas.getContext( '2d' );
41
42 if ( ! imgEl ) {
43 return;
44 }
45
46 context.filter = 'blur(20px) ';
47 context.drawImage( imgEl, 0, 0 );
48 var url = canvas.toDataURL( 'image/png' );
49 canvas = null;
50
51 return url;
52 }
53
54 return {
55 noop: noop,
56 texturize: texturize,
57 applyReplacements: applyReplacements,
58 getBackgroundImage: getBackgroundImage,
59 };
60 } )();
61
62 /////////////////////////////////////
63 // DOM-related utility functions
64 /////////////////////////////////////
65 var domUtil = ( function () {
66 // Helper matches function (not a polyfill), compatible with IE 11.
67 function matches( el, sel ) {
68 if ( Element.prototype.matches ) {
69 return el.matches( sel );
70 }
71
72 if ( Element.prototype.msMatchesSelector ) {
73 return el.msMatchesSelector( sel );
74 }
75 }
76
77 // Helper closest parent node function (not a polyfill) based on
78 // https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill
79 function closest( el, sel ) {
80 if ( el.closest ) {
81 return el.closest( sel );
82 }
83
84 var current = el;
85
86 do {
87 if ( matches( current, sel ) ) {
88 return current;
89 }
90 current = current.parentElement || current.parentNode;
91 } while ( current !== null && current.nodeType === 1 );
92
93 return null;
94 }
95
96 function hide( el ) {
97 if ( el ) {
98 el.style.display = 'none';
99 }
100 }
101
102 function show( el ) {
103 if ( el ) {
104 // Everything we show and hide in Carousel is currently a block,
105 // so we can make this really straightforward.
106 el.style.display = 'block';
107 }
108 }
109
110 function fade( el, start, end, callback ) {
111 if ( ! el ) {
112 return callback();
113 }
114
115 // Prepare for transition.
116 // Ensure the item is in the render tree, in its initial state.
117 el.style.removeProperty( 'display' );
118 el.style.opacity = start;
119 el.style.pointerEvents = 'none';
120
121 var animate = function ( t0, duration ) {
122 var t = performance.now();
123 var diff = t - t0;
124 var ratio = diff / duration;
125
126 if ( ratio < 1 ) {
127 el.style.opacity = start + ( end - start ) * ratio;
128 requestAnimationFrame( () => animate( t0, duration ) );
129 } else {
130 el.style.opacity = end;
131 el.style.removeProperty( 'pointer-events' );
132 callback();
133 }
134 };
135
136 requestAnimationFrame( function () {
137 // Double rAF for browser compatibility.
138 requestAnimationFrame( function () {
139 animate( performance.now(), 200 );
140 } );
141 } );
142 }
143
144 function fadeIn( el, callback ) {
145 callback = callback || util.noop;
146 fade( el, 0, 1, callback );
147 }
148
149 function fadeOut( el, callback ) {
150 callback = callback || util.noop;
151 fade( el, 1, 0, function () {
152 if ( el ) {
153 el.style.display = 'none';
154 }
155 callback();
156 } );
157 }
158
159 function emitEvent( el, type, detail ) {
160 var e;
161 try {
162 e = new CustomEvent( type, {
163 bubbles: true,
164 cancelable: true,
165 detail: detail || null,
166 } );
167 } catch ( err ) {
168 e = document.createEvent( 'CustomEvent' );
169 e.initCustomEvent( type, true, true, detail || null );
170 }
171 el.dispatchEvent( e );
172 }
173
174 // From: https://easings.net/#easeInOutQuad
175 function easeInOutQuad( num ) {
176 return num < 0.5 ? 2 * num * num : 1 - Math.pow( -2 * num + 2, 2 ) / 2;
177 }
178
179 function getFooterClearance( container ) {
180 var footer = container.querySelector( '.jp-carousel-info-footer' );
181 var infoArea = container.querySelector( '.jp-carousel-info-extra' );
182 var contentArea = container.querySelector( '.jp-carousel-info-content-wrapper' );
183
184 if ( footer && infoArea && contentArea ) {
185 var styles = window.getComputedStyle( infoArea );
186 var padding = parseInt( styles.paddingTop, 10 ) + parseInt( styles.paddingBottom, 10 );
187 padding = isNaN( padding ) ? 0 : padding;
188 return contentArea.offsetHeight + footer.offsetHeight + padding;
189 }
190 return 0;
191 }
192
193 function isTouch() {
194 return (
195 'ontouchstart' in window || ( window.DocumentTouch && document instanceof DocumentTouch )
196 );
197 }
198
199 function scrollToElement( el, container, callback ) {
200 if ( ! el || ! container ) {
201 if ( callback ) {
202 return callback();
203 }
204 return;
205 }
206
207 // For iOS Safari compatibility, use JS to set the minimum height.
208 var infoArea = container.querySelector( '.jp-carousel-info-extra' );
209 if ( infoArea ) {
210 // 64px is the same height as `.jp-carousel-info-footer` in the CSS.
211 infoArea.style.minHeight = window.innerHeight - 64 + 'px';
212 }
213
214 var isScrolling = true;
215 var startTime = Date.now();
216 var duration = 300;
217 var originalPosition = container.scrollTop;
218 var targetPosition = Math.max(
219 0,
220 el.offsetTop - Math.max( 0, window.innerHeight - getFooterClearance( container ) )
221 );
222 var distance = targetPosition - container.scrollTop;
223 distance = Math.min( distance, container.scrollHeight - window.innerHeight );
224
225 function stopScroll() {
226 isScrolling = false;
227 }
228
229 function runScroll() {
230 var now = Date.now();
231 var progress = easeInOutQuad( ( now - startTime ) / duration );
232
233 progress = progress > 1 ? 1 : progress;
234 var newVal = progress * distance;
235 container.scrollTop = originalPosition + newVal;
236
237 if ( now <= startTime + duration && isScrolling ) {
238 return requestAnimationFrame( runScroll );
239 }
240 if ( callback ) {
241 callback();
242 }
243 if ( infoArea ) {
244 infoArea.style.minHeight = '';
245 }
246 isScrolling = false;
247 container.removeEventListener( 'wheel', stopScroll );
248 }
249
250 // Allow scroll to be cancelled by user interaction.
251 container.addEventListener( 'wheel', stopScroll );
252 runScroll();
253 }
254
255 function getJSONAttribute( el, attr ) {
256 if ( ! el || ! el.hasAttribute( attr ) ) {
257 return undefined;
258 }
259
260 try {
261 return JSON.parse( el.getAttribute( attr ) );
262 } catch ( e ) {
263 return undefined;
264 }
265 }
266
267 function convertToPlainText( html ) {
268 var dummy = document.createElement( 'div' );
269 dummy.textContent = html;
270 return dummy.innerHTML;
271 }
272
273 function stripHTML( text ) {
274 return text.replace( /<[^>]*>?/gm, '' );
275 }
276
277 return {
278 closest: closest,
279 matches: matches,
280 hide: hide,
281 show: show,
282 fadeIn: fadeIn,
283 fadeOut: fadeOut,
284 scrollToElement: scrollToElement,
285 getJSONAttribute: getJSONAttribute,
286 convertToPlainText: convertToPlainText,
287 stripHTML: stripHTML,
288 emitEvent: emitEvent,
289 isTouch: isTouch,
290 };
291 } )();
292
293 /////////////////////////////////////
294 // Carousel implementation
295 /////////////////////////////////////
296 function init() {
297 var commentInterval;
298 var screenPadding;
299 var originalOverflow;
300 var originalHOverflow;
301 var scrollPos;
302
303 var lastKnownLocationHash = '';
304 var isUserTyping = false;
305
306 var gallerySelector =
307 'div.gallery, div.tiled-gallery, ul.wp-block-gallery, ul.blocks-gallery-grid, ' +
308 'figure.wp-block-gallery.has-nested-images, div.wp-block-jetpack-tiled-gallery, a.single-image-gallery';
309
310 // Selector for items within a gallery or tiled gallery.
311 var galleryItemSelector =
312 '.gallery-item, .tiled-gallery-item, .blocks-gallery-item, ' + ' .tiled-gallery__item';
313
314 // Selector for all items including single images.
315 var itemSelector = galleryItemSelector + ', .wp-block-image';
316
317 var carousel = {};
318
319 var stat =
320 typeof wpcom !== 'undefined' && wpcom.carousel && wpcom.carousel.stat
321 ? wpcom.carousel.stat
322 : util.noop;
323
324 var pageview =
325 typeof wpcom !== 'undefined' && wpcom.carousel && wpcom.carousel.pageview
326 ? wpcom.carousel.pageview
327 : util.noop;
328
329 function handleKeyboardEvent( e ) {
330 if ( ! isUserTyping ) {
331 switch ( e.which ) {
332 case 38: // up
333 e.preventDefault();
334 carousel.overlay.scrollTop -= 100;
335 break;
336 case 40: // down
337 e.preventDefault();
338 carousel.overlay.scrollTop += 100;
339 break;
340 case 39: // right
341 e.preventDefault();
342 swiper.slideNext();
343 break;
344 case 37: // left
345 case 8: // backspace
346 e.preventDefault();
347 swiper.slidePrev();
348 break;
349 case 27: // escape
350 e.preventDefault();
351 closeCarousel();
352 break;
353 default:
354 break;
355 }
356 }
357 }
358
359 function disableKeyboardNavigation() {
360 isUserTyping = true;
361 }
362
363 function enableKeyboardNavigation() {
364 isUserTyping = false;
365 }
366
367 function calculatePadding() {
368 var baseScreenPadding = 110;
369 screenPadding = baseScreenPadding;
370
371 if ( window.innerWidth <= 760 ) {
372 screenPadding = Math.round( ( window.innerWidth / 760 ) * baseScreenPadding );
373
374 if ( screenPadding < 40 && domUtil.isTouch() ) {
375 screenPadding = 0;
376 }
377 }
378 }
379
380 function makeGalleryImageAccessible( img ) {
381 img.role = 'button';
382 img.tabIndex = 0;
383 img.ariaLabel = jetpackCarouselStrings.image_label;
384 }
385
386 function initializeCarousel() {
387 if ( ! carousel.overlay ) {
388 carousel.overlay = document.querySelector( '.jp-carousel-overlay' );
389 carousel.container = carousel.overlay.querySelector( '.jp-carousel-wrap' );
390 carousel.gallery = carousel.container.querySelector( '.jp-carousel' );
391 carousel.info = carousel.overlay.querySelector( '.jp-carousel-info' );
392 carousel.caption = carousel.info.querySelector( '.jp-carousel-caption' );
393 carousel.commentField = carousel.overlay.querySelector(
394 '#jp-carousel-comment-form-comment-field'
395 );
396 carousel.emailField = carousel.overlay.querySelector(
397 '#jp-carousel-comment-form-email-field'
398 );
399 carousel.authorField = carousel.overlay.querySelector(
400 '#jp-carousel-comment-form-author-field'
401 );
402 carousel.urlField = carousel.overlay.querySelector( '#jp-carousel-comment-form-url-field' );
403
404 calculatePadding();
405
406 [
407 carousel.commentField,
408 carousel.emailField,
409 carousel.authorField,
410 carousel.urlField,
411 ].forEach( function ( field ) {
412 if ( field ) {
413 field.addEventListener( 'focus', disableKeyboardNavigation );
414 field.addEventListener( 'blur', enableKeyboardNavigation );
415 }
416 } );
417
418 carousel.overlay.addEventListener( 'click', function ( e ) {
419 var target = e.target;
420 var isTargetCloseHint = !! domUtil.closest( target, '.jp-carousel-close-hint' );
421 var isSmallScreen = !! window.matchMedia( '(max-device-width: 760px)' ).matches;
422 if ( target === carousel.overlay ) {
423 if ( isSmallScreen ) {
424 return;
425 } else {
426 closeCarousel();
427 }
428 } else if ( isTargetCloseHint ) {
429 closeCarousel();
430 } else if ( target.classList.contains( 'jp-carousel-image-download' ) ) {
431 stat( 'download_original_click' );
432 } else if ( target.classList.contains( 'jp-carousel-comment-login' ) ) {
433 handleCommentLoginClick( e );
434 } else if ( domUtil.closest( target, '#jp-carousel-comment-form-container' ) ) {
435 handleCommentFormClick( e );
436 } else if (
437 domUtil.closest( target, '.jp-carousel-photo-icons-container' ) ||
438 target.classList.contains( 'jp-carousel-photo-title' )
439 ) {
440 handleFooterElementClick( e );
441 } else if ( ! domUtil.closest( target, '.jp-carousel-info' ) ) {
442 return;
443 }
444 } );
445
446 window.addEventListener( 'keydown', handleKeyboardEvent );
447
448 carousel.overlay.addEventListener( 'jp_carousel.afterOpen', function () {
449 enableKeyboardNavigation();
450
451 // Don't show navigation if there's only one image.
452 if ( carousel.slides.length <= 1 ) {
453 return;
454 }
455 // Show dot pagination if slide count is <= 5, otherwise show n/total.
456 if ( carousel.slides.length <= 5 ) {
457 domUtil.show( carousel.info.querySelector( '.jp-swiper-pagination' ) );
458 } else {
459 domUtil.show( carousel.info.querySelector( '.jp-carousel-pagination' ) );
460 }
461 } );
462
463 carousel.overlay.addEventListener( 'jp_carousel.beforeClose', function () {
464 disableKeyboardNavigation();
465
466 // Fixes some themes where closing carousel brings view back to top.
467 document.documentElement.style.removeProperty( 'height' );
468
469 // If we disable the swiper (because there's only one image)
470 // we have to re-enable it here again as Swiper doesn't, for some reason,
471 // show the navigation buttons again after reinitialization.
472 if ( swiper ) {
473 swiper.enable();
474 }
475
476 // Hide pagination.
477 domUtil.hide( carousel.info.querySelector( '.jp-swiper-pagination' ) );
478 domUtil.hide( carousel.info.querySelector( '.jp-carousel-pagination' ) );
479 } );
480
481 carousel.overlay.addEventListener( 'jp_carousel.afterClose', function () {
482 // don't force the browser back when the carousel closes.
483 if ( window.history.pushState ) {
484 history.pushState(
485 '',
486 document.title,
487 window.location.pathname + window.location.search
488 );
489 } else {
490 window.location.href = '';
491 }
492 lastKnownLocationHash = '';
493 carousel.isOpen = false;
494 } );
495
496 // Prevent native browser zooming
497 carousel.overlay.addEventListener( 'touchstart', function ( e ) {
498 if ( e.touches.length > 1 ) {
499 e.preventDefault();
500 }
501 } );
502 }
503 }
504
505 function handleCommentLoginClick() {
506 var slide = carousel.currentSlide;
507 var attachmentId = slide ? slide.attrs.attachmentId : '0';
508
509 window.location.href = jetpackCarouselStrings.login_url + '%23jp-carousel-' + attachmentId;
510 }
511
512 function updatePostResults( msg, isSuccess ) {
513 var results = carousel.overlay.querySelector( '#jp-carousel-comment-post-results' );
514 var elClass = 'jp-carousel-comment-post-' + ( isSuccess ? 'success' : 'error' );
515 results.innerHTML = '<span class="' + elClass + '">' + msg + '</span>';
516 domUtil.hide( carousel.overlay.querySelector( '#jp-carousel-comment-form-spinner' ) );
517 carousel.overlay
518 .querySelector( '#jp-carousel-comment-form' )
519 .classList.remove( 'jp-carousel-is-disabled' );
520 domUtil.show( results );
521 }
522
523 function handleCommentFormClick( e ) {
524 var target = e.target;
525 var data = domUtil.getJSONAttribute( carousel.container, 'data-carousel-extra' ) || {};
526 var attachmentId = carousel.currentSlide.attrs.attachmentId;
527
528 var wrapper = document.querySelector( '#jp-carousel-comment-form-submit-and-info-wrapper' );
529 var spinner = document.querySelector( '#jp-carousel-comment-form-spinner' );
530 var submit = document.querySelector( '#jp-carousel-comment-form-button-submit' );
531 var form = document.querySelector( '#jp-carousel-comment-form' );
532
533 if (
534 carousel.commentField &&
535 carousel.commentField.getAttribute( 'id' ) === target.getAttribute( 'id' )
536 ) {
537 // For first page load
538 disableKeyboardNavigation();
539 domUtil.show( wrapper );
540 } else if ( domUtil.matches( target, 'input[type="submit"]' ) ) {
541 e.preventDefault();
542 e.stopPropagation();
543
544 domUtil.show( spinner );
545 form.classList.add( 'jp-carousel-is-disabled' );
546
547 var ajaxData = {
548 action: 'post_attachment_comment',
549 nonce: jetpackCarouselStrings.nonce,
550 blog_id: data.blog_id,
551 id: attachmentId,
552 comment: carousel.commentField.value,
553 };
554
555 if ( ! ajaxData.comment.length ) {
556 updatePostResults( jetpackCarouselStrings.no_comment_text, false );
557 return;
558 }
559
560 if ( Number( jetpackCarouselStrings.is_logged_in ) !== 1 ) {
561 ajaxData.email = carousel.emailField.value;
562 ajaxData.author = carousel.authorField.value;
563 ajaxData.url = carousel.urlField.value;
564
565 if ( Number( jetpackCarouselStrings.require_name_email ) === 1 ) {
566 if ( ! ajaxData.email.length || ! ajaxData.email.match( '@' ) ) {
567 updatePostResults( jetpackCarouselStrings.no_comment_email, false );
568 return;
569 } else if ( ! ajaxData.author.length ) {
570 updatePostResults( jetpackCarouselStrings.no_comment_author, false );
571 return;
572 }
573 }
574 }
575
576 var xhr = new XMLHttpRequest();
577 xhr.open( 'POST', jetpackCarouselStrings.ajaxurl, true );
578 xhr.setRequestHeader( 'X-Requested-With', 'XMLHttpRequest' );
579 xhr.setRequestHeader( 'Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8' );
580
581 xhr.onreadystatechange = function () {
582 if (
583 this.readyState === XMLHttpRequest.DONE &&
584 this.status >= 200 &&
585 this.status < 300
586 ) {
587 var response;
588 try {
589 response = JSON.parse( this.response );
590 } catch ( error ) {
591 updatePostResults( jetpackCarouselStrings.comment_post_error, false );
592 return;
593 }
594 if ( response.comment_status === 'approved' ) {
595 updatePostResults( jetpackCarouselStrings.comment_approved, true );
596 } else if ( response.comment_status === 'unapproved' ) {
597 updatePostResults( jetpackCarouselStrings.comment_unapproved, true );
598 } else {
599 // 'deleted', 'spam', false
600 updatePostResults( jetpackCarouselStrings.comment_post_error, false );
601 }
602 clearCommentTextAreaValue();
603 fetchComments( attachmentId );
604 submit.value = jetpackCarouselStrings.post_comment;
605 domUtil.hide( spinner );
606 form.classList.remove( 'jp-carousel-is-disabled' );
607 } else {
608 // TODO: Add error handling and display here
609 updatePostResults( jetpackCarouselStrings.comment_post_error, false );
610 }
611 };
612
613 var params = [];
614 for ( var item in ajaxData ) {
615 if ( item ) {
616 // Encode each form element into a URI-compatible string.
617 var encoded = encodeURIComponent( item ) + '=' + encodeURIComponent( ajaxData[ item ] );
618 // In x-www-form-urlencoded, spaces should be `+`, not `%20`.
619 params.push( encoded.replace( /%20/g, '+' ) );
620 }
621 }
622 var encodedData = params.join( '&' );
623
624 xhr.send( encodedData );
625 }
626 }
627
628 /**
629 * Handles clicks to icons and other action elements in the icon container.
630 * @param {MouseEvent|TouchEvent|KeyBoardEvent} Event object.
631 */
632 function handleFooterElementClick( e ) {
633 e.preventDefault();
634
635 var target = e.target;
636 var extraInfoContainer = carousel.info.querySelector( '.jp-carousel-info-extra' );
637 var photoMetaContainer = carousel.info.querySelector( '.jp-carousel-image-meta' );
638 var commentsContainer = carousel.info.querySelector( '.jp-carousel-comments-wrapper' );
639 var infoIcon = carousel.info.querySelector( '.jp-carousel-icon-info' );
640 var commentsIcon = carousel.info.querySelector( '.jp-carousel-icon-comments' );
641
642 function handleInfoToggle() {
643 if ( commentsIcon ) {
644 commentsIcon.classList.remove( 'jp-carousel-selected' );
645 }
646 infoIcon.classList.toggle( 'jp-carousel-selected' );
647
648 if ( commentsContainer ) {
649 commentsContainer.classList.remove( 'jp-carousel-show' );
650 }
651 if ( photoMetaContainer ) {
652 photoMetaContainer.classList.toggle( 'jp-carousel-show' );
653 if ( photoMetaContainer.classList.contains( 'jp-carousel-show' ) ) {
654 extraInfoContainer.classList.add( 'jp-carousel-show' );
655 } else {
656 extraInfoContainer.classList.remove( 'jp-carousel-show' );
657 }
658 }
659 }
660
661 function handleCommentToggle() {
662 if ( infoIcon ) {
663 infoIcon.classList.remove( 'jp-carousel-selected' );
664 }
665 commentsIcon.classList.toggle( 'jp-carousel-selected' );
666
667 if ( photoMetaContainer ) {
668 photoMetaContainer.classList.remove( 'jp-carousel-show' );
669 }
670 if ( commentsContainer ) {
671 commentsContainer.classList.toggle( 'jp-carousel-show' );
672 if ( commentsContainer.classList.contains( 'jp-carousel-show' ) ) {
673 extraInfoContainer.classList.add( 'jp-carousel-show' );
674 } else {
675 extraInfoContainer.classList.remove( 'jp-carousel-show' );
676 }
677 }
678 }
679
680 if (
681 domUtil.closest( target, '.jp-carousel-icon-info' ) ||
682 target.classList.contains( 'jp-carousel-photo-title' )
683 ) {
684 if ( photoMetaContainer && photoMetaContainer.classList.contains( 'jp-carousel-show' ) ) {
685 domUtil.scrollToElement( carousel.overlay, carousel.overlay, handleInfoToggle );
686 } else {
687 handleInfoToggle();
688 domUtil.scrollToElement( carousel.info, carousel.overlay );
689 }
690 }
691
692 if ( domUtil.closest( target, '.jp-carousel-icon-comments' ) ) {
693 if ( commentsContainer && commentsContainer.classList.contains( 'jp-carousel-show' ) ) {
694 domUtil.scrollToElement( carousel.overlay, carousel.overlay, handleCommentToggle );
695 } else {
696 handleCommentToggle();
697 domUtil.scrollToElement( carousel.info, carousel.overlay );
698 }
699 }
700 }
701
702 function processSingleImageGallery() {
703 var images = document.querySelectorAll( 'a img[data-attachment-id]' );
704 Array.prototype.forEach.call( images, function ( image ) {
705 var link = image.parentElement;
706 var container = link.parentElement;
707
708 // Skip if image was already added to gallery by shortcode.
709 if ( container.classList.contains( 'gallery-icon' ) ) {
710 return;
711 }
712
713 // Skip if image is part of a gallery.
714 if ( domUtil.closest( container, galleryItemSelector ) ) {
715 return;
716 }
717
718 // Skip if the parent is not actually a link.
719 if ( ! link.hasAttribute( 'href' ) ) {
720 return;
721 }
722
723 var valid = false;
724
725 // If link points to 'Media File' (ignoring GET parameters) and flag is set, allow it.
726 if (
727 link.getAttribute( 'href' ).split( '?' )[ 0 ] ===
728 image.getAttribute( 'data-orig-file' ).split( '?' )[ 0 ] &&
729 Number( jetpackCarouselStrings.single_image_gallery_media_file ) === 1
730 ) {
731 valid = true;
732 }
733
734 // If link points to 'Attachment Page', allow it.
735 if ( link.getAttribute( 'href' ) === image.getAttribute( 'data-permalink' ) ) {
736 valid = true;
737 }
738
739 // Links to 'Custom URL' or 'Media File' when flag is not set are not valid.
740 if ( ! valid ) {
741 return;
742 }
743
744 makeGalleryImageAccessible( image );
745
746 // Make this node a gallery recognizable by event listener above.
747 link.classList.add( 'single-image-gallery' );
748 // blog_id is needed to allow posting comments to correct blog.
749 link.setAttribute(
750 'data-carousel-extra',
751 JSON.stringify( {
752 blog_id: Number( jetpackCarouselStrings.blog_id ),
753 } )
754 );
755 } );
756 }
757
758 function testForData( el ) {
759 return !! ( el && el.getAttribute( 'data-carousel-extra' ) );
760 }
761
762 function openOrSelectSlide( gal, index ) {
763 if ( ! carousel.isOpen ) {
764 // The `open` method selects the correct slide during the initialization.
765 loadSwiper( gal, { startIndex: index } );
766 } else {
767 selectSlideAtIndex( index );
768 // We have to force swiper to slide to the index onHasChange.
769 swiper.slideTo( index + 1 );
770 }
771 }
772
773 function selectSlideAtIndex( index ) {
774 if ( ! index || index < 0 || index > carousel.slides.length ) {
775 index = 0;
776 }
777 carousel.currentSlide = carousel.slides[ index ];
778
779 var current = carousel.currentSlide;
780 var attachmentId = current.attrs.attachmentId;
781
782 loadFullImage( carousel.slides[ index ] );
783
784 if (
785 Number( jetpackCarouselStrings.display_background_image ) === 1 &&
786 ! carousel.slides[ index ].backgroundImage
787 ) {
788 loadBackgroundImage( carousel.slides[ index ] );
789 }
790
791 domUtil.hide( carousel.caption );
792 updateTitleCaptionAndDesc( {
793 caption: current.attrs.caption,
794 title: current.attrs.title,
795 desc: current.attrs.desc,
796 } );
797
798 var imageMeta = carousel.slides[ index ].attrs.imageMeta;
799 updateExif( imageMeta );
800 updateFullSizeLink( current );
801
802 if ( Number( jetpackCarouselStrings.display_comments ) === 1 ) {
803 testCommentsOpened( carousel.slides[ index ].attrs.commentsOpened );
804 fetchComments( attachmentId );
805 domUtil.hide( carousel.info.querySelector( '#jp-carousel-comment-post-results' ) );
806 }
807
808 // Update pagination in footer.
809 var pagination = carousel.info.querySelector( '.jp-carousel-pagination' );
810 if ( pagination && carousel.slides.length > 5 ) {
811 var currentPage = index + 1;
812 pagination.innerHTML = '<span>' + currentPage + ' / ' + carousel.slides.length + '</span>';
813 }
814
815 // Record pageview in WP Stats, for each new image loaded full-screen.
816 if ( jetpackCarouselStrings.stats ) {
817 new Image().src =
818 document.location.protocol +
819 '//pixel.wp.com/g.gif?' +
820 jetpackCarouselStrings.stats +
821 '&post=' +
822 encodeURIComponent( attachmentId ) +
823 '&rand=' +
824 Math.random();
825 }
826
827 pageview( attachmentId );
828
829 window.location.hash = lastKnownLocationHash = '#jp-carousel-' + attachmentId;
830 }
831
832 function restoreScroll() {
833 window.scrollTo( window.scrollX || window.pageXOffset || 0, scrollPos || 0 );
834 }
835
836 function closeCarousel() {
837 // Make sure to let the page scroll again.
838 document.body.style.overflow = originalOverflow;
839 document.documentElement.style.overflow = originalHOverflow;
840 clearCommentTextAreaValue();
841
842 disableKeyboardNavigation();
843
844 domUtil.emitEvent( carousel.overlay, 'jp_carousel.beforeClose' );
845 restoreScroll();
846 swiper.destroy();
847 carousel.isOpen = false;
848 // Clear slide data for DOM garbage collection.
849 carousel.slides = [];
850 carousel.currentSlide = undefined;
851 carousel.gallery.innerHTML = '';
852
853 domUtil.fadeOut( carousel.overlay, function () {
854 domUtil.emitEvent( carousel.overlay, 'jp_carousel.afterClose' );
855 } );
856 }
857
858 function calculateMaxSlideDimensions() {
859 return {
860 width: window.innerWidth,
861 height: window.innerHeight - 64, //subtract height of bottom info bar,
862 };
863 }
864
865 function selectBestImageUrl( args ) {
866 if ( typeof args !== 'object' ) {
867 args = {};
868 }
869
870 if ( typeof args.origFile === 'undefined' ) {
871 return '';
872 }
873
874 if ( typeof args.origWidth === 'undefined' || typeof args.maxWidth === 'undefined' ) {
875 return args.origFile;
876 }
877
878 if ( typeof args.mediumFile === 'undefined' || typeof args.largeFile === 'undefined' ) {
879 return args.origFile;
880 }
881
882 // Check if the image is being served by Photon (using a regular expression on the hostname).
883
884 var imageLinkParser = document.createElement( 'a' );
885 imageLinkParser.href = args.largeFile;
886
887 var isPhotonUrl = /^i[0-2]\.wp\.com$/i.test( imageLinkParser.hostname );
888
889 var mediumSizeParts = getImageSizeParts( args.mediumFile, args.origWidth, isPhotonUrl );
890 var largeSizeParts = getImageSizeParts( args.largeFile, args.origWidth, isPhotonUrl );
891
892 var largeWidth = parseInt( largeSizeParts[ 0 ], 10 );
893 var largeHeight = parseInt( largeSizeParts[ 1 ], 10 );
894 var mediumWidth = parseInt( mediumSizeParts[ 0 ], 10 );
895 var mediumHeight = parseInt( mediumSizeParts[ 1 ], 10 );
896
897 args.origMaxWidth = args.maxWidth;
898 args.origMaxHeight = args.maxHeight;
899
900 // Give devices with a higher devicePixelRatio higher-res images (Retina display = 2, Android phones = 1.5, etc)
901 if ( typeof window.devicePixelRatio !== 'undefined' && window.devicePixelRatio > 1 ) {
902 args.maxWidth = args.maxWidth * window.devicePixelRatio;
903 args.maxHeight = args.maxHeight * window.devicePixelRatio;
904 }
905
906 if ( largeWidth >= args.maxWidth || largeHeight >= args.maxHeight ) {
907 return args.largeFile;
908 }
909
910 if ( mediumWidth >= args.maxWidth || mediumHeight >= args.maxHeight ) {
911 return args.mediumFile;
912 }
913
914 if ( isPhotonUrl ) {
915 // args.origFile doesn't point to a Photon url, so in this case we use args.largeFile
916 // to return the photon url of the original image.
917 var largeFileIndex = args.largeFile.lastIndexOf( '?' );
918 var origPhotonUrl = args.largeFile;
919 if ( largeFileIndex !== -1 ) {
920 origPhotonUrl = args.largeFile.substring( 0, largeFileIndex );
921 // If we have a really large image load a smaller version
922 // that is closer to the viewable size
923 if ( args.origWidth > args.maxWidth || args.origHeight > args.maxHeight ) {
924 // @2x the max sizes so we get a high enough resolution for zooming.
925 args.origMaxWidth = args.maxWidth * 2;
926 args.origMaxHeight = args.maxHeight * 2;
927 origPhotonUrl += '?fit=' + args.origMaxWidth + '%2C' + args.origMaxHeight;
928 }
929 }
930 return origPhotonUrl;
931 }
932
933 return args.origFile;
934 }
935
936 function getImageSizeParts( file, origWidth, isPhotonUrl ) {
937 var size = isPhotonUrl
938 ? file.replace( /.*=([\d]+%2C[\d]+).*$/, '$1' )
939 : file.replace( /.*-([\d]+x[\d]+)\..+$/, '$1' );
940
941 var sizeParts =
942 size !== file
943 ? isPhotonUrl
944 ? size.split( '%2C' )
945 : size.split( 'x' )
946 : [ origWidth, 0 ];
947
948 // If one of the dimensions is set to 9999, then the actual value of that dimension can't be retrieved from the url.
949 // In that case, we set the value to 0.
950 if ( sizeParts[ 0 ] === '9999' ) {
951 sizeParts[ 0 ] = '0';
952 }
953
954 if ( sizeParts[ 1 ] === '9999' ) {
955 sizeParts[ 1 ] = '0';
956 }
957
958 return sizeParts;
959 }
960
961 /**
962 * Returns a number in a fraction format that represents the shutter speed.
963 * @param Number speed
964 * @return String
965 */
966 function formatShutterSpeed( speed ) {
967 var denominator;
968
969 // round to one decimal if value > 1s by multiplying it by 10, rounding, then dividing by 10 again
970 if ( speed >= 1 ) {
971 return Math.round( speed * 10 ) / 10 + 's';
972 }
973
974 // If the speed is less than one, we find the denominator by inverting
975 // the number. Since cameras usually use rational numbers as shutter
976 // speeds, we should get a nice round number. Or close to one in cases
977 // like 1/30. So we round it.
978 denominator = Math.round( 1 / speed );
979
980 return '1/' + denominator + 's';
981 }
982
983 function parseTitleOrDesc( value ) {
984 if ( ! value.match( ' ' ) && value.match( '_' ) ) {
985 return '';
986 }
987
988 return value;
989 }
990
991 function updateTitleCaptionAndDesc( data ) {
992 var caption = '';
993 var title = '';
994 var desc = '';
995 var captionMainElement;
996 var captionInfoExtraElement;
997 var titleElement;
998 var descriptionElement;
999
1000 captionMainElement = carousel.overlay.querySelector( '.jp-carousel-photo-caption' );
1001 captionInfoExtraElement = carousel.overlay.querySelector( '.jp-carousel-caption' );
1002
1003 titleElement = carousel.overlay.querySelector( '.jp-carousel-photo-title' );
1004 descriptionElement = carousel.overlay.querySelector( '.jp-carousel-photo-description' );
1005
1006 domUtil.hide( captionMainElement );
1007 domUtil.hide( captionInfoExtraElement );
1008 domUtil.hide( titleElement );
1009 domUtil.hide( descriptionElement );
1010
1011 caption = parseTitleOrDesc( data.caption ) || '';
1012 title = parseTitleOrDesc( data.title ) || '';
1013 desc = parseTitleOrDesc( data.desc ) || '';
1014
1015 if ( caption || title || desc ) {
1016 if ( caption ) {
1017 captionMainElement.innerHTML = caption;
1018 captionInfoExtraElement.innerHTML = caption;
1019
1020 domUtil.show( captionMainElement );
1021 domUtil.show( captionInfoExtraElement );
1022 }
1023
1024 if ( domUtil.stripHTML( caption ) === domUtil.stripHTML( title ) ) {
1025 title = '';
1026 }
1027
1028 if ( domUtil.stripHTML( caption ) === domUtil.stripHTML( desc ) ) {
1029 desc = '';
1030 }
1031
1032 if ( domUtil.stripHTML( title ) === domUtil.stripHTML( desc ) ) {
1033 desc = '';
1034 }
1035
1036 if ( desc ) {
1037 descriptionElement.innerHTML = desc;
1038 domUtil.show( descriptionElement );
1039
1040 if ( ! title && ! caption ) {
1041 captionMainElement.innerHTML = domUtil.stripHTML( desc );
1042 domUtil.show( captionMainElement );
1043 }
1044 }
1045
1046 if ( title ) {
1047 var plainTitle = domUtil.stripHTML( title );
1048 titleElement.innerHTML = plainTitle;
1049
1050 if ( ! caption ) {
1051 captionMainElement.innerHTML = plainTitle;
1052 captionInfoExtraElement.innerHTML = plainTitle;
1053
1054 domUtil.show( captionMainElement );
1055 }
1056
1057 domUtil.show( titleElement );
1058 }
1059 }
1060 }
1061
1062 // updateExif updates the contents of the exif UL (.jp-carousel-image-exif)
1063 function updateExif( meta ) {
1064 if ( ! meta || Number( jetpackCarouselStrings.display_exif ) !== 1 ) {
1065 return false;
1066 }
1067
1068 var ul = carousel.info.querySelector( '.jp-carousel-image-meta ul.jp-carousel-image-exif' );
1069 var html = '';
1070
1071 for ( var key in meta ) {
1072 var val = meta[ key ];
1073 var metaKeys = jetpackCarouselStrings.meta_data || [];
1074
1075 if ( parseFloat( val ) === 0 || ! val.length || metaKeys.indexOf( key ) === -1 ) {
1076 continue;
1077 }
1078
1079 switch ( key ) {
1080 case 'focal_length':
1081 val = val + 'mm';
1082 break;
1083 case 'shutter_speed':
1084 val = formatShutterSpeed( val );
1085 break;
1086 case 'aperture':
1087 val = 'f/' + val;
1088 break;
1089 }
1090
1091 html += '<li><h5>' + jetpackCarouselStrings[ key ] + '</h5>' + val + '</li>';
1092 }
1093
1094 ul.innerHTML = html;
1095 ul.style.removeProperty( 'display' );
1096 }
1097
1098 // Update the contents of the jp-carousel-image-download link
1099 function updateFullSizeLink( currentSlide ) {
1100 if ( ! currentSlide ) {
1101 return false;
1102 }
1103 var original;
1104 var origSize = [ currentSlide.attrs.origWidth, currentSlide.attrs.origHeight ];
1105 var imageLinkParser = document.createElement( 'a' );
1106
1107 imageLinkParser.href = currentSlide.attrs.src.replace( /\?.+$/, '' );
1108
1109 // Is this a Photon URL?
1110 if ( imageLinkParser.hostname.match( /^i[\d]{1}\.wp\.com$/i ) !== null ) {
1111 original = imageLinkParser.href;
1112 } else {
1113 original = currentSlide.attrs.origFile.replace( /\?.+$/, '' );
1114 }
1115
1116 var downloadText = carousel.info.querySelector( '.jp-carousel-download-text' );
1117 var permalink = carousel.info.querySelector( '.jp-carousel-image-download' );
1118
1119 downloadText.innerHTML = util.applyReplacements(
1120 jetpackCarouselStrings.download_original,
1121 origSize
1122 );
1123 permalink.setAttribute( 'href', original );
1124 permalink.style.removeProperty( 'display' );
1125 }
1126
1127 function testCommentsOpened( opened ) {
1128 var commentForm = carousel.container.querySelector( '.jp-carousel-comment-form-container' );
1129 var isOpened = parseInt( opened, 10 ) === 1;
1130
1131 if ( isOpened ) {
1132 domUtil.fadeIn( commentForm );
1133 } else {
1134 domUtil.fadeOut( commentForm );
1135 }
1136 }
1137
1138 function fetchComments( attachmentId, offset ) {
1139 var shouldClear = offset === undefined;
1140 var commentsIndicator = carousel.info.querySelector(
1141 '.jp-carousel-icon-comments .jp-carousel-has-comments-indicator'
1142 );
1143
1144 commentsIndicator.classList.remove( 'jp-carousel-show' );
1145
1146 clearInterval( commentInterval );
1147
1148 if ( ! attachmentId ) {
1149 return;
1150 }
1151
1152 if ( ! offset || offset < 1 ) {
1153 offset = 0;
1154 }
1155
1156 var comments = carousel.info.querySelector( '.jp-carousel-comments' );
1157 var commentsLoading = carousel.info.querySelector( '#jp-carousel-comments-loading' );
1158 domUtil.show( commentsLoading );
1159
1160 if ( shouldClear ) {
1161 domUtil.hide( comments );
1162 comments.innerHTML = '';
1163 }
1164
1165 var xhr = new XMLHttpRequest();
1166 var url =
1167 jetpackCarouselStrings.ajaxurl +
1168 '?action=get_attachment_comments' +
1169 '&nonce=' +
1170 jetpackCarouselStrings.nonce +
1171 '&id=' +
1172 attachmentId +
1173 '&offset=' +
1174 offset;
1175 xhr.open( 'GET', url );
1176 xhr.setRequestHeader( 'X-Requested-With', 'XMLHttpRequest' );
1177
1178 var onError = function () {
1179 domUtil.fadeIn( comments );
1180 domUtil.fadeOut( commentsLoading );
1181 };
1182
1183 xhr.onload = function () {
1184 // Ignore the results if they arrive late and we're now on a different slide.
1185 if (
1186 ! carousel.currentSlide ||
1187 carousel.currentSlide.attrs.attachmentId !== attachmentId
1188 ) {
1189 return;
1190 }
1191
1192 var isSuccess = xhr.status >= 200 && xhr.status < 300;
1193 var data;
1194 try {
1195 data = JSON.parse( xhr.responseText );
1196 } catch ( e ) {
1197 // Do nothing.
1198 }
1199
1200 if ( ! isSuccess || ! data || ! Array.isArray( data ) ) {
1201 return onError();
1202 }
1203
1204 if ( shouldClear ) {
1205 comments.innerHTML = '';
1206 }
1207
1208 for ( var i = 0; i < data.length; i++ ) {
1209 var entry = data[ i ];
1210 var comment = document.createElement( 'div' );
1211 comment.classList.add( 'jp-carousel-comment' );
1212 comment.setAttribute( 'id', 'jp-carousel-comment-' + entry.id );
1213 comment.innerHTML =
1214 '<div class="comment-gravatar">' +
1215 entry.gravatar_markup +
1216 '</div>' +
1217 '<div class="comment-content">' +
1218 '<div class="comment-author">' +
1219 entry.author_markup +
1220 '</div>' +
1221 '<div class="comment-date">' +
1222 entry.date_gmt +
1223 '</div>' +
1224 entry.content +
1225 '</div>';
1226 comments.appendChild( comment );
1227
1228 // Set the interval to check for a new page of comments.
1229 clearInterval( commentInterval );
1230 commentInterval = setInterval( function () {
1231 if ( carousel.container.scrollTop + 150 > window.innerHeight ) {
1232 fetchComments( attachmentId, offset + 10 );
1233 clearInterval( commentInterval );
1234 }
1235 }, 300 );
1236 }
1237
1238 if ( data.length > 0 ) {
1239 domUtil.show( comments );
1240 commentsIndicator.innerText = data.length;
1241 commentsIndicator.classList.add( 'jp-carousel-show' );
1242 }
1243
1244 domUtil.hide( commentsLoading );
1245 };
1246
1247 xhr.onerror = onError;
1248
1249 xhr.send();
1250 }
1251
1252 function loadFullImage( slide ) {
1253 var el = slide.el;
1254 var attrs = slide.attrs;
1255 var image = el.querySelector( 'img' );
1256
1257 if ( ! image.hasAttribute( 'data-loaded' ) ) {
1258 var hasPreview = !! attrs.previewImage;
1259 var thumbSize = attrs.thumbSize;
1260
1261 if ( ! hasPreview || ( thumbSize && el.offsetWidth > thumbSize.width ) ) {
1262 image.src = attrs.src;
1263 } else {
1264 image.src = attrs.previewImage;
1265 }
1266
1267 image.setAttribute( 'itemprop', 'image' );
1268 image.setAttribute( 'data-loaded', 1 );
1269 }
1270 }
1271
1272 function loadBackgroundImage( slide ) {
1273 var currentSlide = slide.el;
1274
1275 if ( swiper && swiper.slides ) {
1276 currentSlide = swiper.slides[ swiper.activeIndex ];
1277 }
1278
1279 var image = slide.attrs.originalElement;
1280 var isLoaded = image.complete && image.naturalHeight !== 0;
1281
1282 if ( isLoaded ) {
1283 applyBackgroundImage( slide, currentSlide, image );
1284 return;
1285 }
1286
1287 image.onload = function () {
1288 applyBackgroundImage( slide, currentSlide, image );
1289 };
1290 }
1291
1292 function applyBackgroundImage( slide, currentSlide, image ) {
1293 var url = util.getBackgroundImage( image );
1294 slide.backgroundImage = url;
1295 currentSlide.style.backgroundImage = 'url(' + url + ')';
1296 currentSlide.style.backgroundSize = 'cover';
1297 }
1298
1299 function clearCommentTextAreaValue() {
1300 if ( carousel.commentField ) {
1301 carousel.commentField.value = '';
1302 }
1303 }
1304
1305 function getOriginalDimensions( el ) {
1306 var size = el.getAttribute( 'data-orig-size' ) || '';
1307
1308 if ( size ) {
1309 var parts = size.split( ',' );
1310 return { width: parseInt( parts[ 0 ], 10 ), height: parseInt( parts[ 1 ], 10 ) };
1311 } else {
1312 return {
1313 width:
1314 el.getAttribute( 'data-original-width' ) || el.getAttribute( 'width' ) || undefined,
1315 height:
1316 el.getAttribute( 'data-original-height' ) || el.getAttribute( 'height' ) || undefined,
1317 };
1318 }
1319 }
1320
1321 function initCarouselSlides( items, startIndex ) {
1322 carousel.slides = [];
1323
1324 var max = calculateMaxSlideDimensions();
1325
1326 // If the startIndex is not 0 then preload the clicked image first.
1327 if ( startIndex !== 0 ) {
1328 var img = new Image();
1329 img.src = items[ startIndex ].getAttribute( 'data-gallery-src' );
1330 }
1331
1332 var useInPageThumbnails = !! domUtil.closest( items[ 0 ], '.tiled-gallery.type-rectangular' );
1333
1334 // create the 'slide'
1335 Array.prototype.forEach.call( items, function ( item, i ) {
1336 var permalinkEl = domUtil.closest( item, 'a' );
1337 var origFile = item.getAttribute( 'data-orig-file' ) || item.getAttribute( 'src-orig' );
1338 var attrID =
1339 item.getAttribute( 'data-attachment-id' ) || item.getAttribute( 'data-id' ) || '0';
1340 var caption = document.querySelector(
1341 'img[data-attachment-id="' + attrID + '"] + figcaption'
1342 );
1343
1344 if ( caption ) {
1345 caption = caption.innerHTML;
1346 } else {
1347 caption = item.getAttribute( 'data-image-caption' );
1348 }
1349
1350 var attrs = {
1351 originalElement: item,
1352 attachmentId: attrID,
1353 commentsOpened: item.getAttribute( 'data-comments-opened' ) || '0',
1354 imageMeta: domUtil.getJSONAttribute( item, 'data-image-meta' ) || {},
1355 title: item.getAttribute( 'data-image-title' ) || '',
1356 desc: item.getAttribute( 'data-image-description' ) || '',
1357 mediumFile: item.getAttribute( 'data-medium-file' ) || '',
1358 largeFile: item.getAttribute( 'data-large-file' ) || '',
1359 origFile: origFile || '',
1360 thumbSize: { width: item.naturalWidth, height: item.naturalHeight },
1361 caption: caption || '',
1362 permalink: permalinkEl && permalinkEl.getAttribute( 'href' ),
1363 src: origFile || item.getAttribute( 'src' ) || '',
1364 };
1365
1366 var tiledGalleryItem = domUtil.closest( item, '.tiled-gallery-item' );
1367 var tiledCaptionEl =
1368 tiledGalleryItem && tiledGalleryItem.querySelector( '.tiled-gallery-caption' );
1369 var tiledCaption = tiledCaptionEl && tiledCaptionEl.innerHTML;
1370 if ( tiledCaption ) {
1371 attrs.caption = tiledCaption;
1372 }
1373
1374 var origDimensions = getOriginalDimensions( item );
1375
1376 attrs.origWidth = origDimensions.width || attrs.thumbSize.width;
1377 attrs.origHeight = origDimensions.height || attrs.thumbSize.height;
1378
1379 if ( typeof wpcom !== 'undefined' && wpcom.carousel && wpcom.carousel.generateImgSrc ) {
1380 attrs.src = wpcom.carousel.generateImgSrc( item, max );
1381 } else {
1382 attrs.src = selectBestImageUrl( {
1383 origFile: attrs.src,
1384 origWidth: attrs.origWidth,
1385 origHeight: attrs.origHeight,
1386 maxWidth: max.width,
1387 maxHeight: max.height,
1388 mediumFile: attrs.mediumFile,
1389 largeFile: attrs.largeFile,
1390 } );
1391 }
1392
1393 // Set the final src.
1394 item.setAttribute( 'data-gallery-src', attrs.src );
1395
1396 if ( attrs.attachmentId !== '0' ) {
1397 attrs.title = util.texturize( attrs.title );
1398 attrs.desc = util.texturize( attrs.desc );
1399 attrs.caption = util.texturize( attrs.caption );
1400
1401 // Initially, the image is a 1x1 transparent gif.
1402 // The preview is shown as a background image on the slide itself.
1403 var image = new Image();
1404 image.src = attrs.src;
1405
1406 var slideEl = document.createElement( 'div' );
1407 slideEl.classList.add( 'swiper-slide' );
1408 slideEl.setAttribute( 'itemprop', 'associatedMedia' );
1409 slideEl.setAttribute( 'itemscope', '' );
1410 slideEl.setAttribute( 'itemtype', 'https://schema.org/ImageObject' );
1411 var zoomEl = document.createElement( 'div' );
1412 zoomEl.classList.add( 'swiper-zoom-container' );
1413
1414 carousel.gallery.appendChild( slideEl );
1415
1416 slideEl.appendChild( zoomEl );
1417 zoomEl.appendChild( image );
1418 slideEl.setAttribute( 'data-attachment-id', attrs.attachmentId );
1419 slideEl.setAttribute( 'data-permalink', attrs.permalink );
1420 slideEl.setAttribute( 'data-orig-file', attrs.origFile );
1421
1422 if ( useInPageThumbnails ) {
1423 // Use the image already loaded in the gallery as a preview.
1424 attrs.previewImage = attrs.src;
1425 }
1426
1427 var slide = { el: slideEl, attrs: attrs, index: i };
1428 carousel.slides.push( slide );
1429 }
1430 } );
1431 }
1432
1433 function loadSwiper( gallery, options ) {
1434 if ( ! window.Swiper670 ) {
1435 var loader = document.querySelector( '#jp-carousel-loading-overlay' );
1436 domUtil.show( loader );
1437 var jsScript = document.createElement( 'script' );
1438 jsScript.id = 'jetpack-carousel-swiper-js';
1439 jsScript.src = window.jetpackSwiperLibraryPath.url;
1440 jsScript.async = true;
1441 jsScript.onload = function () {
1442 domUtil.hide( loader );
1443 openCarousel( gallery, options );
1444 };
1445 jsScript.onerror = function () {
1446 domUtil.hide( loader );
1447 };
1448 document.head.appendChild( jsScript );
1449 return;
1450 }
1451 openCarousel( gallery, options );
1452 }
1453
1454 function openCarousel( gallery, options ) {
1455 var settings = {
1456 imgSelector:
1457 '.gallery-item [data-attachment-id], .tiled-gallery-item [data-attachment-id], img[data-attachment-id], img[data-id]',
1458 startIndex: 0,
1459 };
1460
1461 var data = domUtil.getJSONAttribute( gallery, 'data-carousel-extra' );
1462 var tapTimeout;
1463
1464 if ( ! data ) {
1465 return; // don't run if the default gallery functions weren't used
1466 }
1467
1468 const images = gallery.querySelectorAll( settings.imgSelector );
1469 if ( ! images.length ) {
1470 return; // don't run if we found no images in the gallery (somehow it has images that aren't in the media library?)
1471 }
1472
1473 initializeCarousel();
1474
1475 if ( carousel.isOpen ) {
1476 return; // don't open if already opened
1477 }
1478 carousel.isOpen = true;
1479
1480 // make sure to stop the page from scrolling behind the carousel overlay, so we don't trigger
1481 // infiniscroll for it when enabled (Reader, theme infiniscroll, etc).
1482 originalOverflow = getComputedStyle( document.body ).overflow;
1483 document.body.style.overflow = 'hidden';
1484 // prevent html from overflowing on some of the new themes.
1485 originalHOverflow = getComputedStyle( document.documentElement ).overflow;
1486 document.documentElement.style.overflow = 'hidden';
1487 scrollPos = window.scrollY || window.pageYOffset || 0;
1488
1489 carousel.container.setAttribute( 'data-carousel-extra', JSON.stringify( data ) );
1490 stat( [ 'open', 'view_image' ] );
1491
1492 // If options exist, lets merge them
1493 // with our default settings
1494 for ( var option in options || {} ) {
1495 settings[ option ] = options[ option ];
1496 }
1497
1498 if ( settings.startIndex === -1 ) {
1499 settings.startIndex = 0; // -1 returned if can't find index, so start from beginning
1500 }
1501
1502 domUtil.emitEvent( carousel.overlay, 'jp_carousel.beforeOpen' );
1503 carousel.gallery.innerHTML = '';
1504
1505 // Need to set the overlay manually to block or swiper does't initialise properly.
1506 carousel.overlay.style.opacity = 1;
1507 carousel.overlay.style.display = 'block';
1508
1509 initCarouselSlides( images, settings.startIndex );
1510
1511 swiper = new window.Swiper670( '.jp-carousel-swiper-container', {
1512 centeredSlides: true,
1513 zoom: true,
1514 loop: carousel.slides.length > 1,
1515 // Turn off interactions and hide navigation arrows if there is only one slide.
1516 enabled: carousel.slides.length > 1,
1517 pagination: {
1518 el: '.jp-swiper-pagination',
1519 clickable: true,
1520 },
1521 navigation: {
1522 nextEl: '.jp-swiper-button-next',
1523 prevEl: '.jp-swiper-button-prev',
1524 },
1525 initialSlide: settings.startIndex,
1526 on: {
1527 init: function () {
1528 selectSlideAtIndex( settings.startIndex );
1529 },
1530 },
1531 preventClicks: false,
1532 preventClicksPropagation: false,
1533 preventInteractionOnTransition: ! domUtil.isTouch(),
1534 threshold: 5,
1535 } );
1536
1537 swiper.on( 'slideChange', function ( swiper ) {
1538 var index;
1539 // Swiper indexes slides from 1, plus when looping to left last slide ends up
1540 // as 0 and looping to right first slide as total slides + 1. These are adjusted
1541 // here to match index of carousel.slides.
1542 if ( swiper.activeIndex === 0 ) {
1543 index = carousel.slides.length - 1;
1544 } else if ( swiper.activeIndex === carousel.slides.length + 1 ) {
1545 index = 0;
1546 } else {
1547 index = swiper.activeIndex - 1;
1548 }
1549 selectSlideAtIndex( index );
1550
1551 carousel.overlay.classList.remove( 'jp-carousel-hide-controls' );
1552 } );
1553
1554 swiper.on( 'zoomChange', function ( swiper, scale ) {
1555 if ( scale > 1 ) {
1556 carousel.overlay.classList.add( 'jp-carousel-hide-controls' );
1557 }
1558
1559 if ( scale === 1 ) {
1560 carousel.overlay.classList.remove( 'jp-carousel-hide-controls' );
1561 }
1562 } );
1563
1564 swiper.on( 'doubleTap', function ( swiper ) {
1565 clearTimeout( tapTimeout );
1566 if ( swiper.zoom.scale === 1 ) {
1567 var zoomTimeout = setTimeout( function () {
1568 carousel.overlay.classList.remove( 'jp-carousel-hide-controls' );
1569 clearTimeout( zoomTimeout );
1570 }, 150 );
1571 }
1572 } );
1573
1574 swiper.on( 'tap', function () {
1575 if ( swiper.zoom.scale > 1 ) {
1576 tapTimeout = setTimeout( function () {
1577 carousel.overlay.classList.toggle( 'jp-carousel-hide-controls' );
1578 }, 150 );
1579 }
1580 } );
1581
1582 domUtil.fadeIn( carousel.overlay, function () {
1583 domUtil.emitEvent( carousel.overlay, 'jp_carousel.afterOpen' );
1584 } );
1585 }
1586
1587 // Register the event listeners for starting the gallery
1588 document.body.addEventListener( 'click', handleInteraction );
1589 document.body.addEventListener( 'keydown', handleInteraction );
1590 document.querySelectorAll( galleryItemSelector + 'img' ).forEach( function ( galleryImage ) {
1591 if ( shouldOpenModal( galleryImage ) ) {
1592 makeGalleryImageAccessible( galleryImage );
1593 }
1594 } );
1595
1596 function handleInteraction( e ) {
1597 if ( e.type === 'click' ) {
1598 handleClick( e );
1599 return;
1600 }
1601
1602 if ( e.type === 'keydown' ) {
1603 const parentElement = document.activeElement.parentElement;
1604 const isParentCarouselContainer =
1605 parentElement && parentElement.classList.contains( 'tiled-gallery__item' );
1606
1607 if ( ( e.key === ' ' || e.key === 'Enter' ) && isParentCarouselContainer ) {
1608 handleClick( e );
1609 return;
1610 }
1611 }
1612 }
1613
1614 function shouldOpenModal( el ) {
1615 var parent = el.parentElement;
1616 var grandparent = parent.parentElement;
1617
1618 // If Gallery is made up of individual Image blocks check for custom link before
1619 // loading carousel. The custom link may be the parent or could be a descendant
1620 // of the parent if the image has rounded corners.
1621 var parentHref = null;
1622 if ( grandparent && grandparent.classList.contains( 'wp-block-image' ) ) {
1623 parentHref = parent.getAttribute( 'href' );
1624 } else if (
1625 parent &&
1626 parent.classList.contains( 'wp-block-image' ) &&
1627 parent.querySelector( ':scope > a' )
1628 ) {
1629 parentHref = parent.querySelector( ':scope > a' ).getAttribute( 'href' );
1630 }
1631
1632 // If the link does not point to the attachment or media file then assume Image has
1633 // a custom link so don't load the carousel.
1634 if (
1635 parentHref &&
1636 parentHref.split( '?' )[ 0 ] !== el.getAttribute( 'data-orig-file' ).split( '?' )[ 0 ] &&
1637 parentHref !== el.getAttribute( 'data-permalink' )
1638 ) {
1639 return false;
1640 }
1641
1642 // Do not open the modal if we are looking at a gallery caption from before WP5, which may contain a link.
1643 if ( parent.classList.contains( 'gallery-caption' ) ) {
1644 return false;
1645 }
1646
1647 // Do not open the modal if we are looking at a caption of a gallery block, which may contain a link.
1648 if ( domUtil.matches( parent, 'figcaption' ) ) {
1649 return false;
1650 }
1651
1652 return true;
1653 }
1654
1655 function handleClick( e ) {
1656 var isCompatible =
1657 window.CSS && window.CSS.supports && window.CSS.supports( 'display', 'grid' );
1658
1659 // IE11 support is being dropped in August 2021. The new swiper.js libray is not IE11 compat
1660 // so just default to opening individual image attachment/media pages for IE.
1661 if ( ! isCompatible ) {
1662 return;
1663 }
1664
1665 var target = e.target;
1666 var gallery = domUtil.closest( target, gallerySelector );
1667
1668 if ( gallery ) {
1669 if ( ! testForData( gallery ) ) {
1670 return;
1671 }
1672
1673 if ( ! shouldOpenModal( target ) ) {
1674 return;
1675 }
1676
1677 // Set height to auto.
1678 // Fix some themes where closing carousel brings view back to top.
1679 document.documentElement.style.height = 'auto';
1680
1681 e.preventDefault();
1682
1683 // Stopping propagation in case there are parent elements
1684 // with .gallery or .tiled-gallery class
1685 e.stopPropagation();
1686
1687 var item = domUtil.closest( target, itemSelector );
1688 var index = Array.prototype.indexOf.call( gallery.querySelectorAll( itemSelector ), item );
1689 loadSwiper( gallery, { startIndex: index } );
1690 }
1691 }
1692
1693 // Handle lightbox (single image gallery) for images linking to 'Attachment Page'.
1694 if ( Number( jetpackCarouselStrings.single_image_gallery ) === 1 ) {
1695 processSingleImageGallery();
1696 document.body.addEventListener( 'is.post-load', function () {
1697 processSingleImageGallery();
1698 } );
1699 }
1700
1701 // Makes carousel work on page load and when back button leads to same URL with carousel hash
1702 // (i.e. no actual document.ready trigger).
1703 window.addEventListener( 'hashchange', function () {
1704 var hashRegExp = /jp-carousel-(\d+)/;
1705
1706 if ( ! window.location.hash || ! hashRegExp.test( window.location.hash ) ) {
1707 if ( carousel.isOpen ) {
1708 closeCarousel();
1709 }
1710
1711 return;
1712 }
1713
1714 if ( window.location.hash === lastKnownLocationHash && carousel.isOpen ) {
1715 return;
1716 }
1717
1718 if ( window.location.hash && carousel.gallery && ! carousel.isOpen && history.back ) {
1719 history.back();
1720 return;
1721 }
1722
1723 lastKnownLocationHash = window.location.hash;
1724 var matchList = window.location.hash.match( hashRegExp );
1725 var attachmentId = parseInt( matchList[ 1 ], 10 );
1726 var galleries = document.querySelectorAll( gallerySelector );
1727
1728 // Find the first thumbnail that matches the attachment ID in the location
1729 // hash, then open the gallery that contains it.
1730 for ( var i = 0; i < galleries.length; i++ ) {
1731 var gallery = galleries[ i ];
1732 var selected;
1733
1734 var images = gallery.querySelectorAll( 'img' );
1735 for ( var j = 0; j < images.length; j++ ) {
1736 if (
1737 parseInt( images[ j ].getAttribute( 'data-attachment-id' ), 10 ) === attachmentId ||
1738 parseInt( images[ j ].getAttribute( 'data-id' ), 10 ) === attachmentId
1739 ) {
1740 selected = j;
1741 break;
1742 }
1743 }
1744
1745 if ( selected !== undefined ) {
1746 openOrSelectSlide( gallery, selected );
1747 break;
1748 }
1749 }
1750 } );
1751
1752 if ( window.location.hash ) {
1753 domUtil.emitEvent( window, 'hashchange' );
1754 }
1755 }
1756
1757 if ( document.readyState !== 'loading' ) {
1758 init();
1759 } else {
1760 document.addEventListener( 'DOMContentLoaded', init );
1761 }
1762 } )();
1763