PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 16.1-beta.2
Jetpack – WP Security, Backup, Speed, & Growth v16.1-beta.2
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 5 days ago jetpack-carousel.css 5 days ago jetpack-carousel.js 5 days ago jetpack-carousel.php 5 days ago
jetpack-carousel.js
1921 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 /**
112 * CSS-transition fade (compositor). Duration + reduced-motion live in the
113 * `.jp-carousel-fade` rule. A timer drives the finish, not `transitionend` -- that
114 * event is skipped in background tabs / `transition: none` / zero duration, so the
115 * timer is the reliable single source. `callback` fires once, after the fade.
116 */
117 function fade( el, start, end, callback ) {
118 if ( ! el ) {
119 return callback();
120 }
121
122 // A fade already running on this element must not deliver its callback any more.
123 if ( el.jpCarouselCancelFade ) {
124 el.jpCarouselCancelFade();
125 }
126
127 // Set + commit the start state before attaching the transition, or the fade is swallowed.
128 el.classList.remove( 'jp-carousel-fade' );
129 el.style.removeProperty( 'display' );
130 el.style.opacity = start;
131 el.style.pointerEvents = 'none';
132
133 // Commit the starting opacity, otherwise the browser has nothing to animate from.
134 void el.offsetWidth;
135
136 el.classList.add( 'jp-carousel-fade' );
137 el.style.opacity = end;
138
139 // Read the duration back from the stylesheet so the timer always outlives the transition.
140 var duration = parseFloat( getComputedStyle( el ).transitionDuration ) * 1000 || 0;
141
142 var timer = setTimeout( function () {
143 el.jpCarouselCancelFade = null;
144 el.style.removeProperty( 'pointer-events' );
145 callback();
146 }, duration + 50 );
147
148 el.jpCarouselCancelFade = function () {
149 clearTimeout( timer );
150 el.jpCarouselCancelFade = null;
151 };
152 }
153
154 function fadeIn( el, callback ) {
155 callback = callback || util.noop;
156 fade( el, 0, 1, callback );
157 }
158
159 function fadeOut( el, callback ) {
160 callback = callback || util.noop;
161 fade( el, 1, 0, function () {
162 if ( el ) {
163 el.style.display = 'none';
164 }
165 callback();
166 } );
167 }
168
169 function emitEvent( el, type, detail ) {
170 var e;
171 try {
172 e = new CustomEvent( type, {
173 bubbles: true,
174 cancelable: true,
175 detail: detail || null,
176 } );
177 } catch {
178 e = document.createEvent( 'CustomEvent' );
179 e.initCustomEvent( type, true, true, detail || null );
180 }
181 el.dispatchEvent( e );
182 }
183
184 // From: https://easings.net/#easeInOutQuad
185 function easeInOutQuad( num ) {
186 return num < 0.5 ? 2 * num * num : 1 - Math.pow( -2 * num + 2, 2 ) / 2;
187 }
188
189 function getFooterClearance( container ) {
190 var footer = container.querySelector( '.jp-carousel-info-footer' );
191 var infoArea = container.querySelector( '.jp-carousel-info-extra' );
192 var contentArea = container.querySelector( '.jp-carousel-info-content-wrapper' );
193
194 if ( footer && infoArea && contentArea ) {
195 var styles = window.getComputedStyle( infoArea );
196 var padding = parseInt( styles.paddingTop, 10 ) + parseInt( styles.paddingBottom, 10 );
197 padding = isNaN( padding ) ? 0 : padding;
198 return contentArea.offsetHeight + footer.offsetHeight + padding;
199 }
200 return 0;
201 }
202
203 function isTouch() {
204 return (
205 'ontouchstart' in window || ( window.DocumentTouch && document instanceof DocumentTouch )
206 );
207 }
208
209 function scrollToElement( el, container, callback ) {
210 if ( ! el || ! container ) {
211 if ( callback ) {
212 return callback();
213 }
214 return;
215 }
216
217 // For iOS Safari compatibility, use JS to set the minimum height.
218 var infoArea = container.querySelector( '.jp-carousel-info-extra' );
219 if ( infoArea ) {
220 // 64px is the same height as `.jp-carousel-info-footer` in the CSS.
221 infoArea.style.minHeight = window.innerHeight - 64 + 'px';
222 }
223
224 var isScrolling = true;
225 var startTime = Date.now();
226 var duration = 300;
227 var originalPosition = container.scrollTop;
228 var targetPosition = Math.max(
229 0,
230 el.offsetTop - Math.max( 0, window.innerHeight - getFooterClearance( container ) )
231 );
232 var distance = targetPosition - container.scrollTop;
233 distance = Math.min( distance, container.scrollHeight - window.innerHeight );
234
235 function stopScroll() {
236 isScrolling = false;
237 }
238
239 function runScroll() {
240 var now = Date.now();
241 var progress = easeInOutQuad( ( now - startTime ) / duration );
242
243 progress = progress > 1 ? 1 : progress;
244 var newVal = progress * distance;
245 container.scrollTop = originalPosition + newVal;
246
247 if ( now <= startTime + duration && isScrolling ) {
248 return requestAnimationFrame( runScroll );
249 }
250 if ( callback ) {
251 callback();
252 }
253 if ( infoArea ) {
254 infoArea.style.minHeight = '';
255 }
256 isScrolling = false;
257 container.removeEventListener( 'wheel', stopScroll );
258 }
259
260 // Allow scroll to be cancelled by user interaction.
261 container.addEventListener( 'wheel', stopScroll );
262 runScroll();
263 }
264
265 function getJSONAttribute( el, attr ) {
266 if ( ! el || ! el.hasAttribute( attr ) ) {
267 return undefined;
268 }
269
270 try {
271 return JSON.parse( el.getAttribute( attr ) );
272 } catch {
273 return undefined;
274 }
275 }
276
277 function convertToPlainText( html ) {
278 var dummy = document.createElement( 'div' );
279 dummy.textContent = html;
280 return dummy.innerHTML;
281 }
282
283 function stripHTML( text ) {
284 var tmp = document.createElement( 'div' );
285 tmp.innerHTML = text.replace( /<[^>]*>?/gm, '' );
286 return tmp.textContent;
287 }
288
289 return {
290 closest: closest,
291 matches: matches,
292 hide: hide,
293 show: show,
294 fadeIn: fadeIn,
295 fadeOut: fadeOut,
296 scrollToElement: scrollToElement,
297 getJSONAttribute: getJSONAttribute,
298 convertToPlainText: convertToPlainText,
299 stripHTML: stripHTML,
300 emitEvent: emitEvent,
301 isTouch: isTouch,
302 };
303 } )();
304
305 /////////////////////////////////////
306 // Carousel implementation
307 /////////////////////////////////////
308 function init() {
309 var commentInterval;
310 var screenPadding;
311 var originalOverflow;
312 var originalHOverflow;
313 var scrollPos;
314
315 var lastKnownLocationHash = '';
316 var isUserTyping = false;
317
318 var gallerySelector =
319 'div.gallery, div.tiled-gallery, ul.wp-block-gallery, ul.blocks-gallery-grid, ' +
320 'figure.wp-block-gallery.has-nested-images, div.wp-block-jetpack-tiled-gallery, a.single-image-gallery';
321
322 // Selector for items within a gallery or tiled gallery.
323 var galleryItemSelector =
324 '.gallery-item, .tiled-gallery-item, .blocks-gallery-item, ' + ' .tiled-gallery__item';
325
326 // Selector for all items including single images.
327 var itemSelector = galleryItemSelector + ', .wp-block-image';
328
329 var carousel = {};
330
331 var stat =
332 typeof wpcom !== 'undefined' && wpcom.carousel && wpcom.carousel.stat
333 ? wpcom.carousel.stat
334 : util.noop;
335
336 var pageview =
337 typeof wpcom !== 'undefined' && wpcom.carousel && wpcom.carousel.pageview
338 ? wpcom.carousel.pageview
339 : util.noop;
340
341 function handleKeyboardEvent( e ) {
342 if ( ! isUserTyping ) {
343 switch ( e.which ) {
344 case 38: // up
345 e.preventDefault();
346 carousel.overlay.scrollTop -= 100;
347 break;
348 case 40: // down
349 e.preventDefault();
350 carousel.overlay.scrollTop += 100;
351 break;
352 case 39: // right
353 e.preventDefault();
354 swiper.slideNext();
355 break;
356 case 37: // left
357 case 8: // backspace
358 e.preventDefault();
359 swiper.slidePrev();
360 break;
361 case 27: // escape
362 e.preventDefault();
363 closeCarousel();
364 break;
365 default:
366 break;
367 }
368 }
369 }
370
371 function disableKeyboardNavigation() {
372 isUserTyping = true;
373 }
374
375 function enableKeyboardNavigation() {
376 isUserTyping = false;
377 }
378
379 function calculatePadding() {
380 var baseScreenPadding = 110;
381 screenPadding = baseScreenPadding;
382
383 if ( window.innerWidth <= 760 ) {
384 screenPadding = Math.round( ( window.innerWidth / 760 ) * baseScreenPadding );
385
386 if ( screenPadding < 40 && domUtil.isTouch() ) {
387 screenPadding = 0;
388 }
389 }
390 }
391
392 function makeGalleryImageAccessible( img ) {
393 img.role = 'button';
394 img.tabIndex = 0;
395 img.ariaLabel = jetpackCarouselStrings.image_label;
396 }
397
398 function initializeCarousel() {
399 if ( ! carousel.overlay ) {
400 carousel.overlay = document.querySelector( '.jp-carousel-overlay' );
401 carousel.container = carousel.overlay.querySelector( '.jp-carousel-wrap' );
402 carousel.gallery = carousel.container.querySelector( '.jp-carousel' );
403 carousel.info = carousel.overlay.querySelector( '.jp-carousel-info' );
404 carousel.caption = carousel.info.querySelector( '.jp-carousel-caption' );
405 carousel.commentField = carousel.overlay.querySelector(
406 '#jp-carousel-comment-form-comment-field'
407 );
408 carousel.emailField = carousel.overlay.querySelector(
409 '#jp-carousel-comment-form-email-field'
410 );
411 carousel.authorField = carousel.overlay.querySelector(
412 '#jp-carousel-comment-form-author-field'
413 );
414 carousel.urlField = carousel.overlay.querySelector( '#jp-carousel-comment-form-url-field' );
415
416 calculatePadding();
417
418 [
419 carousel.commentField,
420 carousel.emailField,
421 carousel.authorField,
422 carousel.urlField,
423 ].forEach( function ( field ) {
424 if ( field ) {
425 field.addEventListener( 'focus', disableKeyboardNavigation );
426 field.addEventListener( 'blur', enableKeyboardNavigation );
427 }
428 } );
429
430 carousel.overlay.addEventListener( 'click', function ( e ) {
431 var target = e.target;
432 var isTargetCloseHint = !! domUtil.closest( target, '.jp-carousel-close-hint' );
433 var isSmallScreen = !! window.matchMedia( '(max-device-width: 760px)' ).matches;
434 if ( target === carousel.overlay ) {
435 if ( ! isSmallScreen ) {
436 closeCarousel();
437 }
438 } else if ( isTargetCloseHint ) {
439 closeCarousel();
440 } else if ( target.classList.contains( 'jp-carousel-image-download' ) ) {
441 stat( 'download_original_click' );
442 } else if ( target.classList.contains( 'jp-carousel-comment-login' ) ) {
443 handleCommentLoginClick( e );
444 } else if ( domUtil.closest( target, '#jp-carousel-comment-form-container' ) ) {
445 handleCommentFormClick( e );
446 } else if (
447 domUtil.closest( target, '.jp-carousel-photo-icons-container' ) ||
448 target.classList.contains( 'jp-carousel-photo-title' )
449 ) {
450 handleFooterElementClick( e );
451 }
452 } );
453
454 window.addEventListener( 'keydown', handleKeyboardEvent );
455
456 carousel.overlay.addEventListener( 'jp_carousel.afterOpen', function () {
457 enableKeyboardNavigation();
458
459 // Don't show navigation if there's only one image.
460 if ( carousel.slides.length <= 1 ) {
461 return;
462 }
463 // Show dot pagination if slide count is <= 5, otherwise show n/total.
464 if ( carousel.slides.length <= 5 ) {
465 domUtil.show( carousel.info.querySelector( '.jp-swiper-pagination' ) );
466 } else {
467 domUtil.show( carousel.info.querySelector( '.jp-carousel-pagination' ) );
468 }
469 } );
470
471 carousel.overlay.addEventListener( 'jp_carousel.beforeClose', function () {
472 disableKeyboardNavigation();
473
474 // Fixes some themes where closing carousel brings view back to top.
475 document.documentElement.style.removeProperty( 'height' );
476
477 // If we disable the swiper (because there's only one image)
478 // we have to re-enable it here again as Swiper doesn't, for some reason,
479 // show the navigation buttons again after reinitialization.
480 if ( swiper ) {
481 swiper.enable();
482 }
483
484 // Hide pagination.
485 domUtil.hide( carousel.info.querySelector( '.jp-swiper-pagination' ) );
486 domUtil.hide( carousel.info.querySelector( '.jp-carousel-pagination' ) );
487 } );
488
489 carousel.overlay.addEventListener( 'jp_carousel.afterClose', function () {
490 // don't force the browser back when the carousel closes.
491 if ( window.history.pushState ) {
492 history.pushState(
493 '',
494 document.title,
495 window.location.pathname + window.location.search
496 );
497 } else {
498 window.location.href = '';
499 }
500 lastKnownLocationHash = '';
501 carousel.isOpen = false;
502 } );
503
504 // Prevent native browser zooming
505 carousel.overlay.addEventListener( 'touchstart', function ( e ) {
506 if ( e.touches.length > 1 ) {
507 e.preventDefault();
508 }
509 } );
510 }
511 }
512
513 function handleCommentLoginClick() {
514 var slide = carousel.currentSlide;
515 var attachmentId = slide ? slide.attrs.attachmentId : '0';
516
517 window.location.href = jetpackCarouselStrings.login_url + '%23jp-carousel-' + attachmentId;
518 }
519
520 function updatePostResults( msg, isSuccess ) {
521 var results = carousel.overlay.querySelector( '#jp-carousel-comment-post-results' );
522 var elClass = 'jp-carousel-comment-post-' + ( isSuccess ? 'success' : 'error' );
523 results.innerHTML = '<span class="' + elClass + '">' + msg + '</span>';
524 domUtil.hide( carousel.overlay.querySelector( '#jp-carousel-comment-form-spinner' ) );
525 carousel.overlay
526 .querySelector( '#jp-carousel-comment-form' )
527 .classList.remove( 'jp-carousel-is-disabled' );
528 domUtil.show( results );
529 }
530
531 function handleCommentFormClick( e ) {
532 var target = e.target;
533 var data = domUtil.getJSONAttribute( carousel.container, 'data-carousel-extra' ) || {};
534 var attachmentId = carousel.currentSlide.attrs.attachmentId;
535
536 var wrapper = document.querySelector( '#jp-carousel-comment-form-submit-and-info-wrapper' );
537 var spinner = document.querySelector( '#jp-carousel-comment-form-spinner' );
538 // eslint-disable-next-line @wordpress/no-unused-vars-before-return
539 var submit = document.querySelector( '#jp-carousel-comment-form-button-submit' );
540 var form = document.querySelector( '#jp-carousel-comment-form' );
541
542 if (
543 carousel.commentField &&
544 carousel.commentField.getAttribute( 'id' ) === target.getAttribute( 'id' )
545 ) {
546 // For first page load
547 disableKeyboardNavigation();
548 domUtil.show( wrapper );
549 } else if ( domUtil.matches( target, 'input[type="submit"]' ) ) {
550 e.preventDefault();
551 e.stopPropagation();
552
553 domUtil.show( spinner );
554 form.classList.add( 'jp-carousel-is-disabled' );
555
556 var ajaxData = {
557 action: 'post_attachment_comment',
558 nonce: jetpackCarouselStrings.nonce,
559 blog_id: data.blog_id,
560 id: attachmentId,
561 comment: carousel.commentField.value,
562 };
563
564 if ( ! ajaxData.comment.length ) {
565 updatePostResults( jetpackCarouselStrings.no_comment_text, false );
566 return;
567 }
568
569 if ( Number( jetpackCarouselStrings.is_logged_in ) !== 1 ) {
570 ajaxData.email = carousel.emailField.value;
571 ajaxData.author = carousel.authorField.value;
572 ajaxData.url = carousel.urlField.value;
573
574 if ( Number( jetpackCarouselStrings.require_name_email ) === 1 ) {
575 if ( ! ajaxData.email.length || ! ajaxData.email.match( '@' ) ) {
576 updatePostResults( jetpackCarouselStrings.no_comment_email, false );
577 return;
578 } else if ( ! ajaxData.author.length ) {
579 updatePostResults( jetpackCarouselStrings.no_comment_author, false );
580 return;
581 }
582 }
583 }
584
585 var xhr = new XMLHttpRequest();
586 xhr.open( 'POST', jetpackCarouselStrings.ajaxurl, true );
587 xhr.setRequestHeader( 'X-Requested-With', 'XMLHttpRequest' );
588 xhr.setRequestHeader( 'Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8' );
589
590 xhr.onreadystatechange = function () {
591 if (
592 this.readyState === XMLHttpRequest.DONE &&
593 this.status >= 200 &&
594 this.status < 300
595 ) {
596 var response;
597 try {
598 response = JSON.parse( this.response );
599 } catch {
600 updatePostResults( jetpackCarouselStrings.comment_post_error, false );
601 return;
602 }
603 if ( response.comment_status === 'approved' ) {
604 updatePostResults( jetpackCarouselStrings.comment_approved, true );
605 } else if ( response.comment_status === 'unapproved' ) {
606 updatePostResults( jetpackCarouselStrings.comment_unapproved, true );
607 } else {
608 // 'deleted', 'spam', false
609 updatePostResults( jetpackCarouselStrings.comment_post_error, false );
610 }
611 clearCommentTextAreaValue();
612 fetchComments( attachmentId );
613 submit.value = jetpackCarouselStrings.post_comment;
614 domUtil.hide( spinner );
615 form.classList.remove( 'jp-carousel-is-disabled' );
616 } else {
617 // TODO: Add error handling and display here
618 updatePostResults( jetpackCarouselStrings.comment_post_error, false );
619 }
620 };
621
622 var params = [];
623 for ( var item in ajaxData ) {
624 if ( item ) {
625 // Encode each form element into a URI-compatible string.
626 var encoded = encodeURIComponent( item ) + '=' + encodeURIComponent( ajaxData[ item ] );
627 // In x-www-form-urlencoded, spaces should be `+`, not `%20`.
628 params.push( encoded.replace( /%20/g, '+' ) );
629 }
630 }
631 var encodedData = params.join( '&' );
632
633 xhr.send( encodedData );
634 }
635 }
636
637 /**
638 * Handles clicks to icons and other action elements in the icon container.
639 * @param {MouseEvent|TouchEvent|KeyBoardEvent} Event object.
640 */
641 function handleFooterElementClick( e ) {
642 e.preventDefault();
643
644 var target = e.target;
645 var extraInfoContainer = carousel.info.querySelector( '.jp-carousel-info-extra' );
646 var photoMetaContainer = carousel.info.querySelector( '.jp-carousel-image-meta' );
647 var commentsContainer = carousel.info.querySelector( '.jp-carousel-comments-wrapper' );
648 var infoIcon = carousel.info.querySelector( '.jp-carousel-icon-info' );
649 var commentsIcon = carousel.info.querySelector( '.jp-carousel-icon-comments' );
650
651 function handleInfoToggle() {
652 if ( commentsIcon ) {
653 commentsIcon.classList.remove( 'jp-carousel-selected' );
654 }
655 infoIcon.classList.toggle( 'jp-carousel-selected' );
656
657 if ( commentsContainer ) {
658 commentsContainer.classList.remove( 'jp-carousel-show' );
659 }
660 if ( photoMetaContainer ) {
661 photoMetaContainer.classList.toggle( 'jp-carousel-show' );
662 if ( photoMetaContainer.classList.contains( 'jp-carousel-show' ) ) {
663 extraInfoContainer.classList.add( 'jp-carousel-show' );
664 } else {
665 extraInfoContainer.classList.remove( 'jp-carousel-show' );
666 }
667 }
668 }
669
670 function handleCommentToggle() {
671 if ( infoIcon ) {
672 infoIcon.classList.remove( 'jp-carousel-selected' );
673 }
674 commentsIcon.classList.toggle( 'jp-carousel-selected' );
675
676 if ( photoMetaContainer ) {
677 photoMetaContainer.classList.remove( 'jp-carousel-show' );
678 }
679 if ( commentsContainer ) {
680 commentsContainer.classList.toggle( 'jp-carousel-show' );
681 if ( commentsContainer.classList.contains( 'jp-carousel-show' ) ) {
682 extraInfoContainer.classList.add( 'jp-carousel-show' );
683 } else {
684 extraInfoContainer.classList.remove( 'jp-carousel-show' );
685 }
686 }
687 }
688
689 if (
690 domUtil.closest( target, '.jp-carousel-icon-info' ) ||
691 target.classList.contains( 'jp-carousel-photo-title' )
692 ) {
693 if ( photoMetaContainer && photoMetaContainer.classList.contains( 'jp-carousel-show' ) ) {
694 domUtil.scrollToElement( carousel.overlay, carousel.overlay, handleInfoToggle );
695 } else {
696 handleInfoToggle();
697 domUtil.scrollToElement( carousel.info, carousel.overlay );
698 }
699 }
700
701 if ( domUtil.closest( target, '.jp-carousel-icon-comments' ) ) {
702 if ( commentsContainer && commentsContainer.classList.contains( 'jp-carousel-show' ) ) {
703 domUtil.scrollToElement( carousel.overlay, carousel.overlay, handleCommentToggle );
704 } else {
705 handleCommentToggle();
706 domUtil.scrollToElement( carousel.info, carousel.overlay );
707 }
708 }
709 }
710
711 function processSingleImageGallery() {
712 var images = document.querySelectorAll( 'a img[data-attachment-id]' );
713 Array.prototype.forEach.call( images, function ( image ) {
714 var link = image.parentElement;
715 var container = link.parentElement;
716
717 // Skip if image was already added to gallery by shortcode.
718 if ( container.classList.contains( 'gallery-icon' ) ) {
719 return;
720 }
721
722 // Skip if image is part of a gallery.
723 if ( domUtil.closest( container, galleryItemSelector ) ) {
724 return;
725 }
726
727 // Skip if the parent is not actually a link.
728 if ( ! link.hasAttribute( 'href' ) ) {
729 return;
730 }
731
732 var valid = false;
733
734 // If link points to 'Media File' (ignoring GET parameters) and flag is set, allow it.
735 if (
736 link.getAttribute( 'href' ).split( '?' )[ 0 ] ===
737 image.getAttribute( 'data-orig-file' ).split( '?' )[ 0 ] &&
738 Number( jetpackCarouselStrings.single_image_gallery_media_file ) === 1
739 ) {
740 valid = true;
741 }
742
743 // If link points to 'Attachment Page', allow it.
744 if ( link.getAttribute( 'href' ) === image.getAttribute( 'data-permalink' ) ) {
745 valid = true;
746 }
747
748 // Links to 'Custom URL' or 'Media File' when flag is not set are not valid.
749 if ( ! valid ) {
750 return;
751 }
752
753 makeGalleryImageAccessible( image );
754
755 // Make this node a gallery recognizable by event listener above.
756 link.classList.add( 'single-image-gallery' );
757 // blog_id is needed to allow posting comments to correct blog.
758 link.setAttribute(
759 'data-carousel-extra',
760 JSON.stringify( {
761 blog_id: Number( jetpackCarouselStrings.blog_id ),
762 } )
763 );
764 } );
765 }
766
767 function testForData( el ) {
768 return !! ( el && el.getAttribute( 'data-carousel-extra' ) );
769 }
770
771 function openOrSelectSlide( gal, index ) {
772 if ( ! carousel.isOpen ) {
773 // The `open` method selects the correct slide during the initialization.
774 loadSwiper( gal, { startIndex: index } );
775 } else {
776 selectSlideAtIndex( index );
777 // We have to force swiper to slide to the index onHasChange.
778 swiper.slideTo( index + 1 );
779 }
780 }
781
782 function selectSlideAtIndex( index ) {
783 if ( ! index || index < 0 || index > carousel.slides.length ) {
784 index = 0;
785 }
786 carousel.currentSlide = carousel.slides[ index ];
787
788 var current = carousel.currentSlide;
789 var attachmentId = current.attrs.attachmentId;
790
791 // Load current image immediately
792 loadFullImage( carousel.slides[ index ] );
793
794 // Preload adjacent images in background
795 preloadAdjacentImages( index );
796
797 if (
798 Number( jetpackCarouselStrings.display_background_image ) === 1 &&
799 ! carousel.slides[ index ].backgroundImage
800 ) {
801 loadBackgroundImage( carousel.slides[ index ] );
802 }
803
804 domUtil.hide( carousel.caption );
805 updateTitleCaptionAndDesc( {
806 caption: current.attrs.caption,
807 title: current.attrs.title,
808 desc: current.attrs.desc,
809 } );
810
811 var imageMeta = carousel.slides[ index ].attrs.imageMeta;
812 updateExif( imageMeta );
813 updateFullSizeLink( current );
814
815 if ( Number( jetpackCarouselStrings.display_comments ) === 1 ) {
816 testCommentsOpened( carousel.slides[ index ].attrs.commentsOpened );
817 fetchComments( attachmentId );
818 domUtil.hide( carousel.info.querySelector( '#jp-carousel-comment-post-results' ) );
819 }
820
821 // Update pagination in footer.
822 var pagination = carousel.info.querySelector( '.jp-carousel-pagination' );
823 if ( pagination && carousel.slides.length > 5 ) {
824 var currentPage = index + 1;
825 pagination.innerHTML = '<span>' + currentPage + ' / ' + carousel.slides.length + '</span>';
826 }
827
828 // Record pageview in WP Stats, for each new image loaded full-screen.
829 if ( jetpackCarouselStrings.stats && carousel.isOpen ) {
830 new Image().src =
831 document.location.protocol +
832 '//pixel.wp.com/g.gif?' +
833 jetpackCarouselStrings.stats +
834 '&post=' +
835 encodeURIComponent( attachmentId ) +
836 '&rand=' +
837 Math.random();
838 }
839
840 if ( carousel.isOpen ) {
841 pageview( attachmentId );
842 }
843
844 lastKnownLocationHash = '#jp-carousel-' + attachmentId;
845 window.location.hash = lastKnownLocationHash;
846 }
847
848 function restoreScroll() {
849 window.scrollTo( window.scrollX || window.pageXOffset || 0, scrollPos || 0 );
850 }
851
852 function closeCarousel() {
853 // Make sure to let the page scroll again.
854 document.body.style.overflow = originalOverflow;
855 document.documentElement.style.overflow = originalHOverflow;
856 clearCommentTextAreaValue();
857
858 disableKeyboardNavigation();
859
860 domUtil.emitEvent( carousel.overlay, 'jp_carousel.beforeClose' );
861 restoreScroll();
862 carousel.isOpen = false;
863 swiper.destroy();
864 // Clear slide data for DOM garbage collection.
865 carousel.slides = [];
866 carousel.currentSlide = undefined;
867 carousel.gallery.innerHTML = '';
868
869 domUtil.fadeOut( carousel.overlay, function () {
870 domUtil.emitEvent( carousel.overlay, 'jp_carousel.afterClose' );
871 } );
872 }
873
874 function calculateMaxSlideDimensions() {
875 return {
876 width: window.innerWidth,
877 height: window.innerHeight - 64, //subtract height of bottom info bar,
878 };
879 }
880
881 function sanitizePhotonUrl( url ) {
882 var urlObj;
883 try {
884 urlObj = new URL( url );
885 // eslint-disable-next-line no-unused-vars
886 } catch ( e ) {
887 return url;
888 }
889
890 var whitelistedPhotonArgs = [
891 'quality',
892 'ssl',
893 'filter',
894 'brightness',
895 'contrast',
896 'colorize',
897 'smooth',
898 ];
899
900 // Get all search params
901 var searchParams = Array.from( urlObj.searchParams.entries() );
902
903 // Clear all existing params
904 urlObj.search = '';
905
906 // Only add back whitelisted params
907 searchParams.forEach( ( [ key, value ] ) => {
908 if ( whitelistedPhotonArgs.includes( key ) ) {
909 urlObj.searchParams.append( key, value );
910 }
911 } );
912
913 return urlObj;
914 }
915
916 function selectBestImageUrl( args ) {
917 if ( typeof args !== 'object' ) {
918 args = {};
919 }
920
921 if ( typeof args.origFile === 'undefined' ) {
922 return '';
923 }
924
925 if ( typeof args.origWidth === 'undefined' || typeof args.maxWidth === 'undefined' ) {
926 return args.origFile;
927 }
928
929 // When there's no large file to fall back on (e.g. images that weren't enriched with
930 // Jetpack's data-large-file attribute), use the original file. A missing attribute is
931 // read as an empty string, so we can't only guard against `undefined` here: otherwise a
932 // narrow (portrait, mobile) viewport would return that empty string as the image source,
933 // leaving the carousel with a blank slide.
934 if ( ! args.largeFile ) {
935 return args.origFile;
936 }
937
938 // Check if the image is being served by Photon (using a regular expression on the hostname).
939
940 var imageLinkParser = document.createElement( 'a' );
941 imageLinkParser.href = args.largeFile;
942
943 var isPhotonUrl = /^i[0-2]\.wp\.com$/i.test( imageLinkParser.hostname );
944
945 var largeSizeParts = getImageSizeParts( args.largeFile, args.origWidth, isPhotonUrl );
946
947 var largeWidth = parseInt( largeSizeParts[ 0 ], 10 );
948 var largeHeight = parseInt( largeSizeParts[ 1 ], 10 );
949
950 args.origMaxWidth = args.maxWidth;
951 args.origMaxHeight = args.maxHeight;
952
953 // Give devices with a higher devicePixelRatio higher-res images (Retina display = 2, Android phones = 1.5, etc)
954 if ( typeof window.devicePixelRatio !== 'undefined' && window.devicePixelRatio > 1 ) {
955 args.maxWidth = args.maxWidth * window.devicePixelRatio;
956 args.maxHeight = args.maxHeight * window.devicePixelRatio;
957 }
958
959 if ( largeWidth >= args.maxWidth || largeHeight >= args.maxHeight ) {
960 return args.largeFile;
961 }
962
963 if ( isPhotonUrl ) {
964 // args.origFile doesn't point to a Photon url, so in this case we use args.largeFile
965 // to return the photon url of the original image.
966 if ( args.largeFile.lastIndexOf( '?' ) === -1 ) {
967 return args.largeFile;
968 }
969
970 // Sanitize the URL to remove non-cosmetic changes like resize, fit, etc.
971 var sanitizedUrl = sanitizePhotonUrl( args.largeFile );
972
973 // If we have a really large image load a smaller version
974 // that is closer to the viewable size
975 if ( args.origWidth > args.maxWidth || args.origHeight > args.maxHeight ) {
976 // @2x the max sizes so we get a high enough resolution for zooming.
977 args.origMaxWidth = args.maxWidth * 2;
978 args.origMaxHeight = args.maxHeight * 2;
979 // Add the fit arg to the list of Photon args.
980 sanitizedUrl.searchParams.set( 'fit', args.origMaxWidth + ',' + args.origMaxHeight );
981 }
982
983 // Return a Photon URL image that's better fitted for the viewport.
984 return sanitizedUrl.toString();
985 }
986
987 return args.origFile;
988 }
989
990 function getImageSizeParts( file, origWidth, isPhotonUrl ) {
991 var size = isPhotonUrl
992 ? file.replace( /.*=([\d]+%2C[\d]+).*$/, '$1' )
993 : file.replace( /.*-([\d]+x[\d]+)\..+$/, '$1' );
994
995 var sizeParts;
996 if ( size !== file ) {
997 sizeParts = isPhotonUrl ? size.split( '%2C' ) : size.split( 'x' );
998 } else {
999 sizeParts = [ origWidth, 0 ];
1000 }
1001
1002 // If one of the dimensions is set to 9999, then the actual value of that dimension can't be retrieved from the url.
1003 // In that case, we set the value to 0.
1004 if ( sizeParts[ 0 ] === '9999' ) {
1005 sizeParts[ 0 ] = '0';
1006 }
1007
1008 if ( sizeParts[ 1 ] === '9999' ) {
1009 sizeParts[ 1 ] = '0';
1010 }
1011
1012 return sizeParts;
1013 }
1014
1015 /**
1016 * Returns a number in a fraction format that represents the shutter speed.
1017 * @param Number speed
1018 * @return String
1019 */
1020 function formatShutterSpeed( speed ) {
1021 var denominator;
1022
1023 // round to one decimal if value > 1s by multiplying it by 10, rounding, then dividing by 10 again
1024 if ( speed >= 1 ) {
1025 return Math.round( speed * 10 ) / 10 + 's';
1026 }
1027
1028 // If the speed is less than one, we find the denominator by inverting
1029 // the number. Since cameras usually use rational numbers as shutter
1030 // speeds, we should get a nice round number. Or close to one in cases
1031 // like 1/30. So we round it.
1032 denominator = Math.round( 1 / speed );
1033
1034 return '1/' + denominator + 's';
1035 }
1036
1037 function parseTitleOrDesc( value ) {
1038 if ( ! value.match( ' ' ) && value.match( '_' ) ) {
1039 return '';
1040 }
1041
1042 return value;
1043 }
1044
1045 function updateTitleCaptionAndDesc( data ) {
1046 var caption;
1047 var title;
1048 var desc;
1049 var captionMainElement;
1050 var captionInfoExtraElement;
1051 var titleElement;
1052 var descriptionElement;
1053
1054 captionMainElement = carousel.overlay.querySelector( '.jp-carousel-photo-caption' );
1055 captionInfoExtraElement = carousel.overlay.querySelector( '.jp-carousel-caption' );
1056
1057 titleElement = carousel.overlay.querySelector( '.jp-carousel-photo-title' );
1058 descriptionElement = carousel.overlay.querySelector( '.jp-carousel-photo-description' );
1059
1060 domUtil.hide( captionMainElement );
1061 domUtil.hide( captionInfoExtraElement );
1062 domUtil.hide( titleElement );
1063 domUtil.hide( descriptionElement );
1064
1065 caption = parseTitleOrDesc( data.caption ) || '';
1066 title = parseTitleOrDesc( data.title ) || '';
1067 desc = parseTitleOrDesc( data.desc ) || '';
1068
1069 if ( caption || title || desc ) {
1070 if ( caption ) {
1071 captionMainElement.innerHTML = caption;
1072 captionInfoExtraElement.innerHTML = caption;
1073
1074 domUtil.show( captionMainElement );
1075 domUtil.show( captionInfoExtraElement );
1076 }
1077
1078 if ( domUtil.stripHTML( caption ) === domUtil.stripHTML( title ) ) {
1079 title = '';
1080 }
1081
1082 if ( domUtil.stripHTML( caption ) === domUtil.stripHTML( desc ) ) {
1083 desc = '';
1084 }
1085
1086 if ( domUtil.stripHTML( title ) === domUtil.stripHTML( desc ) ) {
1087 desc = '';
1088 }
1089
1090 if ( desc ) {
1091 descriptionElement.innerHTML = desc;
1092 domUtil.show( descriptionElement );
1093
1094 if ( ! title && ! caption ) {
1095 captionMainElement.textContent = domUtil.stripHTML( desc );
1096 domUtil.show( captionMainElement );
1097 }
1098 }
1099
1100 if ( title ) {
1101 var plainTitle = domUtil.stripHTML( title );
1102 titleElement.textContent = plainTitle;
1103
1104 if ( ! caption ) {
1105 captionMainElement.textContent = plainTitle;
1106 captionInfoExtraElement.textContent = plainTitle;
1107
1108 domUtil.show( captionMainElement );
1109 }
1110
1111 domUtil.show( titleElement );
1112 }
1113 }
1114 }
1115
1116 // updateExif updates the contents of the exif UL (.jp-carousel-image-exif)
1117 function updateExif( meta ) {
1118 if ( ! meta || Number( jetpackCarouselStrings.display_exif ) !== 1 ) {
1119 return false;
1120 }
1121
1122 // Locate the parent container for the metadata.
1123 var metaContainer = carousel.info.querySelector( '.jp-carousel-image-meta' );
1124 if ( ! metaContainer ) {
1125 return false;
1126 }
1127
1128 var html = '';
1129
1130 for ( var key in meta ) {
1131 var val = meta[ key ];
1132 var metaKeys = jetpackCarouselStrings.meta_data || [];
1133
1134 if ( parseFloat( val ) === 0 || ! val.length || metaKeys.indexOf( key ) === -1 ) {
1135 continue;
1136 }
1137
1138 switch ( key ) {
1139 case 'focal_length':
1140 val = val + 'mm';
1141 break;
1142 case 'shutter_speed':
1143 val = formatShutterSpeed( val );
1144 break;
1145 case 'aperture':
1146 val = 'f/' + val;
1147 break;
1148 }
1149
1150 html += '<li><h5>' + jetpackCarouselStrings[ key ] + '</h5>' + val + '</li>';
1151 }
1152
1153 // Handle the UL element dynamically to ensure valid markup.
1154 var ul = metaContainer.querySelector( 'ul.jp-carousel-image-exif' );
1155
1156 if ( html !== '' ) {
1157 // If there is data to display and the UL doesn't exist, create it.
1158 if ( ! ul ) {
1159 ul = document.createElement( 'ul' );
1160 ul.className = 'jp-carousel-image-exif';
1161
1162 // Insert right after the title/caption container if it exists, otherwise prepend
1163 var titleAndCaption = metaContainer.querySelector( '.jp-carousel-title-and-caption' );
1164 if ( titleAndCaption && titleAndCaption.nextSibling ) {
1165 metaContainer.insertBefore( ul, titleAndCaption.nextSibling );
1166 } else {
1167 metaContainer.insertBefore( ul, metaContainer.firstChild );
1168 }
1169 }
1170 ul.innerHTML = html;
1171 ul.style.removeProperty( 'display' );
1172 } else if ( ul ) {
1173 // If the data is empty but the UL exists in the DOM, remove it.
1174 ul.parentNode.removeChild( ul );
1175 }
1176 }
1177
1178 // Update the contents of the jp-carousel-image-download link
1179 function updateFullSizeLink( currentSlide ) {
1180 if ( ! currentSlide ) {
1181 return false;
1182 }
1183 var original;
1184 var origSize = [ currentSlide.attrs.origWidth, currentSlide.attrs.origHeight ];
1185 var imageLinkParser = document.createElement( 'a' );
1186
1187 imageLinkParser.href = currentSlide.attrs.src.replace( /\?.+$/, '' );
1188
1189 // Is this a Photon URL?
1190 if ( imageLinkParser.hostname.match( /^i[\d]{1}\.wp\.com$/i ) !== null ) {
1191 original = imageLinkParser.href;
1192 } else {
1193 original = currentSlide.attrs.origFile.replace( /\?.+$/, '' );
1194 }
1195
1196 var downloadText = carousel.info.querySelector( '.jp-carousel-download-text' );
1197 var permalink = carousel.info.querySelector( '.jp-carousel-image-download' );
1198
1199 downloadText.innerHTML = util.applyReplacements(
1200 jetpackCarouselStrings.download_original,
1201 origSize
1202 );
1203 permalink.setAttribute( 'href', original );
1204 permalink.style.removeProperty( 'display' );
1205 }
1206
1207 function testCommentsOpened( opened ) {
1208 var commentForm = carousel.info.querySelector( '#jp-carousel-comment-form-container' );
1209 var isOpened = parseInt( opened, 10 ) === 1;
1210
1211 if ( isOpened ) {
1212 domUtil.fadeIn( commentForm );
1213 } else {
1214 domUtil.fadeOut( commentForm );
1215 }
1216 }
1217
1218 function fetchComments( attachmentId, offset ) {
1219 var shouldClear = offset === undefined;
1220 var commentsIndicator = carousel.info.querySelector(
1221 '.jp-carousel-icon-comments .jp-carousel-has-comments-indicator'
1222 );
1223
1224 commentsIndicator.classList.remove( 'jp-carousel-show' );
1225
1226 clearInterval( commentInterval );
1227
1228 if ( ! attachmentId ) {
1229 return;
1230 }
1231
1232 if ( ! offset || offset < 1 ) {
1233 offset = 0;
1234 }
1235
1236 var comments = carousel.info.querySelector( '.jp-carousel-comments' );
1237 var commentsLoading = carousel.info.querySelector( '#jp-carousel-comments-loading' );
1238 domUtil.show( commentsLoading );
1239
1240 if ( shouldClear ) {
1241 domUtil.hide( comments );
1242 comments.innerHTML = '';
1243 }
1244
1245 var xhr = new XMLHttpRequest();
1246 var url =
1247 jetpackCarouselStrings.ajaxurl +
1248 '?action=get_attachment_comments' +
1249 '&nonce=' +
1250 jetpackCarouselStrings.nonce +
1251 '&id=' +
1252 attachmentId +
1253 '&offset=' +
1254 offset;
1255 xhr.open( 'GET', url );
1256 xhr.setRequestHeader( 'X-Requested-With', 'XMLHttpRequest' );
1257
1258 var onError = function () {
1259 domUtil.fadeIn( comments );
1260 domUtil.fadeOut( commentsLoading );
1261 };
1262
1263 xhr.onload = function () {
1264 // Ignore the results if they arrive late and we're now on a different slide.
1265 if (
1266 ! carousel.currentSlide ||
1267 carousel.currentSlide.attrs.attachmentId !== attachmentId
1268 ) {
1269 return;
1270 }
1271
1272 var isSuccess = xhr.status >= 200 && xhr.status < 300;
1273 var data;
1274 try {
1275 data = JSON.parse( xhr.responseText );
1276 } catch {
1277 // Do nothing.
1278 }
1279
1280 if ( ! isSuccess || ! data || ! Array.isArray( data ) ) {
1281 return onError();
1282 }
1283
1284 if ( shouldClear ) {
1285 comments.innerHTML = '';
1286 }
1287
1288 for ( var i = 0; i < data.length; i++ ) {
1289 var entry = data[ i ];
1290 var comment = document.createElement( 'div' );
1291 comment.classList.add( 'jp-carousel-comment' );
1292 comment.setAttribute( 'id', 'jp-carousel-comment-' + entry.id );
1293 comment.innerHTML =
1294 '<div class="comment-gravatar">' +
1295 entry.gravatar_markup +
1296 '</div>' +
1297 '<div class="comment-content">' +
1298 '<div class="comment-author">' +
1299 entry.author_markup +
1300 '</div>' +
1301 '<div class="comment-date">' +
1302 entry.date_gmt +
1303 '</div>' +
1304 entry.content +
1305 '</div>';
1306 comments.appendChild( comment );
1307
1308 // Set the interval to check for a new page of comments.
1309 clearInterval( commentInterval );
1310 commentInterval = setInterval( function () {
1311 if ( carousel.container.scrollTop + 150 > window.innerHeight ) {
1312 fetchComments( attachmentId, offset + 10 );
1313 clearInterval( commentInterval );
1314 }
1315 }, 300 );
1316 }
1317
1318 if ( data.length > 0 ) {
1319 domUtil.show( comments );
1320 commentsIndicator.innerText = data.length;
1321 commentsIndicator.classList.add( 'jp-carousel-show' );
1322 }
1323
1324 domUtil.hide( commentsLoading );
1325 };
1326
1327 xhr.onerror = onError;
1328
1329 xhr.send();
1330 }
1331
1332 function loadFullImage( slide ) {
1333 var attrs = slide.attrs;
1334 var image = slide.el.querySelector( 'img' );
1335
1336 if ( image.hasAttribute( 'data-loaded' ) ) {
1337 return;
1338 }
1339
1340 image.setAttribute( 'itemprop', 'image' );
1341 image.setAttribute( 'data-loaded', 1 );
1342
1343 var hasPreview = attrs.previewImage && attrs.previewImage !== attrs.src;
1344
1345 if ( ! hasPreview ) {
1346 // No usable in-page thumbnail (e.g. a lazy-loading plugin swapped the
1347 // gallery src for a placeholder). Load the full-size image straight
1348 // into the visible element so the slide is never left without a src.
1349 image.src = attrs.src;
1350 return;
1351 }
1352
1353 // Show the thumbnail the browser has already decoded for this image in the
1354 // post itself. Without it the slide stays empty until the full-size image
1355 // arrives, which reads as a black screen whenever the reader moves through
1356 // the gallery faster than the images can download.
1357 image.src = attrs.previewImage;
1358 // The thumbnail is much smaller than the slide, so soften the upscale
1359 // until the full-size image replaces it.
1360 image.style.filter = 'blur(8px)';
1361
1362 // Load the full-size image off-DOM, then swap it in over the preview. On
1363 // error the (blurred) preview stays put rather than reverting to blank.
1364 var fullImage = new window.Image();
1365
1366 fullImage.addEventListener(
1367 'load',
1368 function () {
1369 // Cached by this point, so swapping it in is effectively instant.
1370 image.src = attrs.src;
1371 image.style.filter = '';
1372 },
1373 { once: true }
1374 );
1375
1376 fullImage.addEventListener(
1377 'error',
1378 function () {
1379 image.style.filter = '';
1380 },
1381 { once: true }
1382 );
1383
1384 fullImage.src = attrs.src;
1385 }
1386
1387 function preloadAdjacentImages( currentIndex ) {
1388 var indicesToPreload = [];
1389 var totalSlides = carousel.slides.length;
1390
1391 // Only preload adjacent images if we have more than one slide (matching loop condition)
1392 if ( totalSlides > 1 ) {
1393 // Previous image (with loop handling)
1394 var prevIndex = currentIndex > 0 ? currentIndex - 1 : totalSlides - 1;
1395 indicesToPreload.push( prevIndex );
1396
1397 // Next image (with loop handling)
1398 var nextIndex = currentIndex < totalSlides - 1 ? currentIndex + 1 : 0;
1399 indicesToPreload.push( nextIndex );
1400 }
1401
1402 indicesToPreload.forEach( function ( index ) {
1403 var slide = carousel.slides[ index ];
1404 if ( slide ) {
1405 // Load in background without showing
1406 loadFullImage( slide );
1407
1408 // Also load background image if enabled
1409 if (
1410 Number( jetpackCarouselStrings.display_background_image ) === 1 &&
1411 ! slide.backgroundImage
1412 ) {
1413 loadBackgroundImage( slide );
1414 }
1415 }
1416 } );
1417 }
1418
1419 function loadBackgroundImage( slide ) {
1420 var image = slide.attrs.originalElement;
1421
1422 if ( ! image ) {
1423 return;
1424 }
1425
1426 if ( image.complete && image.naturalHeight !== 0 ) {
1427 applyBackgroundImage( slide, image );
1428 return;
1429 }
1430
1431 // The thumbnail in the post may still be loading, or may be lazy-loaded.
1432 // Use an event listener rather than `onload`, which would overwrite any
1433 // handler the page has already attached to its own image. Pair the load
1434 // handler with an error handler so a thumbnail that never loads doesn't
1435 // leave a listener (and its reference to the slide) attached for good.
1436 var onLoad = function () {
1437 image.removeEventListener( 'error', onError );
1438 applyBackgroundImage( slide, image );
1439 };
1440 var onError = function () {
1441 image.removeEventListener( 'load', onLoad );
1442 };
1443 image.addEventListener( 'load', onLoad, { once: true } );
1444 image.addEventListener( 'error', onError, { once: true } );
1445 }
1446
1447 function applyBackgroundImage( slide, image ) {
1448 var url = util.getBackgroundImage( image );
1449
1450 if ( ! url ) {
1451 return;
1452 }
1453
1454 // Always paint onto the slide the image belongs to. Preloading runs this
1455 // for the neighbouring slides too, so painting onto whichever slide happens
1456 // to be active would put the wrong image behind it and leave the slide it
1457 // was meant for with no placeholder at all.
1458 slide.backgroundImage = url;
1459 slide.el.style.backgroundImage = 'url(' + url + ')';
1460 slide.el.style.backgroundSize = 'cover';
1461 slide.el.style.backgroundPosition = 'center';
1462 }
1463
1464 function clearCommentTextAreaValue() {
1465 if ( carousel.commentField ) {
1466 carousel.commentField.value = '';
1467 }
1468 }
1469
1470 function getOriginalDimensions( el ) {
1471 var size = el.getAttribute( 'data-orig-size' ) || '';
1472
1473 if ( size ) {
1474 var parts = size.split( ',' );
1475 return { width: parseInt( parts[ 0 ], 10 ), height: parseInt( parts[ 1 ], 10 ) };
1476 }
1477 return {
1478 width: el.getAttribute( 'data-original-width' ) || el.getAttribute( 'width' ) || undefined,
1479 height:
1480 el.getAttribute( 'data-original-height' ) || el.getAttribute( 'height' ) || undefined,
1481 };
1482 }
1483
1484 function initCarouselSlides( items, startIndex ) {
1485 carousel.slides = [];
1486
1487 var max = calculateMaxSlideDimensions();
1488
1489 // If the startIndex is not 0 then preload the clicked image first.
1490 if ( startIndex !== 0 && items[ startIndex ].getAttribute( 'data-gallery-src' ) !== null ) {
1491 var img = new Image();
1492 img.src = items[ startIndex ].getAttribute( 'data-gallery-src' );
1493 }
1494
1495 // create the 'slide'
1496 Array.prototype.forEach.call( items, function ( item, i ) {
1497 var permalinkEl = domUtil.closest( item, 'a' );
1498 var origFile = item.getAttribute( 'data-orig-file' ) || item.getAttribute( 'src-orig' );
1499 var attrID =
1500 item.getAttribute( 'data-attachment-id' ) || item.getAttribute( 'data-id' ) || '0';
1501 var caption = document.querySelector(
1502 'img[data-attachment-id="' + attrID + '"] + figcaption'
1503 );
1504
1505 if ( caption ) {
1506 caption = caption.innerHTML;
1507 } else {
1508 caption = item.getAttribute( 'data-image-caption' );
1509 }
1510
1511 var attrs = {
1512 originalElement: item,
1513 attachmentId: attrID,
1514 commentsOpened: item.getAttribute( 'data-comments-opened' ) || '0',
1515 imageMeta: domUtil.getJSONAttribute( item, 'data-image-meta' ) || {},
1516 title: item.getAttribute( 'data-image-title' ) || '',
1517 desc: item.getAttribute( 'data-image-description' ) || '',
1518 largeFile: item.getAttribute( 'data-large-file' ) || '',
1519 origFile: origFile || '',
1520 thumbSize: { width: item.naturalWidth, height: item.naturalHeight },
1521 caption: caption || '',
1522 permalink: permalinkEl && permalinkEl.getAttribute( 'href' ),
1523 src: origFile || item.getAttribute( 'src' ) || '',
1524 };
1525
1526 var tiledGalleryItem = domUtil.closest( item, '.tiled-gallery-item' );
1527 var tiledCaptionEl =
1528 tiledGalleryItem && tiledGalleryItem.querySelector( '.tiled-gallery-caption' );
1529 var tiledCaption = tiledCaptionEl && tiledCaptionEl.innerHTML;
1530 if ( tiledCaption ) {
1531 attrs.caption = tiledCaption;
1532 }
1533
1534 var origDimensions = getOriginalDimensions( item );
1535
1536 attrs.origWidth = origDimensions.width || attrs.thumbSize.width;
1537 attrs.origHeight = origDimensions.height || attrs.thumbSize.height;
1538
1539 if ( typeof wpcom !== 'undefined' && wpcom.carousel && wpcom.carousel.generateImgSrc ) {
1540 attrs.src = wpcom.carousel.generateImgSrc( item, max );
1541 } else {
1542 attrs.src = selectBestImageUrl( {
1543 origFile: attrs.src,
1544 origWidth: attrs.origWidth,
1545 origHeight: attrs.origHeight,
1546 maxWidth: max.width,
1547 maxHeight: max.height,
1548 largeFile: attrs.largeFile,
1549 } );
1550 }
1551
1552 // Set the final src.
1553 item.setAttribute( 'data-gallery-src', attrs.src );
1554
1555 if ( attrs.attachmentId !== '0' ) {
1556 attrs.title = util.texturize( attrs.title );
1557 attrs.desc = util.texturize( attrs.desc );
1558 attrs.caption = util.texturize( attrs.caption );
1559
1560 // Initially, the image is a 1x1 transparent gif.
1561 // The preview is shown as a background image on the slide itself.
1562 var image = new Image();
1563
1564 var slideEl = document.createElement( 'div' );
1565 slideEl.classList.add( 'swiper-slide' );
1566 slideEl.setAttribute( 'itemprop', 'associatedMedia' );
1567 slideEl.setAttribute( 'itemscope', '' );
1568 slideEl.setAttribute( 'itemtype', 'https://schema.org/ImageObject' );
1569 var zoomEl = document.createElement( 'div' );
1570 zoomEl.classList.add( 'swiper-zoom-container' );
1571
1572 carousel.gallery.appendChild( slideEl );
1573
1574 slideEl.appendChild( zoomEl );
1575 zoomEl.appendChild( image );
1576 slideEl.setAttribute( 'data-attachment-id', attrs.attachmentId );
1577 slideEl.setAttribute( 'data-permalink', attrs.permalink );
1578 slideEl.setAttribute( 'data-orig-file', attrs.origFile );
1579
1580 // Reuse the thumbnail the browser has already decoded for this image
1581 // in the post. It costs nothing to display and gives the slide
1582 // something to show while the full-size image is downloading.
1583 attrs.previewImage = item.currentSrc || item.getAttribute( 'src' ) || '';
1584
1585 var slide = { el: slideEl, attrs: attrs, index: i };
1586 carousel.slides.push( slide );
1587 }
1588 } );
1589 }
1590
1591 function loadSwiper( gallery, options ) {
1592 if ( ! window.JetpackSwiper ) {
1593 var loader = document.querySelector( '#jp-carousel-loading-overlay' );
1594 domUtil.show( loader );
1595 var jsScript = document.createElement( 'script' );
1596 jsScript.id = 'jetpack-carousel-swiper-js';
1597 jsScript.src = window.jetpackSwiperLibraryPath.url;
1598 jsScript.async = true;
1599 jsScript.onload = function () {
1600 domUtil.hide( loader );
1601 openCarousel( gallery, options );
1602 };
1603 jsScript.onerror = function () {
1604 domUtil.hide( loader );
1605 };
1606 document.head.appendChild( jsScript );
1607 return;
1608 }
1609 openCarousel( gallery, options );
1610 }
1611
1612 function openCarousel( gallery, options ) {
1613 var settings = {
1614 imgSelector:
1615 '.gallery-item [data-attachment-id], .tiled-gallery-item [data-attachment-id], img[data-attachment-id], img[data-id]',
1616 startIndex: 0,
1617 };
1618
1619 var data = domUtil.getJSONAttribute( gallery, 'data-carousel-extra' );
1620 var tapTimeout;
1621
1622 if ( ! data ) {
1623 return; // don't run if the default gallery functions weren't used
1624 }
1625
1626 const images = gallery.querySelectorAll( settings.imgSelector );
1627 if ( ! images.length ) {
1628 return; // don't run if we found no images in the gallery (somehow it has images that aren't in the media library?)
1629 }
1630
1631 initializeCarousel();
1632
1633 if ( carousel.isOpen ) {
1634 return; // don't open if already opened
1635 }
1636 carousel.isOpen = true;
1637
1638 // make sure to stop the page from scrolling behind the carousel overlay, so we don't trigger
1639 // infiniscroll for it when enabled (Reader, theme infiniscroll, etc).
1640 originalOverflow = getComputedStyle( document.body ).overflow;
1641 document.body.style.overflow = 'hidden';
1642 // prevent html from overflowing on some of the new themes.
1643 originalHOverflow = getComputedStyle( document.documentElement ).overflow;
1644 document.documentElement.style.overflow = 'hidden';
1645 scrollPos = window.scrollY || window.pageYOffset || 0;
1646
1647 carousel.container.setAttribute( 'data-carousel-extra', JSON.stringify( data ) );
1648 stat( [ 'open', 'view_image' ] );
1649
1650 // If options exist, lets merge them
1651 // with our default settings
1652 for ( var option in options || {} ) {
1653 settings[ option ] = options[ option ];
1654 }
1655
1656 if ( settings.startIndex === -1 ) {
1657 settings.startIndex = 0; // -1 returned if can't find index, so start from beginning
1658 }
1659
1660 domUtil.emitEvent( carousel.overlay, 'jp_carousel.beforeOpen' );
1661 carousel.gallery.innerHTML = '';
1662
1663 // Need to set the overlay manually to block or swiper does't initialise properly.
1664 carousel.overlay.style.opacity = 1;
1665 carousel.overlay.style.display = 'block';
1666
1667 initCarouselSlides( images, settings.startIndex );
1668
1669 swiper = new window.JetpackSwiper( '.jp-carousel-swiper-container', {
1670 centeredSlides: true,
1671 zoom: true,
1672 loop: carousel.slides.length > 1,
1673 // Turn off interactions and hide navigation arrows if there is only one slide.
1674 enabled: carousel.slides.length > 1,
1675 pagination: {
1676 el: '.jp-swiper-pagination',
1677 clickable: true,
1678 },
1679 navigation: {
1680 nextEl: '.jp-swiper-button-next',
1681 prevEl: '.jp-swiper-button-prev',
1682 },
1683 initialSlide: settings.startIndex,
1684 on: {
1685 init: function () {
1686 selectSlideAtIndex( settings.startIndex );
1687 },
1688 },
1689 preventClicks: false,
1690 preventClicksPropagation: false,
1691 preventInteractionOnTransition: ! domUtil.isTouch(),
1692 threshold: 5,
1693 } );
1694
1695 swiper.on( 'slideChange', function ( swiper ) {
1696 if ( ! carousel.isOpen ) {
1697 return;
1698 }
1699 selectSlideAtIndex( swiper.realIndex );
1700 carousel.overlay.classList.remove( 'jp-carousel-hide-controls' );
1701 } );
1702
1703 swiper.on( 'zoomChange', function ( swiper, scale ) {
1704 if ( scale > 1 ) {
1705 carousel.overlay.classList.add( 'jp-carousel-hide-controls' );
1706 }
1707
1708 if ( scale === 1 ) {
1709 carousel.overlay.classList.remove( 'jp-carousel-hide-controls' );
1710 }
1711 } );
1712
1713 swiper.on( 'doubleTap', function ( swiper ) {
1714 clearTimeout( tapTimeout );
1715 if ( swiper.zoom.scale === 1 ) {
1716 var zoomTimeout = setTimeout( function () {
1717 carousel.overlay.classList.remove( 'jp-carousel-hide-controls' );
1718 clearTimeout( zoomTimeout );
1719 }, 150 );
1720 }
1721 } );
1722
1723 swiper.on( 'tap', function () {
1724 if ( swiper.zoom.scale > 1 ) {
1725 tapTimeout = setTimeout( function () {
1726 carousel.overlay.classList.toggle( 'jp-carousel-hide-controls' );
1727 }, 150 );
1728 }
1729 } );
1730
1731 domUtil.fadeIn( carousel.overlay, function () {
1732 domUtil.emitEvent( carousel.overlay, 'jp_carousel.afterOpen' );
1733 } );
1734 }
1735
1736 // Register the event listeners for starting the gallery
1737 document.body.addEventListener( 'click', handleInteraction );
1738 document.body.addEventListener( 'keydown', handleInteraction );
1739 document.querySelectorAll( galleryItemSelector + 'img' ).forEach( function ( galleryImage ) {
1740 if ( shouldOpenModal( galleryImage ) ) {
1741 makeGalleryImageAccessible( galleryImage );
1742 }
1743 } );
1744
1745 function handleInteraction( e ) {
1746 if ( e.type === 'click' ) {
1747 handleClick( e );
1748 return;
1749 }
1750
1751 if ( e.type === 'keydown' ) {
1752 const parentElement = document.activeElement.parentElement;
1753 const isParentCarouselContainer =
1754 parentElement && parentElement.classList.contains( 'tiled-gallery__item' );
1755
1756 if ( ( e.key === ' ' || e.key === 'Enter' ) && isParentCarouselContainer ) {
1757 handleClick( e );
1758 }
1759 }
1760 }
1761
1762 function normalizeUrl( url ) {
1763 return ( url || '' ).split( '?' )[ 0 ].replace( /\/$/, '' );
1764 }
1765
1766 function shouldOpenModal( el ) {
1767 if ( el.tagName === 'A' ) {
1768 el = el.querySelector( 'img' ) || el;
1769 }
1770
1771 var parent = el.parentElement;
1772 var grandparent = parent ? parent.parentElement : null;
1773
1774 // If Gallery is made up of individual Image blocks check for custom link before
1775 // loading carousel. The custom link may be the parent or could be a descendant
1776 // of the parent if the image has rounded corners.
1777 var parentHref = null;
1778 if ( grandparent && grandparent.classList.contains( 'wp-block-image' ) ) {
1779 parentHref = parent.getAttribute( 'href' );
1780 } else if (
1781 parent &&
1782 parent.classList.contains( 'wp-block-image' ) &&
1783 parent.querySelector( ':scope > a' )
1784 ) {
1785 parentHref = parent.querySelector( ':scope > a' ).getAttribute( 'href' );
1786 }
1787
1788 // If the link does not point to the attachment or media file then assume Image has
1789 // a custom link so don't load the carousel.
1790 if ( parentHref ) {
1791 var cleanHref = normalizeUrl( parentHref );
1792 var cleanOrig = normalizeUrl( el.getAttribute( 'data-orig-file' ) );
1793 var cleanPerm = normalizeUrl( el.getAttribute( 'data-permalink' ) );
1794
1795 if ( cleanHref !== cleanOrig && cleanHref !== cleanPerm ) {
1796 return false;
1797 }
1798 }
1799
1800 // Do not open the modal if we are looking at a gallery caption from before WP5, which may contain a link.
1801 if ( parent && parent.classList.contains( 'gallery-caption' ) ) {
1802 return false;
1803 }
1804
1805 // Do not open the modal if we are looking at a caption of a gallery block, which may contain a link.
1806 if ( domUtil.matches( parent, 'figcaption' ) ) {
1807 return false;
1808 }
1809
1810 return true;
1811 }
1812
1813 function handleClick( e ) {
1814 var isCompatible =
1815 window.CSS && window.CSS.supports && window.CSS.supports( 'display', 'grid' );
1816
1817 // IE11 support is being dropped in August 2021. The new swiper.js libray is not IE11 compat
1818 // so just default to opening individual image attachment/media pages for IE.
1819 if ( ! isCompatible ) {
1820 return;
1821 }
1822
1823 var target = e.target;
1824 var gallery = domUtil.closest( target, gallerySelector );
1825
1826 if ( gallery ) {
1827 if ( ! testForData( gallery ) ) {
1828 return;
1829 }
1830
1831 if ( ! shouldOpenModal( target ) ) {
1832 return;
1833 }
1834
1835 // Set height to auto.
1836 // Fix some themes where closing carousel brings view back to top.
1837 document.documentElement.style.height = 'auto';
1838
1839 e.preventDefault();
1840
1841 // Stopping propagation in case there are parent elements
1842 // with .gallery or .tiled-gallery class
1843 e.stopPropagation();
1844
1845 var item = domUtil.closest( target, itemSelector );
1846 var index = Array.prototype.indexOf.call( gallery.querySelectorAll( itemSelector ), item );
1847 loadSwiper( gallery, { startIndex: index } );
1848 }
1849 }
1850
1851 // Handle lightbox (single image gallery) for images linking to 'Attachment Page'.
1852 if ( Number( jetpackCarouselStrings.single_image_gallery ) === 1 ) {
1853 processSingleImageGallery();
1854 document.body.addEventListener( 'is.post-load', function () {
1855 processSingleImageGallery();
1856 } );
1857 }
1858
1859 // Makes carousel work on page load and when back button leads to same URL with carousel hash
1860 // (i.e. no actual document.ready trigger).
1861 window.addEventListener( 'hashchange', function () {
1862 var hashRegExp = /jp-carousel-(\d+)/;
1863
1864 if ( ! window.location.hash || ! hashRegExp.test( window.location.hash ) ) {
1865 if ( carousel.isOpen ) {
1866 closeCarousel();
1867 }
1868
1869 return;
1870 }
1871
1872 if ( window.location.hash === lastKnownLocationHash && carousel.isOpen ) {
1873 return;
1874 }
1875
1876 if ( window.location.hash && carousel.gallery && ! carousel.isOpen && history.back ) {
1877 history.back();
1878 return;
1879 }
1880
1881 lastKnownLocationHash = window.location.hash;
1882 var matchList = window.location.hash.match( hashRegExp );
1883 var attachmentId = parseInt( matchList[ 1 ], 10 );
1884 var galleries = document.querySelectorAll( gallerySelector );
1885
1886 // Find the first thumbnail that matches the attachment ID in the location
1887 // hash, then open the gallery that contains it.
1888 for ( var i = 0; i < galleries.length; i++ ) {
1889 var gallery = galleries[ i ];
1890 var selected;
1891
1892 var images = gallery.querySelectorAll( 'img' );
1893 for ( var j = 0; j < images.length; j++ ) {
1894 if (
1895 parseInt( images[ j ].getAttribute( 'data-attachment-id' ), 10 ) === attachmentId ||
1896 parseInt( images[ j ].getAttribute( 'data-id' ), 10 ) === attachmentId
1897 ) {
1898 selected = j;
1899 break;
1900 }
1901 }
1902
1903 if ( selected !== undefined ) {
1904 openOrSelectSlide( gallery, selected );
1905 break;
1906 }
1907 }
1908 } );
1909
1910 if ( window.location.hash ) {
1911 domUtil.emitEvent( window, 'hashchange' );
1912 }
1913 }
1914
1915 if ( document.readyState !== 'loading' ) {
1916 init();
1917 } else {
1918 document.addEventListener( 'DOMContentLoaded', init );
1919 }
1920 } )();
1921