PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.1
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.1
16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 13.8.3 13.9.2 14.0.1 All 504 releases
← All changes | modules/carousel/jetpack-carousel.js +375 -183 12.0.3 → 16.3-a.1 View file →
@@ -1,5 +1,6 @@
1 1 /* global wpcom, jetpackCarouselStrings, DocumentTouch */
2 +/* eslint-disable no-shadow */
2 3
3 4 ( function () {
4 5 'use strict';
5 6 var swiper;
@@ -45,9 +46,9 @@
45 46
46 47 context.filter = 'blur(20px) ';
47 48 context.drawImage( imgEl, 0, 0 );
48 49 var url = canvas.toDataURL( 'image/png' );
49 - canvas = null;
50 + canvas = null; // eslint-disable-line no-useless-assignment -- Verify this isn't needed to free memory or something.
50 51
51 52 return url;
52 53 }
53 54
@@ -106,50 +107,59 @@
106 107 el.style.display = 'block';
107 108 }
108 109 }
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 + */
110 117 function fade( el, start, end, callback ) {
111 118 if ( ! el ) {
112 119 return callback();
113 120 }
114 121
115 - // Prepare for transition.
116 - // Ensure the item is in the render tree, in its initial state.
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' );
117 129 el.style.removeProperty( 'display' );
118 130 el.style.opacity = start;
119 - el.style.transition = 'opacity 0.2s';
120 131 el.style.pointerEvents = 'none';
121 132
122 - var finished = function ( e ) {
123 - if ( e.target === el && e.propertyName === 'opacity' ) {
124 - el.style.removeProperty( 'transition' );
125 - el.style.removeProperty( 'opacity' );
126 - el.style.removeProperty( 'pointer-events' );
127 - el.removeEventListener( 'transitionend', finished );
128 - el.removeEventListener( 'transitioncancel', finished );
129 - callback();
130 - }
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;
131 151 };
132 -
133 - requestAnimationFrame( function () {
134 - // Double rAF for browser compatibility.
135 - requestAnimationFrame( function () {
136 - el.addEventListener( 'transitionend', finished );
137 - el.addEventListener( 'transitioncancel', finished );
138 - // Trigger transition.
139 - el.style.opacity = end;
140 - } );
141 - } );
142 152 }
143 153
144 154 function fadeIn( el, callback ) {
145 155 callback = callback || util.noop;
146 - fade( el, '0', '1', callback );
156 + fade( el, 0, 1, callback );
147 157 }
148 158
149 159 function fadeOut( el, callback ) {
150 160 callback = callback || util.noop;
151 - fade( el, '1', '0', function () {
161 + fade( el, 1, 0, function () {
152 162 if ( el ) {
153 163 el.style.display = 'none';
154 164 }
155 165 callback();
@@ -163,9 +173,9 @@
163 173 bubbles: true,
164 174 cancelable: true,
165 175 detail: detail || null,
166 176 } );
167 - } catch ( err ) {
177 + } catch {
168 178 e = document.createEvent( 'CustomEvent' );
169 179 e.initCustomEvent( type, true, true, detail || null );
170 180 }
171 181 el.dispatchEvent( e );
@@ -258,9 +268,9 @@
258 268 }
259 269
260 270 try {
261 271 return JSON.parse( el.getAttribute( attr ) );
262 - } catch ( e ) {
272 + } catch {
263 273 return undefined;
264 274 }
265 275 }
266 276
@@ -270,9 +280,11 @@
270 280 return dummy.innerHTML;
271 281 }
272 282
273 283 function stripHTML( text ) {
274 - return text.replace( /<[^>]*>?/gm, '' );
284 + var tmp = document.createElement( 'div' );
285 + tmp.innerHTML = text.replace( /<[^>]*>?/gm, '' );
286 + return tmp.textContent;
275 287 }
276 288
277 289 return {
278 290 closest: closest,
@@ -376,8 +388,14 @@
376 388 }
377 389 }
378 390 }
379 391
392 + function makeGalleryImageAccessible( img ) {
393 + img.role = 'button';
394 + img.tabIndex = 0;
395 + img.ariaLabel = jetpackCarouselStrings.image_label;
396 + }
397 +
380 398 function initializeCarousel() {
381 399 if ( ! carousel.overlay ) {
382 400 carousel.overlay = document.querySelector( '.jp-carousel-overlay' );
383 401 carousel.container = carousel.overlay.querySelector( '.jp-carousel-wrap' );
@@ -413,11 +431,9 @@
413 431 var target = e.target;
414 432 var isTargetCloseHint = !! domUtil.closest( target, '.jp-carousel-close-hint' );
415 433 var isSmallScreen = !! window.matchMedia( '(max-device-width: 760px)' ).matches;
416 434 if ( target === carousel.overlay ) {
417 - if ( isSmallScreen ) {
418 - return;
419 - } else {
435 + if ( ! isSmallScreen ) {
420 436 closeCarousel();
421 437 }
422 438 } else if ( isTargetCloseHint ) {
423 439 closeCarousel();
@@ -431,10 +447,8 @@
431 447 domUtil.closest( target, '.jp-carousel-photo-icons-container' ) ||
432 448 target.classList.contains( 'jp-carousel-photo-title' )
433 449 ) {
434 450 handleFooterElementClick( e );
435 - } else if ( ! domUtil.closest( target, '.jp-carousel-info' ) ) {
436 - return;
437 451 }
438 452 } );
439 453
440 454 window.addEventListener( 'keydown', handleKeyboardEvent );
@@ -520,8 +534,9 @@
520 534 var attachmentId = carousel.currentSlide.attrs.attachmentId;
521 535
522 536 var wrapper = document.querySelector( '#jp-carousel-comment-form-submit-and-info-wrapper' );
523 537 var spinner = document.querySelector( '#jp-carousel-comment-form-spinner' );
538 + // eslint-disable-next-line @wordpress/no-unused-vars-before-return
524 539 var submit = document.querySelector( '#jp-carousel-comment-form-button-submit' );
525 540 var form = document.querySelector( '#jp-carousel-comment-form' );
526 541
527 542 if (
@@ -580,9 +595,9 @@
580 595 ) {
581 596 var response;
582 597 try {
583 598 response = JSON.parse( this.response );
584 - } catch ( error ) {
599 + } catch {
585 600 updatePostResults( jetpackCarouselStrings.comment_post_error, false );
586 601 return;
587 602 }
588 603 if ( response.comment_status === 'approved' ) {
@@ -734,8 +749,10 @@
734 749 if ( ! valid ) {
735 750 return;
736 751 }
737 752
753 + makeGalleryImageAccessible( image );
754 +
738 755 // Make this node a gallery recognizable by event listener above.
739 756 link.classList.add( 'single-image-gallery' );
740 757 // blog_id is needed to allow posting comments to correct blog.
741 758 link.setAttribute(
@@ -769,22 +786,14 @@
769 786 carousel.currentSlide = carousel.slides[ index ];
770 787
771 788 var current = carousel.currentSlide;
772 789 var attachmentId = current.attrs.attachmentId;
773 - var infoIcon = carousel.info.querySelector( '.jp-carousel-icon-info' );
774 - var commentsIcon = carousel.info.querySelector( '.jp-carousel-icon-comments' );
775 790
776 - // If the comment/info section is toggled open, it's kept open, but scroll to top of the next slide.
777 - if (
778 - ( infoIcon && infoIcon.classList.contains( 'jp-carousel-selected' ) ) ||
779 - ( commentsIcon && commentsIcon.classList.contains( 'jp-carousel-selected' ) )
780 - ) {
781 - if ( carousel.overlay.scrollTop !== 0 ) {
782 - domUtil.scrollToElement( carousel.overlay, carousel.overlay );
783 - }
784 - }
791 + // Load current image immediately
792 + loadFullImage( carousel.slides[ index ] );
785 793
786 - loadFullImage( carousel.slides[ index ] );
794 + // Preload adjacent images in background
795 + preloadAdjacentImages( index );
787 796
788 797 if (
789 798 Number( jetpackCarouselStrings.display_background_image ) === 1 &&
790 799 ! carousel.slides[ index ].backgroundImage
@@ -816,9 +825,9 @@
816 825 pagination.innerHTML = '<span>' + currentPage + ' / ' + carousel.slides.length + '</span>';
817 826 }
818 827
819 828 // Record pageview in WP Stats, for each new image loaded full-screen.
820 - if ( jetpackCarouselStrings.stats ) {
829 + if ( jetpackCarouselStrings.stats && carousel.isOpen ) {
821 830 new Image().src =
822 831 document.location.protocol +
823 832 '//pixel.wp.com/g.gif?' +
824 833 jetpackCarouselStrings.stats +
@@ -827,11 +836,14 @@
827 836 '&rand=' +
828 837 Math.random();
829 838 }
830 839
831 - pageview( attachmentId );
840 + if ( carousel.isOpen ) {
841 + pageview( attachmentId );
842 + }
832 843
833 - window.location.hash = lastKnownLocationHash = '#jp-carousel-' + attachmentId;
844 + lastKnownLocationHash = '#jp-carousel-' + attachmentId;
845 + window.location.hash = lastKnownLocationHash;
834 846 }
835 847
836 848 function restoreScroll() {
837 849 window.scrollTo( window.scrollX || window.pageXOffset || 0, scrollPos || 0 );
@@ -846,10 +858,10 @@
846 858 disableKeyboardNavigation();
847 859
848 860 domUtil.emitEvent( carousel.overlay, 'jp_carousel.beforeClose' );
849 861 restoreScroll();
862 + carousel.isOpen = false;
850 863 swiper.destroy();
851 - carousel.isOpen = false;
852 864 // Clear slide data for DOM garbage collection.
853 865 carousel.slides = [];
854 866 carousel.currentSlide = undefined;
855 867 carousel.gallery.innerHTML = '';
@@ -865,8 +877,43 @@
865 877 height: window.innerHeight - 64, //subtract height of bottom info bar,
866 878 };
867 879 }
868 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 +
869 916 function selectBestImageUrl( args ) {
870 917 if ( typeof args !== 'object' ) {
871 918 args = {};
872 919 }
@@ -878,9 +925,14 @@
878 925 if ( typeof args.origWidth === 'undefined' || typeof args.maxWidth === 'undefined' ) {
879 926 return args.origFile;
880 927 }
881 928
882 - if ( typeof args.mediumFile === 'undefined' || typeof args.largeFile === 'undefined' ) {
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 ) {
883 935 return args.origFile;
884 936 }
885 937
886 938 // Check if the image is being served by Photon (using a regular expression on the hostname).
@@ -889,15 +941,12 @@
889 941 imageLinkParser.href = args.largeFile;
890 942
891 943 var isPhotonUrl = /^i[0-2]\.wp\.com$/i.test( imageLinkParser.hostname );
892 944
893 - var mediumSizeParts = getImageSizeParts( args.mediumFile, args.origWidth, isPhotonUrl );
894 945 var largeSizeParts = getImageSizeParts( args.largeFile, args.origWidth, isPhotonUrl );
895 946
896 947 var largeWidth = parseInt( largeSizeParts[ 0 ], 10 );
897 948 var largeHeight = parseInt( largeSizeParts[ 1 ], 10 );
898 - var mediumWidth = parseInt( mediumSizeParts[ 0 ], 10 );
899 - var mediumHeight = parseInt( mediumSizeParts[ 1 ], 10 );
900 949
901 950 args.origMaxWidth = args.maxWidth;
902 951 args.origMaxHeight = args.maxHeight;
903 952
@@ -910,29 +959,30 @@
910 959 if ( largeWidth >= args.maxWidth || largeHeight >= args.maxHeight ) {
911 960 return args.largeFile;
912 961 }
913 962
914 - if ( mediumWidth >= args.maxWidth || mediumHeight >= args.maxHeight ) {
915 - return args.mediumFile;
916 - }
917 -
918 963 if ( isPhotonUrl ) {
919 964 // args.origFile doesn't point to a Photon url, so in this case we use args.largeFile
920 965 // to return the photon url of the original image.
921 - var largeFileIndex = args.largeFile.lastIndexOf( '?' );
922 - var origPhotonUrl = args.largeFile;
923 - if ( largeFileIndex !== -1 ) {
924 - origPhotonUrl = args.largeFile.substring( 0, largeFileIndex );
925 - // If we have a really large image load a smaller version
926 - // that is closer to the viewable size
927 - if ( args.origWidth > args.maxWidth || args.origHeight > args.maxHeight ) {
928 - // @2x the max sizes so we get a high enough resolution for zooming.
929 - args.origMaxWidth = args.maxWidth * 2;
930 - args.origMaxHeight = args.maxHeight * 2;
931 - origPhotonUrl += '?fit=' + args.origMaxWidth + '%2C' + args.origMaxHeight;
932 - }
966 + if ( args.largeFile.lastIndexOf( '?' ) === -1 ) {
967 + return args.largeFile;
933 968 }
934 - return origPhotonUrl;
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();
935 985 }
936 986
937 987 return args.origFile;
938 988 }
@@ -941,14 +991,14 @@
941 991 var size = isPhotonUrl
942 992 ? file.replace( /.*=([\d]+%2C[\d]+).*$/, '$1' )
943 993 : file.replace( /.*-([\d]+x[\d]+)\..+$/, '$1' );
944 994
945 - var sizeParts =
946 - size !== file
947 - ? isPhotonUrl
948 - ? size.split( '%2C' )
949 - : size.split( 'x' )
950 - : [ origWidth, 0 ];
995 + var sizeParts;
996 + if ( size !== file ) {
997 + sizeParts = isPhotonUrl ? size.split( '%2C' ) : size.split( 'x' );
998 + } else {
999 + sizeParts = [ origWidth, 0 ];
1000 + }
951 1001
952 1002 // If one of the dimensions is set to 9999, then the actual value of that dimension can't be retrieved from the url.
953 1003 // In that case, we set the value to 0.
954 1004 if ( sizeParts[ 0 ] === '9999' ) {
@@ -992,11 +1042,11 @@
992 1042 return value;
993 1043 }
994 1044
995 1045 function updateTitleCaptionAndDesc( data ) {
996 - var caption = '';
997 - var title = '';
998 - var desc = '';
1046 + var caption;
1047 + var title;
1048 + var desc;
999 1049 var captionMainElement;
1000 1050 var captionInfoExtraElement;
1001 1051 var titleElement;
1002 1052 var descriptionElement;
@@ -1041,9 +1091,9 @@
1041 1091 descriptionElement.innerHTML = desc;
1042 1092 domUtil.show( descriptionElement );
1043 1093
1044 1094 if ( ! title && ! caption ) {
1045 - captionMainElement.innerHTML = domUtil.stripHTML( desc );
1095 + captionMainElement.textContent = domUtil.stripHTML( desc );
1046 1096 domUtil.show( captionMainElement );
1047 1097 }
1048 1098 }
1049 1099
@@ -1048,13 +1098,13 @@
1048 1098 }
1049 1099
1050 1100 if ( title ) {
1051 1101 var plainTitle = domUtil.stripHTML( title );
1052 - titleElement.innerHTML = plainTitle;
1102 + titleElement.textContent = plainTitle;
1053 1103
1054 1104 if ( ! caption ) {
1055 - captionMainElement.innerHTML = plainTitle;
1056 - captionInfoExtraElement.innerHTML = plainTitle;
1105 + captionMainElement.textContent = plainTitle;
1106 + captionInfoExtraElement.textContent = plainTitle;
1057 1107
1058 1108 domUtil.show( captionMainElement );
1059 1109 }
1060 1110
@@ -1068,9 +1118,14 @@
1068 1118 if ( ! meta || Number( jetpackCarouselStrings.display_exif ) !== 1 ) {
1069 1119 return false;
1070 1120 }
1071 1121
1072 - var ul = carousel.info.querySelector( '.jp-carousel-image-meta ul.jp-carousel-image-exif' );
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 +
1073 1128 var html = '';
1074 1129
1075 1130 for ( var key in meta ) {
1076 1131 var val = meta[ key ];
@@ -1094,10 +1149,31 @@
1094 1149
1095 1150 html += '<li><h5>' + jetpackCarouselStrings[ key ] + '</h5>' + val + '</li>';
1096 1151 }
1097 1152
1098 - ul.innerHTML = html;
1099 - ul.style.removeProperty( 'display' );
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 + }
1100 1176 }
1101 1177
1102 1178 // Update the contents of the jp-carousel-image-download link
1103 1179 function updateFullSizeLink( currentSlide ) {
@@ -1128,9 +1204,9 @@
1128 1204 permalink.style.removeProperty( 'display' );
1129 1205 }
1130 1206
1131 1207 function testCommentsOpened( opened ) {
1132 - var commentForm = carousel.container.querySelector( '.jp-carousel-comment-form-container' );
1208 + var commentForm = carousel.info.querySelector( '#jp-carousel-comment-form-container' );
1133 1209 var isOpened = parseInt( opened, 10 ) === 1;
1134 1210
1135 1211 if ( isOpened ) {
1136 1212 domUtil.fadeIn( commentForm );
@@ -1196,9 +1272,9 @@
1196 1272 var isSuccess = xhr.status >= 200 && xhr.status < 300;
1197 1273 var data;
1198 1274 try {
1199 1275 data = JSON.parse( xhr.responseText );
1200 - } catch ( e ) {
1276 + } catch {
1201 1277 // Do nothing.
1202 1278 }
1203 1279
1204 1280 if ( ! isSuccess || ! data || ! Array.isArray( data ) ) {
@@ -1253,52 +1329,137 @@
1253 1329 xhr.send();
1254 1330 }
1255 1331
1256 1332 function loadFullImage( slide ) {
1257 - var el = slide.el;
1258 1333 var attrs = slide.attrs;
1259 - var image = el.querySelector( 'img' );
1334 + var image = slide.el.querySelector( 'img' );
1260 1335
1261 - if ( ! image.hasAttribute( 'data-loaded' ) ) {
1262 - var hasPreview = !! attrs.previewImage;
1263 - var thumbSize = attrs.thumbSize;
1336 + if ( image.hasAttribute( 'data-loaded' ) ) {
1337 + return;
1338 + }
1264 1339
1265 - if ( ! hasPreview || ( thumbSize && el.offsetWidth > thumbSize.width ) ) {
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.
1266 1370 image.src = attrs.src;
1267 - } else {
1268 - image.src = attrs.previewImage;
1269 - }
1371 + image.style.filter = '';
1372 + },
1373 + { once: true }
1374 + );
1270 1375
1271 - image.setAttribute( 'itemprop', 'image' );
1272 - image.setAttribute( 'data-loaded', 1 );
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 );
1273 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 + } );
1274 1417 }
1275 1418
1276 1419 function loadBackgroundImage( slide ) {
1277 - var currentSlide = slide.el;
1420 + var image = slide.attrs.originalElement;
1278 1421
1279 - if ( swiper && swiper.slides ) {
1280 - currentSlide = swiper.slides[ swiper.activeIndex ];
1422 + if ( ! image ) {
1423 + return;
1281 1424 }
1282 1425
1283 - var image = slide.attrs.originalElement;
1284 - var isLoaded = image.complete && image.naturalHeight !== 0;
1285 -
1286 - if ( isLoaded ) {
1287 - applyBackgroundImage( slide, currentSlide, image );
1426 + if ( image.complete && image.naturalHeight !== 0 ) {
1427 + applyBackgroundImage( slide, image );
1288 1428 return;
1289 1429 }
1290 1430
1291 - image.onload = function () {
1292 - applyBackgroundImage( slide, currentSlide, image );
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 );
1293 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 } );
1294 1445 }
1295 1446
1296 - function applyBackgroundImage( slide, currentSlide, image ) {
1447 + function applyBackgroundImage( slide, image ) {
1297 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.
1298 1458 slide.backgroundImage = url;
1299 - currentSlide.style.backgroundImage = 'url(' + url + ')';
1300 - currentSlide.style.backgroundSize = 'cover';
1459 + slide.el.style.backgroundImage = 'url(' + url + ')';
1460 + slide.el.style.backgroundSize = 'cover';
1461 + slide.el.style.backgroundPosition = 'center';
1301 1462 }
1302 1463
1303 1464 function clearCommentTextAreaValue() {
1304 1465 if ( carousel.commentField ) {
@@ -1311,16 +1472,14 @@
1311 1472
1312 1473 if ( size ) {
1313 1474 var parts = size.split( ',' );
1314 1475 return { width: parseInt( parts[ 0 ], 10 ), height: parseInt( parts[ 1 ], 10 ) };
1315 - } else {
1316 - return {
1317 - width:
1318 - el.getAttribute( 'data-original-width' ) || el.getAttribute( 'width' ) || undefined,
1319 - height:
1320 - el.getAttribute( 'data-original-height' ) || el.getAttribute( 'height' ) || undefined,
1321 - };
1322 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 + };
1323 1482 }
1324 1483
1325 1484 function initCarouselSlides( items, startIndex ) {
1326 1485 carousel.slides = [];
@@ -1327,15 +1486,13 @@
1327 1486
1328 1487 var max = calculateMaxSlideDimensions();
1329 1488
1330 1489 // If the startIndex is not 0 then preload the clicked image first.
1331 - if ( startIndex !== 0 ) {
1490 + if ( startIndex !== 0 && items[ startIndex ].getAttribute( 'data-gallery-src' ) !== null ) {
1332 1491 var img = new Image();
1333 1492 img.src = items[ startIndex ].getAttribute( 'data-gallery-src' );
1334 1493 }
1335 1494
1336 - var useInPageThumbnails = !! domUtil.closest( items[ 0 ], '.tiled-gallery.type-rectangular' );
1337 -
1338 1495 // create the 'slide'
1339 1496 Array.prototype.forEach.call( items, function ( item, i ) {
1340 1497 var permalinkEl = domUtil.closest( item, 'a' );
1341 1498 var origFile = item.getAttribute( 'data-orig-file' ) || item.getAttribute( 'src-orig' );
@@ -1357,9 +1514,8 @@
1357 1514 commentsOpened: item.getAttribute( 'data-comments-opened' ) || '0',
1358 1515 imageMeta: domUtil.getJSONAttribute( item, 'data-image-meta' ) || {},
1359 1516 title: item.getAttribute( 'data-image-title' ) || '',
1360 1517 desc: item.getAttribute( 'data-image-description' ) || '',
1361 - mediumFile: item.getAttribute( 'data-medium-file' ) || '',
1362 1518 largeFile: item.getAttribute( 'data-large-file' ) || '',
1363 1519 origFile: origFile || '',
1364 1520 thumbSize: { width: item.naturalWidth, height: item.naturalHeight },
1365 1521 caption: caption || '',
@@ -1388,9 +1544,8 @@
1388 1544 origWidth: attrs.origWidth,
1389 1545 origHeight: attrs.origHeight,
1390 1546 maxWidth: max.width,
1391 1547 maxHeight: max.height,
1392 - mediumFile: attrs.mediumFile,
1393 1548 largeFile: attrs.largeFile,
1394 1549 } );
1395 1550 }
1396 1551
@@ -1404,9 +1559,8 @@
1404 1559
1405 1560 // Initially, the image is a 1x1 transparent gif.
1406 1561 // The preview is shown as a background image on the slide itself.
1407 1562 var image = new Image();
1408 - image.src = attrs.src;
1409 1563
1410 1564 var slideEl = document.createElement( 'div' );
1411 1565 slideEl.classList.add( 'swiper-slide' );
1412 1566 slideEl.setAttribute( 'itemprop', 'associatedMedia' );
@@ -1422,12 +1576,12 @@
1422 1576 slideEl.setAttribute( 'data-attachment-id', attrs.attachmentId );
1423 1577 slideEl.setAttribute( 'data-permalink', attrs.permalink );
1424 1578 slideEl.setAttribute( 'data-orig-file', attrs.origFile );
1425 1579
1426 - if ( useInPageThumbnails ) {
1427 - // Use the image already loaded in the gallery as a preview.
1428 - attrs.previewImage = attrs.src;
1429 - }
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' ) || '';
1430 1584
1431 1585 var slide = { el: slideEl, attrs: attrs, index: i };
1432 1586 carousel.slides.push( slide );
1433 1587 }
@@ -1434,9 +1588,9 @@
1434 1588 } );
1435 1589 }
1436 1590
1437 1591 function loadSwiper( gallery, options ) {
1438 - if ( ! window.Swiper670 ) {
1592 + if ( ! window.JetpackSwiper ) {
1439 1593 var loader = document.querySelector( '#jp-carousel-loading-overlay' );
1440 1594 domUtil.show( loader );
1441 1595 var jsScript = document.createElement( 'script' );
1442 1596 jsScript.id = 'jetpack-carousel-swiper-js';
@@ -1468,8 +1622,13 @@
1468 1622 if ( ! data ) {
1469 1623 return; // don't run if the default gallery functions weren't used
1470 1624 }
1471 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 +
1472 1631 initializeCarousel();
1473 1632
1474 1633 if ( carousel.isOpen ) {
1475 1634 return; // don't open if already opened
@@ -1504,11 +1663,11 @@
1504 1663 // Need to set the overlay manually to block or swiper does't initialise properly.
1505 1664 carousel.overlay.style.opacity = 1;
1506 1665 carousel.overlay.style.display = 'block';
1507 1666
1508 - initCarouselSlides( gallery.querySelectorAll( settings.imgSelector ), settings.startIndex );
1667 + initCarouselSlides( images, settings.startIndex );
1509 1668
1510 - swiper = new window.Swiper670( '.jp-carousel-swiper-container', {
1669 + swiper = new window.JetpackSwiper( '.jp-carousel-swiper-container', {
1511 1670 centeredSlides: true,
1512 1671 zoom: true,
1513 1672 loop: carousel.slides.length > 1,
1514 1673 // Turn off interactions and hide navigation arrows if there is only one slide.
@@ -1533,21 +1692,12 @@
1533 1692 threshold: 5,
1534 1693 } );
1535 1694
1536 1695 swiper.on( 'slideChange', function ( swiper ) {
1537 - var index;
1538 - // Swiper indexes slides from 1, plus when looping to left last slide ends up
1539 - // as 0 and looping to right first slide as total slides + 1. These are adjusted
1540 - // here to match index of carousel.slides.
1541 - if ( swiper.activeIndex === 0 ) {
1542 - index = carousel.slides.length - 1;
1543 - } else if ( swiper.activeIndex === carousel.slides.length + 1 ) {
1544 - index = 0;
1545 - } else {
1546 - index = swiper.activeIndex - 1;
1696 + if ( ! carousel.isOpen ) {
1697 + return;
1547 1698 }
1548 - selectSlideAtIndex( index );
1549 -
1699 + selectSlideAtIndex( swiper.realIndex );
1550 1700 carousel.overlay.classList.remove( 'jp-carousel-hide-controls' );
1551 1701 } );
1552 1702
1553 1703 swiper.on( 'zoomChange', function ( swiper, scale ) {
@@ -1582,10 +1732,86 @@
1582 1732 domUtil.emitEvent( carousel.overlay, 'jp_carousel.afterOpen' );
1583 1733 } );
1584 1734 }
1585 1735
1586 - // Register the event listener for starting the gallery
1587 - document.body.addEventListener( 'click', function ( e ) {
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 ) {
1588 1814 var isCompatible =
1589 1815 window.CSS && window.CSS.supports && window.CSS.supports( 'display', 'grid' );
1590 1816
1591 1817 // IE11 support is being dropped in August 2021. The new swiper.js libray is not IE11 compat
@@ -1601,46 +1827,12 @@
1601 1827 if ( ! testForData( gallery ) ) {
1602 1828 return;
1603 1829 }
1604 1830
1605 - var parent = target.parentElement;
1606 - var grandparent = parent.parentElement;
1607 -
1608 - // If Gallery is made up of individual Image blocks check for custom link before
1609 - // loading carousel. The custom link may be the parent or could be a descendant
1610 - // of the parent if the image has rounded corners.
1611 - var parentHref = null;
1612 - if ( grandparent && grandparent.classList.contains( 'wp-block-image' ) ) {
1613 - parentHref = parent.getAttribute( 'href' );
1614 - } else if (
1615 - parent &&
1616 - parent.classList.contains( 'wp-block-image' ) &&
1617 - parent.querySelector( ':scope > a' )
1618 - ) {
1619 - parentHref = parent.querySelector( ':scope > a' ).getAttribute( 'href' );
1620 - }
1621 -
1622 - // If the link does not point to the attachment or media file then assume Image has
1623 - // a custom link so don't load the carousel.
1624 - if (
1625 - parentHref &&
1626 - parentHref.split( '?' )[ 0 ] !==
1627 - target.getAttribute( 'data-orig-file' ).split( '?' )[ 0 ] &&
1628 - parentHref !== target.getAttribute( 'data-permalink' )
1629 - ) {
1831 + if ( ! shouldOpenModal( target ) ) {
1630 1832 return;
1631 1833 }
1632 1834
1633 - // Do not open the modal if we are looking at a gallery caption from before WP5, which may contain a link.
1634 - if ( parent.classList.contains( 'gallery-caption' ) ) {
1635 - return;
1636 - }
1637 -
1638 - // Do not open the modal if we are looking at a caption of a gallery block, which may contain a link.
1639 - if ( domUtil.matches( parent, 'figcaption' ) ) {
1640 - return;
1641 - }
1642 -
1643 1835 // Set height to auto.
1644 1836 // Fix some themes where closing carousel brings view back to top.
1645 1837 document.documentElement.style.height = 'auto';
1646 1838
@@ -1653,9 +1845,9 @@
1653 1845 var item = domUtil.closest( target, itemSelector );
1654 1846 var index = Array.prototype.indexOf.call( gallery.querySelectorAll( itemSelector ), item );
1655 1847 loadSwiper( gallery, { startIndex: index } );
1656 1848 }
1657 - } );
1849 + }
1658 1850
1659 1851 // Handle lightbox (single image gallery) for images linking to 'Attachment Page'.
1660 1852 if ( Number( jetpackCarouselStrings.single_image_gallery ) === 1 ) {
1661 1853 processSingleImageGallery();