PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 16.1-a.5
Jetpack – WP Security, Backup, Speed, & Growth v16.1-a.5
16.1-a.5 16.1-a.3 16.0.1 16.1-a.1 16.0 16.0-beta 16.0-a.7 16.0-a.5 15.9.1 16.0-a.3 16.0-a.1 15.9 15.9-beta 15.9-a.7 15.9-a.5 15.9-a.3 15.9-a.1 15.8 15.8-beta 15.8-a.7 15.8-a.5 5.2.5 5.3.4 5.4.4 5.5.5 5.6.5 5.7.5 5.8.4 5.9.4 6.0.4 6.1 6.1.1 6.1.2 6.1.3 6.1.4 6.1.5 6.2 6.2.1 6.2.2 6.2.3 6.2.4 6.2.5 6.3 6.3.1 6.3.2 6.3.3 6.3.4 6.3.5 6.3.6 6.3.7 6.4 6.4.1 6.4.2 6.4.3 6.4.4 6.4.5 6.4.6 6.5 6.5.1 6.5.2 6.5.3 6.5.4 6.6 6.6.1 6.6.2 6.6.3 6.6.4 6.6.5 6.7 6.7.1 6.7.2 6.7.3 6.7.4 6.8 6.8.1 6.8.2 6.8.3 6.8.4 6.8.5 6.9 6.9.1 6.9.2 6.9.3 6.9.4 7.0 7.0.1 7.0.2 7.0.3 7.0.4 7.0.5 7.1 7.1.1 7.1.2 7.1.3 7.1.4 7.1.5 7.2 7.2.1 7.2.1.1 7.2.2 7.2.3 7.2.4 7.2.5 7.3 7.3.0.1 7.3.1 7.3.1.1 7.3.2 7.3.3 7.3.4 7.3.5 7.4 7.4.1 7.4.2 7.4.3 7.4.4 7.4.5 7.5 7.5.0.1 7.5.1 7.5.2 7.5.3 7.5.4 7.5.5 7.5.6 7.5.7 7.6 7.6.1 7.6.2 7.6.3 7.6.4 7.7 7.7.1 7.7.2 7.7.3 7.7.4 7.7.5 7.7.6 7.8 7.8.1 7.8.2 7.8.3 7.8.4 7.9 7.9.1 7.9.2 7.9.3 7.9.4 8.0 8.0.1 8.0.2 8.0.3 8.1 8.1.1 8.1.2 8.1.3 8.1.4 8.2 8.2.0.1 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.2.6 8.3 8.3.1 8.3.2 8.3.3 8.4 8.4.1 8.4.2 8.4.3 8.4.4 8.4.5 8.5 8.5.1 8.5.2 8.5.3 8.6 8.6.1 8.6.2 8.6.3 8.6.4 8.7 8.7.0.1 8.7.1 8.7.2 8.7.3 8.7.4 8.8 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.9 8.9.1 8.9.2 8.9.3 8.9.4 9.0 9.0.1 9.0.2 9.0.3 9.0.4 9.0.5 9.1 9.1.1 9.1.2 9.1.3 9.2 9.2.1 9.2.2 9.2.3 9.2.4 9.3 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.4 9.4.1 9.4.2 9.4.3 9.4.4 9.5 9.5.1 9.5.2 9.5.3 9.5.4 9.5.5 9.6 9.6.1 9.6.2 9.6.3 9.6.4 9.7 9.7.1 9.7.2 15.7-beta.2 9.7.3 15.7.1 9.8 15.8-a.1 9.8.1 15.8-a.3 9.8.2 2.0.9 9.8.3 2.1.7 9.9 2.2.10 9.9.1 2.3.10 9.9.2 2.4.7 9.9.3 2.5.5 2.6.6 2.7.5 2.8.5 2.9.6 3.0.6 3.1.5 3.2.5 3.3.6 3.4.6 3.5.6 3.6.4 3.7.5 3.8.5 3.9.10 4.0.7 4.1.4 4.2.5 4.3.5 4.4.5 4.5.3 4.6.3 4.7.4 4.8.5 4.9.3 5.0.3 5.1.4 trunk 10.0 10.0.1 10.0.2 10.1 10.1.1 10.1.2 10.2 10.2.1 10.2.2 10.2.3 10.3 10.3.1 10.3.2 10.4 10.4.1 10.4.2 10.5 10.5.1 10.5.2 10.5.3 10.6 10.6.1 10.6.2 10.7 10.7.1 10.7.2 10.8 10.8.1 10.8.2 10.9 10.9.1 10.9.2 10.9.3 11.0 11.0.1 11.0.2 11.1 11.1.1 11.1.2 11.1.3 11.1.4 11.2 11.2.1 11.2.2 11.3 11.3.1 11.3.2 11.3.3 11.3.4 11.4 11.4.1 11.4.2 11.5 11.5.1 11.5.2 11.5.3 11.6 11.6.1 11.6.2 11.7 11.7.1 11.7.2 11.7.3 11.8 11.8.3 11.8.4 11.8.5 11.8.6 11.9 11.9.1 11.9.2 11.9.3 12.0 12.0.1 12.0.2 12.1 12.1.1 12.1.2 12.2 12.2.1 12.2.2 12.3 12.3.1 12.4 12.4.1 12.5 12.5.1 12.6 12.6.1 12.6.2 12.6.3 12.7 12.7.1 12.7.2 12.8 12.8.1 12.8.2 12.9 12.9.1 12.9.2 12.9.3 12.9.4 13.0 13.0.1 13.1 13.1.1 13.1.2 13.1.3 13.1.4 13.2 13.2.1 13.2.2 13.2.3 13.3 13.3.1 13.3.2 13.4 13.4.1 13.4.2 13.4.3 13.4.4 13.5 13.5.1 13.6 13.6.1 13.7 13.7.1 13.8 13.8.1 13.8.2 13.9 13.9.1 14.0 14.1 14.2 14.2.1 14.3 14.4 14.4.1 14.5 14.6 14.7 14.8 14.9 14.9.1 15.0 15.0.1 15.0.2 15.1 15.1.1 15.2 15.3 15.3.1 15.4 15.5 15.6 15.7 15.7-a.1 15.7-a.3 15.7-a.5 15.7-a.7 15.7-beta
jetpack / modules / carousel / jetpack-carousel.js
jetpack / modules / carousel Last commit date
jetpack-carousel-rtl.css 1 day ago jetpack-carousel.css 1 day ago jetpack-carousel.js 1 day ago jetpack-carousel.php 1 day ago
jetpack-carousel.js
1912 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; // eslint-disable-line no-useless-assignment -- Verify this isn't needed to free memory or something.
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 var tmp = document.createElement( 'div' );
276 tmp.innerHTML = text.replace( /<[^>]*>?/gm, '' );
277 return tmp.textContent;
278 }
279
280 return {
281 closest: closest,
282 matches: matches,
283 hide: hide,
284 show: show,
285 fadeIn: fadeIn,
286 fadeOut: fadeOut,
287 scrollToElement: scrollToElement,
288 getJSONAttribute: getJSONAttribute,
289 convertToPlainText: convertToPlainText,
290 stripHTML: stripHTML,
291 emitEvent: emitEvent,
292 isTouch: isTouch,
293 };
294 } )();
295
296 /////////////////////////////////////
297 // Carousel implementation
298 /////////////////////////////////////
299 function init() {
300 var commentInterval;
301 var screenPadding;
302 var originalOverflow;
303 var originalHOverflow;
304 var scrollPos;
305
306 var lastKnownLocationHash = '';
307 var isUserTyping = false;
308
309 var gallerySelector =
310 'div.gallery, div.tiled-gallery, ul.wp-block-gallery, ul.blocks-gallery-grid, ' +
311 'figure.wp-block-gallery.has-nested-images, div.wp-block-jetpack-tiled-gallery, a.single-image-gallery';
312
313 // Selector for items within a gallery or tiled gallery.
314 var galleryItemSelector =
315 '.gallery-item, .tiled-gallery-item, .blocks-gallery-item, ' + ' .tiled-gallery__item';
316
317 // Selector for all items including single images.
318 var itemSelector = galleryItemSelector + ', .wp-block-image';
319
320 var carousel = {};
321
322 var stat =
323 typeof wpcom !== 'undefined' && wpcom.carousel && wpcom.carousel.stat
324 ? wpcom.carousel.stat
325 : util.noop;
326
327 var pageview =
328 typeof wpcom !== 'undefined' && wpcom.carousel && wpcom.carousel.pageview
329 ? wpcom.carousel.pageview
330 : util.noop;
331
332 function handleKeyboardEvent( e ) {
333 if ( ! isUserTyping ) {
334 switch ( e.which ) {
335 case 38: // up
336 e.preventDefault();
337 carousel.overlay.scrollTop -= 100;
338 break;
339 case 40: // down
340 e.preventDefault();
341 carousel.overlay.scrollTop += 100;
342 break;
343 case 39: // right
344 e.preventDefault();
345 swiper.slideNext();
346 break;
347 case 37: // left
348 case 8: // backspace
349 e.preventDefault();
350 swiper.slidePrev();
351 break;
352 case 27: // escape
353 e.preventDefault();
354 closeCarousel();
355 break;
356 default:
357 break;
358 }
359 }
360 }
361
362 function disableKeyboardNavigation() {
363 isUserTyping = true;
364 }
365
366 function enableKeyboardNavigation() {
367 isUserTyping = false;
368 }
369
370 function calculatePadding() {
371 var baseScreenPadding = 110;
372 screenPadding = baseScreenPadding;
373
374 if ( window.innerWidth <= 760 ) {
375 screenPadding = Math.round( ( window.innerWidth / 760 ) * baseScreenPadding );
376
377 if ( screenPadding < 40 && domUtil.isTouch() ) {
378 screenPadding = 0;
379 }
380 }
381 }
382
383 function makeGalleryImageAccessible( img ) {
384 img.role = 'button';
385 img.tabIndex = 0;
386 img.ariaLabel = jetpackCarouselStrings.image_label;
387 }
388
389 function initializeCarousel() {
390 if ( ! carousel.overlay ) {
391 carousel.overlay = document.querySelector( '.jp-carousel-overlay' );
392 carousel.container = carousel.overlay.querySelector( '.jp-carousel-wrap' );
393 carousel.gallery = carousel.container.querySelector( '.jp-carousel' );
394 carousel.info = carousel.overlay.querySelector( '.jp-carousel-info' );
395 carousel.caption = carousel.info.querySelector( '.jp-carousel-caption' );
396 carousel.commentField = carousel.overlay.querySelector(
397 '#jp-carousel-comment-form-comment-field'
398 );
399 carousel.emailField = carousel.overlay.querySelector(
400 '#jp-carousel-comment-form-email-field'
401 );
402 carousel.authorField = carousel.overlay.querySelector(
403 '#jp-carousel-comment-form-author-field'
404 );
405 carousel.urlField = carousel.overlay.querySelector( '#jp-carousel-comment-form-url-field' );
406
407 calculatePadding();
408
409 [
410 carousel.commentField,
411 carousel.emailField,
412 carousel.authorField,
413 carousel.urlField,
414 ].forEach( function ( field ) {
415 if ( field ) {
416 field.addEventListener( 'focus', disableKeyboardNavigation );
417 field.addEventListener( 'blur', enableKeyboardNavigation );
418 }
419 } );
420
421 carousel.overlay.addEventListener( 'click', function ( e ) {
422 var target = e.target;
423 var isTargetCloseHint = !! domUtil.closest( target, '.jp-carousel-close-hint' );
424 var isSmallScreen = !! window.matchMedia( '(max-device-width: 760px)' ).matches;
425 if ( target === carousel.overlay ) {
426 if ( ! isSmallScreen ) {
427 closeCarousel();
428 }
429 } else if ( isTargetCloseHint ) {
430 closeCarousel();
431 } else if ( target.classList.contains( 'jp-carousel-image-download' ) ) {
432 stat( 'download_original_click' );
433 } else if ( target.classList.contains( 'jp-carousel-comment-login' ) ) {
434 handleCommentLoginClick( e );
435 } else if ( domUtil.closest( target, '#jp-carousel-comment-form-container' ) ) {
436 handleCommentFormClick( e );
437 } else if (
438 domUtil.closest( target, '.jp-carousel-photo-icons-container' ) ||
439 target.classList.contains( 'jp-carousel-photo-title' )
440 ) {
441 handleFooterElementClick( e );
442 }
443 } );
444
445 window.addEventListener( 'keydown', handleKeyboardEvent );
446
447 carousel.overlay.addEventListener( 'jp_carousel.afterOpen', function () {
448 enableKeyboardNavigation();
449
450 // Don't show navigation if there's only one image.
451 if ( carousel.slides.length <= 1 ) {
452 return;
453 }
454 // Show dot pagination if slide count is <= 5, otherwise show n/total.
455 if ( carousel.slides.length <= 5 ) {
456 domUtil.show( carousel.info.querySelector( '.jp-swiper-pagination' ) );
457 } else {
458 domUtil.show( carousel.info.querySelector( '.jp-carousel-pagination' ) );
459 }
460 } );
461
462 carousel.overlay.addEventListener( 'jp_carousel.beforeClose', function () {
463 disableKeyboardNavigation();
464
465 // Fixes some themes where closing carousel brings view back to top.
466 document.documentElement.style.removeProperty( 'height' );
467
468 // If we disable the swiper (because there's only one image)
469 // we have to re-enable it here again as Swiper doesn't, for some reason,
470 // show the navigation buttons again after reinitialization.
471 if ( swiper ) {
472 swiper.enable();
473 }
474
475 // Hide pagination.
476 domUtil.hide( carousel.info.querySelector( '.jp-swiper-pagination' ) );
477 domUtil.hide( carousel.info.querySelector( '.jp-carousel-pagination' ) );
478 } );
479
480 carousel.overlay.addEventListener( 'jp_carousel.afterClose', function () {
481 // don't force the browser back when the carousel closes.
482 if ( window.history.pushState ) {
483 history.pushState(
484 '',
485 document.title,
486 window.location.pathname + window.location.search
487 );
488 } else {
489 window.location.href = '';
490 }
491 lastKnownLocationHash = '';
492 carousel.isOpen = false;
493 } );
494
495 // Prevent native browser zooming
496 carousel.overlay.addEventListener( 'touchstart', function ( e ) {
497 if ( e.touches.length > 1 ) {
498 e.preventDefault();
499 }
500 } );
501 }
502 }
503
504 function handleCommentLoginClick() {
505 var slide = carousel.currentSlide;
506 var attachmentId = slide ? slide.attrs.attachmentId : '0';
507
508 window.location.href = jetpackCarouselStrings.login_url + '%23jp-carousel-' + attachmentId;
509 }
510
511 function updatePostResults( msg, isSuccess ) {
512 var results = carousel.overlay.querySelector( '#jp-carousel-comment-post-results' );
513 var elClass = 'jp-carousel-comment-post-' + ( isSuccess ? 'success' : 'error' );
514 results.innerHTML = '<span class="' + elClass + '">' + msg + '</span>';
515 domUtil.hide( carousel.overlay.querySelector( '#jp-carousel-comment-form-spinner' ) );
516 carousel.overlay
517 .querySelector( '#jp-carousel-comment-form' )
518 .classList.remove( 'jp-carousel-is-disabled' );
519 domUtil.show( results );
520 }
521
522 function handleCommentFormClick( e ) {
523 var target = e.target;
524 var data = domUtil.getJSONAttribute( carousel.container, 'data-carousel-extra' ) || {};
525 var attachmentId = carousel.currentSlide.attrs.attachmentId;
526
527 var wrapper = document.querySelector( '#jp-carousel-comment-form-submit-and-info-wrapper' );
528 var spinner = document.querySelector( '#jp-carousel-comment-form-spinner' );
529 // eslint-disable-next-line @wordpress/no-unused-vars-before-return
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 {
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 // Load current image immediately
783 loadFullImage( carousel.slides[ index ] );
784
785 // Preload adjacent images in background
786 preloadAdjacentImages( index );
787
788 if (
789 Number( jetpackCarouselStrings.display_background_image ) === 1 &&
790 ! carousel.slides[ index ].backgroundImage
791 ) {
792 loadBackgroundImage( carousel.slides[ index ] );
793 }
794
795 domUtil.hide( carousel.caption );
796 updateTitleCaptionAndDesc( {
797 caption: current.attrs.caption,
798 title: current.attrs.title,
799 desc: current.attrs.desc,
800 } );
801
802 var imageMeta = carousel.slides[ index ].attrs.imageMeta;
803 updateExif( imageMeta );
804 updateFullSizeLink( current );
805
806 if ( Number( jetpackCarouselStrings.display_comments ) === 1 ) {
807 testCommentsOpened( carousel.slides[ index ].attrs.commentsOpened );
808 fetchComments( attachmentId );
809 domUtil.hide( carousel.info.querySelector( '#jp-carousel-comment-post-results' ) );
810 }
811
812 // Update pagination in footer.
813 var pagination = carousel.info.querySelector( '.jp-carousel-pagination' );
814 if ( pagination && carousel.slides.length > 5 ) {
815 var currentPage = index + 1;
816 pagination.innerHTML = '<span>' + currentPage + ' / ' + carousel.slides.length + '</span>';
817 }
818
819 // Record pageview in WP Stats, for each new image loaded full-screen.
820 if ( jetpackCarouselStrings.stats && carousel.isOpen ) {
821 new Image().src =
822 document.location.protocol +
823 '//pixel.wp.com/g.gif?' +
824 jetpackCarouselStrings.stats +
825 '&post=' +
826 encodeURIComponent( attachmentId ) +
827 '&rand=' +
828 Math.random();
829 }
830
831 if ( carousel.isOpen ) {
832 pageview( attachmentId );
833 }
834
835 lastKnownLocationHash = '#jp-carousel-' + attachmentId;
836 window.location.hash = lastKnownLocationHash;
837 }
838
839 function restoreScroll() {
840 window.scrollTo( window.scrollX || window.pageXOffset || 0, scrollPos || 0 );
841 }
842
843 function closeCarousel() {
844 // Make sure to let the page scroll again.
845 document.body.style.overflow = originalOverflow;
846 document.documentElement.style.overflow = originalHOverflow;
847 clearCommentTextAreaValue();
848
849 disableKeyboardNavigation();
850
851 domUtil.emitEvent( carousel.overlay, 'jp_carousel.beforeClose' );
852 restoreScroll();
853 carousel.isOpen = false;
854 swiper.destroy();
855 // Clear slide data for DOM garbage collection.
856 carousel.slides = [];
857 carousel.currentSlide = undefined;
858 carousel.gallery.innerHTML = '';
859
860 domUtil.fadeOut( carousel.overlay, function () {
861 domUtil.emitEvent( carousel.overlay, 'jp_carousel.afterClose' );
862 } );
863 }
864
865 function calculateMaxSlideDimensions() {
866 return {
867 width: window.innerWidth,
868 height: window.innerHeight - 64, //subtract height of bottom info bar,
869 };
870 }
871
872 function sanitizePhotonUrl( url ) {
873 var urlObj;
874 try {
875 urlObj = new URL( url );
876 // eslint-disable-next-line no-unused-vars
877 } catch ( e ) {
878 return url;
879 }
880
881 var whitelistedPhotonArgs = [
882 'quality',
883 'ssl',
884 'filter',
885 'brightness',
886 'contrast',
887 'colorize',
888 'smooth',
889 ];
890
891 // Get all search params
892 var searchParams = Array.from( urlObj.searchParams.entries() );
893
894 // Clear all existing params
895 urlObj.search = '';
896
897 // Only add back whitelisted params
898 searchParams.forEach( ( [ key, value ] ) => {
899 if ( whitelistedPhotonArgs.includes( key ) ) {
900 urlObj.searchParams.append( key, value );
901 }
902 } );
903
904 return urlObj;
905 }
906
907 function selectBestImageUrl( args ) {
908 if ( typeof args !== 'object' ) {
909 args = {};
910 }
911
912 if ( typeof args.origFile === 'undefined' ) {
913 return '';
914 }
915
916 if ( typeof args.origWidth === 'undefined' || typeof args.maxWidth === 'undefined' ) {
917 return args.origFile;
918 }
919
920 // When there's no large file to fall back on (e.g. images that weren't enriched with
921 // Jetpack's data-large-file attribute), use the original file. A missing attribute is
922 // read as an empty string, so we can't only guard against `undefined` here: otherwise a
923 // narrow (portrait, mobile) viewport would return that empty string as the image source,
924 // leaving the carousel with a blank slide.
925 if ( ! args.largeFile ) {
926 return args.origFile;
927 }
928
929 // Check if the image is being served by Photon (using a regular expression on the hostname).
930
931 var imageLinkParser = document.createElement( 'a' );
932 imageLinkParser.href = args.largeFile;
933
934 var isPhotonUrl = /^i[0-2]\.wp\.com$/i.test( imageLinkParser.hostname );
935
936 var largeSizeParts = getImageSizeParts( args.largeFile, args.origWidth, isPhotonUrl );
937
938 var largeWidth = parseInt( largeSizeParts[ 0 ], 10 );
939 var largeHeight = parseInt( largeSizeParts[ 1 ], 10 );
940
941 args.origMaxWidth = args.maxWidth;
942 args.origMaxHeight = args.maxHeight;
943
944 // Give devices with a higher devicePixelRatio higher-res images (Retina display = 2, Android phones = 1.5, etc)
945 if ( typeof window.devicePixelRatio !== 'undefined' && window.devicePixelRatio > 1 ) {
946 args.maxWidth = args.maxWidth * window.devicePixelRatio;
947 args.maxHeight = args.maxHeight * window.devicePixelRatio;
948 }
949
950 if ( largeWidth >= args.maxWidth || largeHeight >= args.maxHeight ) {
951 return args.largeFile;
952 }
953
954 if ( isPhotonUrl ) {
955 // args.origFile doesn't point to a Photon url, so in this case we use args.largeFile
956 // to return the photon url of the original image.
957 if ( args.largeFile.lastIndexOf( '?' ) === -1 ) {
958 return args.largeFile;
959 }
960
961 // Sanitize the URL to remove non-cosmetic changes like resize, fit, etc.
962 var sanitizedUrl = sanitizePhotonUrl( args.largeFile );
963
964 // If we have a really large image load a smaller version
965 // that is closer to the viewable size
966 if ( args.origWidth > args.maxWidth || args.origHeight > args.maxHeight ) {
967 // @2x the max sizes so we get a high enough resolution for zooming.
968 args.origMaxWidth = args.maxWidth * 2;
969 args.origMaxHeight = args.maxHeight * 2;
970 // Add the fit arg to the list of Photon args.
971 sanitizedUrl.searchParams.set( 'fit', args.origMaxWidth + ',' + args.origMaxHeight );
972 }
973
974 // Return a Photon URL image that's better fitted for the viewport.
975 return sanitizedUrl.toString();
976 }
977
978 return args.origFile;
979 }
980
981 function getImageSizeParts( file, origWidth, isPhotonUrl ) {
982 var size = isPhotonUrl
983 ? file.replace( /.*=([\d]+%2C[\d]+).*$/, '$1' )
984 : file.replace( /.*-([\d]+x[\d]+)\..+$/, '$1' );
985
986 var sizeParts;
987 if ( size !== file ) {
988 sizeParts = isPhotonUrl ? size.split( '%2C' ) : size.split( 'x' );
989 } else {
990 sizeParts = [ origWidth, 0 ];
991 }
992
993 // If one of the dimensions is set to 9999, then the actual value of that dimension can't be retrieved from the url.
994 // In that case, we set the value to 0.
995 if ( sizeParts[ 0 ] === '9999' ) {
996 sizeParts[ 0 ] = '0';
997 }
998
999 if ( sizeParts[ 1 ] === '9999' ) {
1000 sizeParts[ 1 ] = '0';
1001 }
1002
1003 return sizeParts;
1004 }
1005
1006 /**
1007 * Returns a number in a fraction format that represents the shutter speed.
1008 * @param Number speed
1009 * @return String
1010 */
1011 function formatShutterSpeed( speed ) {
1012 var denominator;
1013
1014 // round to one decimal if value > 1s by multiplying it by 10, rounding, then dividing by 10 again
1015 if ( speed >= 1 ) {
1016 return Math.round( speed * 10 ) / 10 + 's';
1017 }
1018
1019 // If the speed is less than one, we find the denominator by inverting
1020 // the number. Since cameras usually use rational numbers as shutter
1021 // speeds, we should get a nice round number. Or close to one in cases
1022 // like 1/30. So we round it.
1023 denominator = Math.round( 1 / speed );
1024
1025 return '1/' + denominator + 's';
1026 }
1027
1028 function parseTitleOrDesc( value ) {
1029 if ( ! value.match( ' ' ) && value.match( '_' ) ) {
1030 return '';
1031 }
1032
1033 return value;
1034 }
1035
1036 function updateTitleCaptionAndDesc( data ) {
1037 var caption;
1038 var title;
1039 var desc;
1040 var captionMainElement;
1041 var captionInfoExtraElement;
1042 var titleElement;
1043 var descriptionElement;
1044
1045 captionMainElement = carousel.overlay.querySelector( '.jp-carousel-photo-caption' );
1046 captionInfoExtraElement = carousel.overlay.querySelector( '.jp-carousel-caption' );
1047
1048 titleElement = carousel.overlay.querySelector( '.jp-carousel-photo-title' );
1049 descriptionElement = carousel.overlay.querySelector( '.jp-carousel-photo-description' );
1050
1051 domUtil.hide( captionMainElement );
1052 domUtil.hide( captionInfoExtraElement );
1053 domUtil.hide( titleElement );
1054 domUtil.hide( descriptionElement );
1055
1056 caption = parseTitleOrDesc( data.caption ) || '';
1057 title = parseTitleOrDesc( data.title ) || '';
1058 desc = parseTitleOrDesc( data.desc ) || '';
1059
1060 if ( caption || title || desc ) {
1061 if ( caption ) {
1062 captionMainElement.innerHTML = caption;
1063 captionInfoExtraElement.innerHTML = caption;
1064
1065 domUtil.show( captionMainElement );
1066 domUtil.show( captionInfoExtraElement );
1067 }
1068
1069 if ( domUtil.stripHTML( caption ) === domUtil.stripHTML( title ) ) {
1070 title = '';
1071 }
1072
1073 if ( domUtil.stripHTML( caption ) === domUtil.stripHTML( desc ) ) {
1074 desc = '';
1075 }
1076
1077 if ( domUtil.stripHTML( title ) === domUtil.stripHTML( desc ) ) {
1078 desc = '';
1079 }
1080
1081 if ( desc ) {
1082 descriptionElement.innerHTML = desc;
1083 domUtil.show( descriptionElement );
1084
1085 if ( ! title && ! caption ) {
1086 captionMainElement.textContent = domUtil.stripHTML( desc );
1087 domUtil.show( captionMainElement );
1088 }
1089 }
1090
1091 if ( title ) {
1092 var plainTitle = domUtil.stripHTML( title );
1093 titleElement.textContent = plainTitle;
1094
1095 if ( ! caption ) {
1096 captionMainElement.textContent = plainTitle;
1097 captionInfoExtraElement.textContent = plainTitle;
1098
1099 domUtil.show( captionMainElement );
1100 }
1101
1102 domUtil.show( titleElement );
1103 }
1104 }
1105 }
1106
1107 // updateExif updates the contents of the exif UL (.jp-carousel-image-exif)
1108 function updateExif( meta ) {
1109 if ( ! meta || Number( jetpackCarouselStrings.display_exif ) !== 1 ) {
1110 return false;
1111 }
1112
1113 // Locate the parent container for the metadata.
1114 var metaContainer = carousel.info.querySelector( '.jp-carousel-image-meta' );
1115 if ( ! metaContainer ) {
1116 return false;
1117 }
1118
1119 var html = '';
1120
1121 for ( var key in meta ) {
1122 var val = meta[ key ];
1123 var metaKeys = jetpackCarouselStrings.meta_data || [];
1124
1125 if ( parseFloat( val ) === 0 || ! val.length || metaKeys.indexOf( key ) === -1 ) {
1126 continue;
1127 }
1128
1129 switch ( key ) {
1130 case 'focal_length':
1131 val = val + 'mm';
1132 break;
1133 case 'shutter_speed':
1134 val = formatShutterSpeed( val );
1135 break;
1136 case 'aperture':
1137 val = 'f/' + val;
1138 break;
1139 }
1140
1141 html += '<li><h5>' + jetpackCarouselStrings[ key ] + '</h5>' + val + '</li>';
1142 }
1143
1144 // Handle the UL element dynamically to ensure valid markup.
1145 var ul = metaContainer.querySelector( 'ul.jp-carousel-image-exif' );
1146
1147 if ( html !== '' ) {
1148 // If there is data to display and the UL doesn't exist, create it.
1149 if ( ! ul ) {
1150 ul = document.createElement( 'ul' );
1151 ul.className = 'jp-carousel-image-exif';
1152
1153 // Insert right after the title/caption container if it exists, otherwise prepend
1154 var titleAndCaption = metaContainer.querySelector( '.jp-carousel-title-and-caption' );
1155 if ( titleAndCaption && titleAndCaption.nextSibling ) {
1156 metaContainer.insertBefore( ul, titleAndCaption.nextSibling );
1157 } else {
1158 metaContainer.insertBefore( ul, metaContainer.firstChild );
1159 }
1160 }
1161 ul.innerHTML = html;
1162 ul.style.removeProperty( 'display' );
1163 } else if ( ul ) {
1164 // If the data is empty but the UL exists in the DOM, remove it.
1165 ul.parentNode.removeChild( ul );
1166 }
1167 }
1168
1169 // Update the contents of the jp-carousel-image-download link
1170 function updateFullSizeLink( currentSlide ) {
1171 if ( ! currentSlide ) {
1172 return false;
1173 }
1174 var original;
1175 var origSize = [ currentSlide.attrs.origWidth, currentSlide.attrs.origHeight ];
1176 var imageLinkParser = document.createElement( 'a' );
1177
1178 imageLinkParser.href = currentSlide.attrs.src.replace( /\?.+$/, '' );
1179
1180 // Is this a Photon URL?
1181 if ( imageLinkParser.hostname.match( /^i[\d]{1}\.wp\.com$/i ) !== null ) {
1182 original = imageLinkParser.href;
1183 } else {
1184 original = currentSlide.attrs.origFile.replace( /\?.+$/, '' );
1185 }
1186
1187 var downloadText = carousel.info.querySelector( '.jp-carousel-download-text' );
1188 var permalink = carousel.info.querySelector( '.jp-carousel-image-download' );
1189
1190 downloadText.innerHTML = util.applyReplacements(
1191 jetpackCarouselStrings.download_original,
1192 origSize
1193 );
1194 permalink.setAttribute( 'href', original );
1195 permalink.style.removeProperty( 'display' );
1196 }
1197
1198 function testCommentsOpened( opened ) {
1199 var commentForm = carousel.info.querySelector( '#jp-carousel-comment-form-container' );
1200 var isOpened = parseInt( opened, 10 ) === 1;
1201
1202 if ( isOpened ) {
1203 domUtil.fadeIn( commentForm );
1204 } else {
1205 domUtil.fadeOut( commentForm );
1206 }
1207 }
1208
1209 function fetchComments( attachmentId, offset ) {
1210 var shouldClear = offset === undefined;
1211 var commentsIndicator = carousel.info.querySelector(
1212 '.jp-carousel-icon-comments .jp-carousel-has-comments-indicator'
1213 );
1214
1215 commentsIndicator.classList.remove( 'jp-carousel-show' );
1216
1217 clearInterval( commentInterval );
1218
1219 if ( ! attachmentId ) {
1220 return;
1221 }
1222
1223 if ( ! offset || offset < 1 ) {
1224 offset = 0;
1225 }
1226
1227 var comments = carousel.info.querySelector( '.jp-carousel-comments' );
1228 var commentsLoading = carousel.info.querySelector( '#jp-carousel-comments-loading' );
1229 domUtil.show( commentsLoading );
1230
1231 if ( shouldClear ) {
1232 domUtil.hide( comments );
1233 comments.innerHTML = '';
1234 }
1235
1236 var xhr = new XMLHttpRequest();
1237 var url =
1238 jetpackCarouselStrings.ajaxurl +
1239 '?action=get_attachment_comments' +
1240 '&nonce=' +
1241 jetpackCarouselStrings.nonce +
1242 '&id=' +
1243 attachmentId +
1244 '&offset=' +
1245 offset;
1246 xhr.open( 'GET', url );
1247 xhr.setRequestHeader( 'X-Requested-With', 'XMLHttpRequest' );
1248
1249 var onError = function () {
1250 domUtil.fadeIn( comments );
1251 domUtil.fadeOut( commentsLoading );
1252 };
1253
1254 xhr.onload = function () {
1255 // Ignore the results if they arrive late and we're now on a different slide.
1256 if (
1257 ! carousel.currentSlide ||
1258 carousel.currentSlide.attrs.attachmentId !== attachmentId
1259 ) {
1260 return;
1261 }
1262
1263 var isSuccess = xhr.status >= 200 && xhr.status < 300;
1264 var data;
1265 try {
1266 data = JSON.parse( xhr.responseText );
1267 } catch {
1268 // Do nothing.
1269 }
1270
1271 if ( ! isSuccess || ! data || ! Array.isArray( data ) ) {
1272 return onError();
1273 }
1274
1275 if ( shouldClear ) {
1276 comments.innerHTML = '';
1277 }
1278
1279 for ( var i = 0; i < data.length; i++ ) {
1280 var entry = data[ i ];
1281 var comment = document.createElement( 'div' );
1282 comment.classList.add( 'jp-carousel-comment' );
1283 comment.setAttribute( 'id', 'jp-carousel-comment-' + entry.id );
1284 comment.innerHTML =
1285 '<div class="comment-gravatar">' +
1286 entry.gravatar_markup +
1287 '</div>' +
1288 '<div class="comment-content">' +
1289 '<div class="comment-author">' +
1290 entry.author_markup +
1291 '</div>' +
1292 '<div class="comment-date">' +
1293 entry.date_gmt +
1294 '</div>' +
1295 entry.content +
1296 '</div>';
1297 comments.appendChild( comment );
1298
1299 // Set the interval to check for a new page of comments.
1300 clearInterval( commentInterval );
1301 commentInterval = setInterval( function () {
1302 if ( carousel.container.scrollTop + 150 > window.innerHeight ) {
1303 fetchComments( attachmentId, offset + 10 );
1304 clearInterval( commentInterval );
1305 }
1306 }, 300 );
1307 }
1308
1309 if ( data.length > 0 ) {
1310 domUtil.show( comments );
1311 commentsIndicator.innerText = data.length;
1312 commentsIndicator.classList.add( 'jp-carousel-show' );
1313 }
1314
1315 domUtil.hide( commentsLoading );
1316 };
1317
1318 xhr.onerror = onError;
1319
1320 xhr.send();
1321 }
1322
1323 function loadFullImage( slide ) {
1324 var attrs = slide.attrs;
1325 var image = slide.el.querySelector( 'img' );
1326
1327 if ( image.hasAttribute( 'data-loaded' ) ) {
1328 return;
1329 }
1330
1331 image.setAttribute( 'itemprop', 'image' );
1332 image.setAttribute( 'data-loaded', 1 );
1333
1334 var hasPreview = attrs.previewImage && attrs.previewImage !== attrs.src;
1335
1336 if ( ! hasPreview ) {
1337 // No usable in-page thumbnail (e.g. a lazy-loading plugin swapped the
1338 // gallery src for a placeholder). Load the full-size image straight
1339 // into the visible element so the slide is never left without a src.
1340 image.src = attrs.src;
1341 return;
1342 }
1343
1344 // Show the thumbnail the browser has already decoded for this image in the
1345 // post itself. Without it the slide stays empty until the full-size image
1346 // arrives, which reads as a black screen whenever the reader moves through
1347 // the gallery faster than the images can download.
1348 image.src = attrs.previewImage;
1349 // The thumbnail is much smaller than the slide, so soften the upscale
1350 // until the full-size image replaces it.
1351 image.style.filter = 'blur(8px)';
1352
1353 // Load the full-size image off-DOM, then swap it in over the preview. On
1354 // error the (blurred) preview stays put rather than reverting to blank.
1355 var fullImage = new window.Image();
1356
1357 fullImage.addEventListener(
1358 'load',
1359 function () {
1360 // Cached by this point, so swapping it in is effectively instant.
1361 image.src = attrs.src;
1362 image.style.filter = '';
1363 },
1364 { once: true }
1365 );
1366
1367 fullImage.addEventListener(
1368 'error',
1369 function () {
1370 image.style.filter = '';
1371 },
1372 { once: true }
1373 );
1374
1375 fullImage.src = attrs.src;
1376 }
1377
1378 function preloadAdjacentImages( currentIndex ) {
1379 var indicesToPreload = [];
1380 var totalSlides = carousel.slides.length;
1381
1382 // Only preload adjacent images if we have more than one slide (matching loop condition)
1383 if ( totalSlides > 1 ) {
1384 // Previous image (with loop handling)
1385 var prevIndex = currentIndex > 0 ? currentIndex - 1 : totalSlides - 1;
1386 indicesToPreload.push( prevIndex );
1387
1388 // Next image (with loop handling)
1389 var nextIndex = currentIndex < totalSlides - 1 ? currentIndex + 1 : 0;
1390 indicesToPreload.push( nextIndex );
1391 }
1392
1393 indicesToPreload.forEach( function ( index ) {
1394 var slide = carousel.slides[ index ];
1395 if ( slide ) {
1396 // Load in background without showing
1397 loadFullImage( slide );
1398
1399 // Also load background image if enabled
1400 if (
1401 Number( jetpackCarouselStrings.display_background_image ) === 1 &&
1402 ! slide.backgroundImage
1403 ) {
1404 loadBackgroundImage( slide );
1405 }
1406 }
1407 } );
1408 }
1409
1410 function loadBackgroundImage( slide ) {
1411 var image = slide.attrs.originalElement;
1412
1413 if ( ! image ) {
1414 return;
1415 }
1416
1417 if ( image.complete && image.naturalHeight !== 0 ) {
1418 applyBackgroundImage( slide, image );
1419 return;
1420 }
1421
1422 // The thumbnail in the post may still be loading, or may be lazy-loaded.
1423 // Use an event listener rather than `onload`, which would overwrite any
1424 // handler the page has already attached to its own image. Pair the load
1425 // handler with an error handler so a thumbnail that never loads doesn't
1426 // leave a listener (and its reference to the slide) attached for good.
1427 var onLoad = function () {
1428 image.removeEventListener( 'error', onError );
1429 applyBackgroundImage( slide, image );
1430 };
1431 var onError = function () {
1432 image.removeEventListener( 'load', onLoad );
1433 };
1434 image.addEventListener( 'load', onLoad, { once: true } );
1435 image.addEventListener( 'error', onError, { once: true } );
1436 }
1437
1438 function applyBackgroundImage( slide, image ) {
1439 var url = util.getBackgroundImage( image );
1440
1441 if ( ! url ) {
1442 return;
1443 }
1444
1445 // Always paint onto the slide the image belongs to. Preloading runs this
1446 // for the neighbouring slides too, so painting onto whichever slide happens
1447 // to be active would put the wrong image behind it and leave the slide it
1448 // was meant for with no placeholder at all.
1449 slide.backgroundImage = url;
1450 slide.el.style.backgroundImage = 'url(' + url + ')';
1451 slide.el.style.backgroundSize = 'cover';
1452 slide.el.style.backgroundPosition = 'center';
1453 }
1454
1455 function clearCommentTextAreaValue() {
1456 if ( carousel.commentField ) {
1457 carousel.commentField.value = '';
1458 }
1459 }
1460
1461 function getOriginalDimensions( el ) {
1462 var size = el.getAttribute( 'data-orig-size' ) || '';
1463
1464 if ( size ) {
1465 var parts = size.split( ',' );
1466 return { width: parseInt( parts[ 0 ], 10 ), height: parseInt( parts[ 1 ], 10 ) };
1467 }
1468 return {
1469 width: el.getAttribute( 'data-original-width' ) || el.getAttribute( 'width' ) || undefined,
1470 height:
1471 el.getAttribute( 'data-original-height' ) || el.getAttribute( 'height' ) || undefined,
1472 };
1473 }
1474
1475 function initCarouselSlides( items, startIndex ) {
1476 carousel.slides = [];
1477
1478 var max = calculateMaxSlideDimensions();
1479
1480 // If the startIndex is not 0 then preload the clicked image first.
1481 if ( startIndex !== 0 && items[ startIndex ].getAttribute( 'data-gallery-src' ) !== null ) {
1482 var img = new Image();
1483 img.src = items[ startIndex ].getAttribute( 'data-gallery-src' );
1484 }
1485
1486 // create the 'slide'
1487 Array.prototype.forEach.call( items, function ( item, i ) {
1488 var permalinkEl = domUtil.closest( item, 'a' );
1489 var origFile = item.getAttribute( 'data-orig-file' ) || item.getAttribute( 'src-orig' );
1490 var attrID =
1491 item.getAttribute( 'data-attachment-id' ) || item.getAttribute( 'data-id' ) || '0';
1492 var caption = document.querySelector(
1493 'img[data-attachment-id="' + attrID + '"] + figcaption'
1494 );
1495
1496 if ( caption ) {
1497 caption = caption.innerHTML;
1498 } else {
1499 caption = item.getAttribute( 'data-image-caption' );
1500 }
1501
1502 var attrs = {
1503 originalElement: item,
1504 attachmentId: attrID,
1505 commentsOpened: item.getAttribute( 'data-comments-opened' ) || '0',
1506 imageMeta: domUtil.getJSONAttribute( item, 'data-image-meta' ) || {},
1507 title: item.getAttribute( 'data-image-title' ) || '',
1508 desc: item.getAttribute( 'data-image-description' ) || '',
1509 largeFile: item.getAttribute( 'data-large-file' ) || '',
1510 origFile: origFile || '',
1511 thumbSize: { width: item.naturalWidth, height: item.naturalHeight },
1512 caption: caption || '',
1513 permalink: permalinkEl && permalinkEl.getAttribute( 'href' ),
1514 src: origFile || item.getAttribute( 'src' ) || '',
1515 };
1516
1517 var tiledGalleryItem = domUtil.closest( item, '.tiled-gallery-item' );
1518 var tiledCaptionEl =
1519 tiledGalleryItem && tiledGalleryItem.querySelector( '.tiled-gallery-caption' );
1520 var tiledCaption = tiledCaptionEl && tiledCaptionEl.innerHTML;
1521 if ( tiledCaption ) {
1522 attrs.caption = tiledCaption;
1523 }
1524
1525 var origDimensions = getOriginalDimensions( item );
1526
1527 attrs.origWidth = origDimensions.width || attrs.thumbSize.width;
1528 attrs.origHeight = origDimensions.height || attrs.thumbSize.height;
1529
1530 if ( typeof wpcom !== 'undefined' && wpcom.carousel && wpcom.carousel.generateImgSrc ) {
1531 attrs.src = wpcom.carousel.generateImgSrc( item, max );
1532 } else {
1533 attrs.src = selectBestImageUrl( {
1534 origFile: attrs.src,
1535 origWidth: attrs.origWidth,
1536 origHeight: attrs.origHeight,
1537 maxWidth: max.width,
1538 maxHeight: max.height,
1539 largeFile: attrs.largeFile,
1540 } );
1541 }
1542
1543 // Set the final src.
1544 item.setAttribute( 'data-gallery-src', attrs.src );
1545
1546 if ( attrs.attachmentId !== '0' ) {
1547 attrs.title = util.texturize( attrs.title );
1548 attrs.desc = util.texturize( attrs.desc );
1549 attrs.caption = util.texturize( attrs.caption );
1550
1551 // Initially, the image is a 1x1 transparent gif.
1552 // The preview is shown as a background image on the slide itself.
1553 var image = new Image();
1554
1555 var slideEl = document.createElement( 'div' );
1556 slideEl.classList.add( 'swiper-slide' );
1557 slideEl.setAttribute( 'itemprop', 'associatedMedia' );
1558 slideEl.setAttribute( 'itemscope', '' );
1559 slideEl.setAttribute( 'itemtype', 'https://schema.org/ImageObject' );
1560 var zoomEl = document.createElement( 'div' );
1561 zoomEl.classList.add( 'swiper-zoom-container' );
1562
1563 carousel.gallery.appendChild( slideEl );
1564
1565 slideEl.appendChild( zoomEl );
1566 zoomEl.appendChild( image );
1567 slideEl.setAttribute( 'data-attachment-id', attrs.attachmentId );
1568 slideEl.setAttribute( 'data-permalink', attrs.permalink );
1569 slideEl.setAttribute( 'data-orig-file', attrs.origFile );
1570
1571 // Reuse the thumbnail the browser has already decoded for this image
1572 // in the post. It costs nothing to display and gives the slide
1573 // something to show while the full-size image is downloading.
1574 attrs.previewImage = item.currentSrc || item.getAttribute( 'src' ) || '';
1575
1576 var slide = { el: slideEl, attrs: attrs, index: i };
1577 carousel.slides.push( slide );
1578 }
1579 } );
1580 }
1581
1582 function loadSwiper( gallery, options ) {
1583 if ( ! window.JetpackSwiper ) {
1584 var loader = document.querySelector( '#jp-carousel-loading-overlay' );
1585 domUtil.show( loader );
1586 var jsScript = document.createElement( 'script' );
1587 jsScript.id = 'jetpack-carousel-swiper-js';
1588 jsScript.src = window.jetpackSwiperLibraryPath.url;
1589 jsScript.async = true;
1590 jsScript.onload = function () {
1591 domUtil.hide( loader );
1592 openCarousel( gallery, options );
1593 };
1594 jsScript.onerror = function () {
1595 domUtil.hide( loader );
1596 };
1597 document.head.appendChild( jsScript );
1598 return;
1599 }
1600 openCarousel( gallery, options );
1601 }
1602
1603 function openCarousel( gallery, options ) {
1604 var settings = {
1605 imgSelector:
1606 '.gallery-item [data-attachment-id], .tiled-gallery-item [data-attachment-id], img[data-attachment-id], img[data-id]',
1607 startIndex: 0,
1608 };
1609
1610 var data = domUtil.getJSONAttribute( gallery, 'data-carousel-extra' );
1611 var tapTimeout;
1612
1613 if ( ! data ) {
1614 return; // don't run if the default gallery functions weren't used
1615 }
1616
1617 const images = gallery.querySelectorAll( settings.imgSelector );
1618 if ( ! images.length ) {
1619 return; // don't run if we found no images in the gallery (somehow it has images that aren't in the media library?)
1620 }
1621
1622 initializeCarousel();
1623
1624 if ( carousel.isOpen ) {
1625 return; // don't open if already opened
1626 }
1627 carousel.isOpen = true;
1628
1629 // make sure to stop the page from scrolling behind the carousel overlay, so we don't trigger
1630 // infiniscroll for it when enabled (Reader, theme infiniscroll, etc).
1631 originalOverflow = getComputedStyle( document.body ).overflow;
1632 document.body.style.overflow = 'hidden';
1633 // prevent html from overflowing on some of the new themes.
1634 originalHOverflow = getComputedStyle( document.documentElement ).overflow;
1635 document.documentElement.style.overflow = 'hidden';
1636 scrollPos = window.scrollY || window.pageYOffset || 0;
1637
1638 carousel.container.setAttribute( 'data-carousel-extra', JSON.stringify( data ) );
1639 stat( [ 'open', 'view_image' ] );
1640
1641 // If options exist, lets merge them
1642 // with our default settings
1643 for ( var option in options || {} ) {
1644 settings[ option ] = options[ option ];
1645 }
1646
1647 if ( settings.startIndex === -1 ) {
1648 settings.startIndex = 0; // -1 returned if can't find index, so start from beginning
1649 }
1650
1651 domUtil.emitEvent( carousel.overlay, 'jp_carousel.beforeOpen' );
1652 carousel.gallery.innerHTML = '';
1653
1654 // Need to set the overlay manually to block or swiper does't initialise properly.
1655 carousel.overlay.style.opacity = 1;
1656 carousel.overlay.style.display = 'block';
1657
1658 initCarouselSlides( images, settings.startIndex );
1659
1660 swiper = new window.JetpackSwiper( '.jp-carousel-swiper-container', {
1661 centeredSlides: true,
1662 zoom: true,
1663 loop: carousel.slides.length > 1,
1664 // Turn off interactions and hide navigation arrows if there is only one slide.
1665 enabled: carousel.slides.length > 1,
1666 pagination: {
1667 el: '.jp-swiper-pagination',
1668 clickable: true,
1669 },
1670 navigation: {
1671 nextEl: '.jp-swiper-button-next',
1672 prevEl: '.jp-swiper-button-prev',
1673 },
1674 initialSlide: settings.startIndex,
1675 on: {
1676 init: function () {
1677 selectSlideAtIndex( settings.startIndex );
1678 },
1679 },
1680 preventClicks: false,
1681 preventClicksPropagation: false,
1682 preventInteractionOnTransition: ! domUtil.isTouch(),
1683 threshold: 5,
1684 } );
1685
1686 swiper.on( 'slideChange', function ( swiper ) {
1687 if ( ! carousel.isOpen ) {
1688 return;
1689 }
1690 selectSlideAtIndex( swiper.realIndex );
1691 carousel.overlay.classList.remove( 'jp-carousel-hide-controls' );
1692 } );
1693
1694 swiper.on( 'zoomChange', function ( swiper, scale ) {
1695 if ( scale > 1 ) {
1696 carousel.overlay.classList.add( 'jp-carousel-hide-controls' );
1697 }
1698
1699 if ( scale === 1 ) {
1700 carousel.overlay.classList.remove( 'jp-carousel-hide-controls' );
1701 }
1702 } );
1703
1704 swiper.on( 'doubleTap', function ( swiper ) {
1705 clearTimeout( tapTimeout );
1706 if ( swiper.zoom.scale === 1 ) {
1707 var zoomTimeout = setTimeout( function () {
1708 carousel.overlay.classList.remove( 'jp-carousel-hide-controls' );
1709 clearTimeout( zoomTimeout );
1710 }, 150 );
1711 }
1712 } );
1713
1714 swiper.on( 'tap', function () {
1715 if ( swiper.zoom.scale > 1 ) {
1716 tapTimeout = setTimeout( function () {
1717 carousel.overlay.classList.toggle( 'jp-carousel-hide-controls' );
1718 }, 150 );
1719 }
1720 } );
1721
1722 domUtil.fadeIn( carousel.overlay, function () {
1723 domUtil.emitEvent( carousel.overlay, 'jp_carousel.afterOpen' );
1724 } );
1725 }
1726
1727 // Register the event listeners for starting the gallery
1728 document.body.addEventListener( 'click', handleInteraction );
1729 document.body.addEventListener( 'keydown', handleInteraction );
1730 document.querySelectorAll( galleryItemSelector + 'img' ).forEach( function ( galleryImage ) {
1731 if ( shouldOpenModal( galleryImage ) ) {
1732 makeGalleryImageAccessible( galleryImage );
1733 }
1734 } );
1735
1736 function handleInteraction( e ) {
1737 if ( e.type === 'click' ) {
1738 handleClick( e );
1739 return;
1740 }
1741
1742 if ( e.type === 'keydown' ) {
1743 const parentElement = document.activeElement.parentElement;
1744 const isParentCarouselContainer =
1745 parentElement && parentElement.classList.contains( 'tiled-gallery__item' );
1746
1747 if ( ( e.key === ' ' || e.key === 'Enter' ) && isParentCarouselContainer ) {
1748 handleClick( e );
1749 }
1750 }
1751 }
1752
1753 function normalizeUrl( url ) {
1754 return ( url || '' ).split( '?' )[ 0 ].replace( /\/$/, '' );
1755 }
1756
1757 function shouldOpenModal( el ) {
1758 if ( el.tagName === 'A' ) {
1759 el = el.querySelector( 'img' ) || el;
1760 }
1761
1762 var parent = el.parentElement;
1763 var grandparent = parent ? parent.parentElement : null;
1764
1765 // If Gallery is made up of individual Image blocks check for custom link before
1766 // loading carousel. The custom link may be the parent or could be a descendant
1767 // of the parent if the image has rounded corners.
1768 var parentHref = null;
1769 if ( grandparent && grandparent.classList.contains( 'wp-block-image' ) ) {
1770 parentHref = parent.getAttribute( 'href' );
1771 } else if (
1772 parent &&
1773 parent.classList.contains( 'wp-block-image' ) &&
1774 parent.querySelector( ':scope > a' )
1775 ) {
1776 parentHref = parent.querySelector( ':scope > a' ).getAttribute( 'href' );
1777 }
1778
1779 // If the link does not point to the attachment or media file then assume Image has
1780 // a custom link so don't load the carousel.
1781 if ( parentHref ) {
1782 var cleanHref = normalizeUrl( parentHref );
1783 var cleanOrig = normalizeUrl( el.getAttribute( 'data-orig-file' ) );
1784 var cleanPerm = normalizeUrl( el.getAttribute( 'data-permalink' ) );
1785
1786 if ( cleanHref !== cleanOrig && cleanHref !== cleanPerm ) {
1787 return false;
1788 }
1789 }
1790
1791 // Do not open the modal if we are looking at a gallery caption from before WP5, which may contain a link.
1792 if ( parent && parent.classList.contains( 'gallery-caption' ) ) {
1793 return false;
1794 }
1795
1796 // Do not open the modal if we are looking at a caption of a gallery block, which may contain a link.
1797 if ( domUtil.matches( parent, 'figcaption' ) ) {
1798 return false;
1799 }
1800
1801 return true;
1802 }
1803
1804 function handleClick( e ) {
1805 var isCompatible =
1806 window.CSS && window.CSS.supports && window.CSS.supports( 'display', 'grid' );
1807
1808 // IE11 support is being dropped in August 2021. The new swiper.js libray is not IE11 compat
1809 // so just default to opening individual image attachment/media pages for IE.
1810 if ( ! isCompatible ) {
1811 return;
1812 }
1813
1814 var target = e.target;
1815 var gallery = domUtil.closest( target, gallerySelector );
1816
1817 if ( gallery ) {
1818 if ( ! testForData( gallery ) ) {
1819 return;
1820 }
1821
1822 if ( ! shouldOpenModal( target ) ) {
1823 return;
1824 }
1825
1826 // Set height to auto.
1827 // Fix some themes where closing carousel brings view back to top.
1828 document.documentElement.style.height = 'auto';
1829
1830 e.preventDefault();
1831
1832 // Stopping propagation in case there are parent elements
1833 // with .gallery or .tiled-gallery class
1834 e.stopPropagation();
1835
1836 var item = domUtil.closest( target, itemSelector );
1837 var index = Array.prototype.indexOf.call( gallery.querySelectorAll( itemSelector ), item );
1838 loadSwiper( gallery, { startIndex: index } );
1839 }
1840 }
1841
1842 // Handle lightbox (single image gallery) for images linking to 'Attachment Page'.
1843 if ( Number( jetpackCarouselStrings.single_image_gallery ) === 1 ) {
1844 processSingleImageGallery();
1845 document.body.addEventListener( 'is.post-load', function () {
1846 processSingleImageGallery();
1847 } );
1848 }
1849
1850 // Makes carousel work on page load and when back button leads to same URL with carousel hash
1851 // (i.e. no actual document.ready trigger).
1852 window.addEventListener( 'hashchange', function () {
1853 var hashRegExp = /jp-carousel-(\d+)/;
1854
1855 if ( ! window.location.hash || ! hashRegExp.test( window.location.hash ) ) {
1856 if ( carousel.isOpen ) {
1857 closeCarousel();
1858 }
1859
1860 return;
1861 }
1862
1863 if ( window.location.hash === lastKnownLocationHash && carousel.isOpen ) {
1864 return;
1865 }
1866
1867 if ( window.location.hash && carousel.gallery && ! carousel.isOpen && history.back ) {
1868 history.back();
1869 return;
1870 }
1871
1872 lastKnownLocationHash = window.location.hash;
1873 var matchList = window.location.hash.match( hashRegExp );
1874 var attachmentId = parseInt( matchList[ 1 ], 10 );
1875 var galleries = document.querySelectorAll( gallerySelector );
1876
1877 // Find the first thumbnail that matches the attachment ID in the location
1878 // hash, then open the gallery that contains it.
1879 for ( var i = 0; i < galleries.length; i++ ) {
1880 var gallery = galleries[ i ];
1881 var selected;
1882
1883 var images = gallery.querySelectorAll( 'img' );
1884 for ( var j = 0; j < images.length; j++ ) {
1885 if (
1886 parseInt( images[ j ].getAttribute( 'data-attachment-id' ), 10 ) === attachmentId ||
1887 parseInt( images[ j ].getAttribute( 'data-id' ), 10 ) === attachmentId
1888 ) {
1889 selected = j;
1890 break;
1891 }
1892 }
1893
1894 if ( selected !== undefined ) {
1895 openOrSelectSlide( gallery, selected );
1896 break;
1897 }
1898 }
1899 } );
1900
1901 if ( window.location.hash ) {
1902 domUtil.emitEvent( window, 'hashchange' );
1903 }
1904 }
1905
1906 if ( document.readyState !== 'loading' ) {
1907 init();
1908 } else {
1909 document.addEventListener( 'DOMContentLoaded', init );
1910 }
1911 } )();
1912