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 +366 -167 13.2.416.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,40 +107,49 @@
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 131 el.style.pointerEvents = 'none';
120 132
121 - var animate = function ( t0, duration ) {
122 - var t = performance.now();
123 - var diff = t - t0;
124 - var ratio = diff / duration;
133 + // Commit the starting opacity, otherwise the browser has nothing to animate from.
134 + void el.offsetWidth;
125 135
126 - if ( ratio < 1 ) {
127 - el.style.opacity = start + ( end - start ) * ratio;
128 - requestAnimationFrame( () => animate( t0, duration ) );
129 - } else {
130 - el.style.opacity = end;
131 - el.style.removeProperty( 'pointer-events' );
132 - callback();
133 - }
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;
134 151 };
135 -
136 - requestAnimationFrame( function () {
137 - // Double rAF for browser compatibility.
138 - requestAnimationFrame( function () {
139 - animate( performance.now(), 200 );
140 - } );
141 - } );
142 152 }
143 153
144 154 function fadeIn( el, callback ) {
145 155 callback = callback || util.noop;
@@ -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(
@@ -770,10 +787,14 @@
770 787
771 788 var current = carousel.currentSlide;
772 789 var attachmentId = current.attrs.attachmentId;
773 790
791 + // Load current image immediately
774 792 loadFullImage( carousel.slides[ index ] );
775 793
794 + // Preload adjacent images in background
795 + preloadAdjacentImages( index );
796 +
776 797 if (
777 798 Number( jetpackCarouselStrings.display_background_image ) === 1 &&
778 799 ! carousel.slides[ index ].backgroundImage
779 800 ) {
@@ -804,9 +825,9 @@
804 825 pagination.innerHTML = '<span>' + currentPage + ' / ' + carousel.slides.length + '</span>';
805 826 }
806 827
807 828 // Record pageview in WP Stats, for each new image loaded full-screen.
808 - if ( jetpackCarouselStrings.stats ) {
829 + if ( jetpackCarouselStrings.stats && carousel.isOpen ) {
809 830 new Image().src =
810 831 document.location.protocol +
811 832 '//pixel.wp.com/g.gif?' +
812 833 jetpackCarouselStrings.stats +
@@ -815,11 +836,14 @@
815 836 '&rand=' +
816 837 Math.random();
817 838 }
818 839
819 - pageview( attachmentId );
840 + if ( carousel.isOpen ) {
841 + pageview( attachmentId );
842 + }
820 843
821 - window.location.hash = lastKnownLocationHash = '#jp-carousel-' + attachmentId;
844 + lastKnownLocationHash = '#jp-carousel-' + attachmentId;
845 + window.location.hash = lastKnownLocationHash;
822 846 }
823 847
824 848 function restoreScroll() {
825 849 window.scrollTo( window.scrollX || window.pageXOffset || 0, scrollPos || 0 );
@@ -834,10 +858,10 @@
834 858 disableKeyboardNavigation();
835 859
836 860 domUtil.emitEvent( carousel.overlay, 'jp_carousel.beforeClose' );
837 861 restoreScroll();
862 + carousel.isOpen = false;
838 863 swiper.destroy();
839 - carousel.isOpen = false;
840 864 // Clear slide data for DOM garbage collection.
841 865 carousel.slides = [];
842 866 carousel.currentSlide = undefined;
843 867 carousel.gallery.innerHTML = '';
@@ -853,8 +877,43 @@
853 877 height: window.innerHeight - 64, //subtract height of bottom info bar,
854 878 };
855 879 }
856 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 +
857 916 function selectBestImageUrl( args ) {
858 917 if ( typeof args !== 'object' ) {
859 918 args = {};
860 919 }
@@ -866,9 +925,14 @@
866 925 if ( typeof args.origWidth === 'undefined' || typeof args.maxWidth === 'undefined' ) {
867 926 return args.origFile;
868 927 }
869 928
870 - 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 ) {
871 935 return args.origFile;
872 936 }
873 937
874 938 // Check if the image is being served by Photon (using a regular expression on the hostname).
@@ -877,15 +941,12 @@
877 941 imageLinkParser.href = args.largeFile;
878 942
879 943 var isPhotonUrl = /^i[0-2]\.wp\.com$/i.test( imageLinkParser.hostname );
880 944
881 - var mediumSizeParts = getImageSizeParts( args.mediumFile, args.origWidth, isPhotonUrl );
882 945 var largeSizeParts = getImageSizeParts( args.largeFile, args.origWidth, isPhotonUrl );
883 946
884 947 var largeWidth = parseInt( largeSizeParts[ 0 ], 10 );
885 948 var largeHeight = parseInt( largeSizeParts[ 1 ], 10 );
886 - var mediumWidth = parseInt( mediumSizeParts[ 0 ], 10 );
887 - var mediumHeight = parseInt( mediumSizeParts[ 1 ], 10 );
888 949
889 950 args.origMaxWidth = args.maxWidth;
890 951 args.origMaxHeight = args.maxHeight;
891 952
@@ -898,29 +959,30 @@
898 959 if ( largeWidth >= args.maxWidth || largeHeight >= args.maxHeight ) {
899 960 return args.largeFile;
900 961 }
901 962
902 - if ( mediumWidth >= args.maxWidth || mediumHeight >= args.maxHeight ) {
903 - return args.mediumFile;
904 - }
905 -
906 963 if ( isPhotonUrl ) {
907 964 // args.origFile doesn't point to a Photon url, so in this case we use args.largeFile
908 965 // to return the photon url of the original image.
909 - var largeFileIndex = args.largeFile.lastIndexOf( '?' );
910 - var origPhotonUrl = args.largeFile;
911 - if ( largeFileIndex !== -1 ) {
912 - origPhotonUrl = args.largeFile.substring( 0, largeFileIndex );
913 - // If we have a really large image load a smaller version
914 - // that is closer to the viewable size
915 - if ( args.origWidth > args.maxWidth || args.origHeight > args.maxHeight ) {
916 - // @2x the max sizes so we get a high enough resolution for zooming.
917 - args.origMaxWidth = args.maxWidth * 2;
918 - args.origMaxHeight = args.maxHeight * 2;
919 - origPhotonUrl += '?fit=' + args.origMaxWidth + '%2C' + args.origMaxHeight;
920 - }
966 + if ( args.largeFile.lastIndexOf( '?' ) === -1 ) {
967 + return args.largeFile;
921 968 }
922 - 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();
923 985 }
924 986
925 987 return args.origFile;
926 988 }
@@ -929,14 +991,14 @@
929 991 var size = isPhotonUrl
930 992 ? file.replace( /.*=([\d]+%2C[\d]+).*$/, '$1' )
931 993 : file.replace( /.*-([\d]+x[\d]+)\..+$/, '$1' );
932 994
933 - var sizeParts =
934 - size !== file
935 - ? isPhotonUrl
936 - ? size.split( '%2C' )
937 - : size.split( 'x' )
938 - : [ 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 + }
939 1001
940 1002 // If one of the dimensions is set to 9999, then the actual value of that dimension can't be retrieved from the url.
941 1003 // In that case, we set the value to 0.
942 1004 if ( sizeParts[ 0 ] === '9999' ) {
@@ -980,11 +1042,11 @@
980 1042 return value;
981 1043 }
982 1044
983 1045 function updateTitleCaptionAndDesc( data ) {
984 - var caption = '';
985 - var title = '';
986 - var desc = '';
1046 + var caption;
1047 + var title;
1048 + var desc;
987 1049 var captionMainElement;
988 1050 var captionInfoExtraElement;
989 1051 var titleElement;
990 1052 var descriptionElement;
@@ -1029,9 +1091,9 @@
1029 1091 descriptionElement.innerHTML = desc;
1030 1092 domUtil.show( descriptionElement );
1031 1093
1032 1094 if ( ! title && ! caption ) {
1033 - captionMainElement.innerHTML = domUtil.stripHTML( desc );
1095 + captionMainElement.textContent = domUtil.stripHTML( desc );
1034 1096 domUtil.show( captionMainElement );
1035 1097 }
1036 1098 }
1037 1099
@@ -1036,13 +1098,13 @@
1036 1098 }
1037 1099
1038 1100 if ( title ) {
1039 1101 var plainTitle = domUtil.stripHTML( title );
1040 - titleElement.innerHTML = plainTitle;
1102 + titleElement.textContent = plainTitle;
1041 1103
1042 1104 if ( ! caption ) {
1043 - captionMainElement.innerHTML = plainTitle;
1044 - captionInfoExtraElement.innerHTML = plainTitle;
1105 + captionMainElement.textContent = plainTitle;
1106 + captionInfoExtraElement.textContent = plainTitle;
1045 1107
1046 1108 domUtil.show( captionMainElement );
1047 1109 }
1048 1110
@@ -1056,9 +1118,14 @@
1056 1118 if ( ! meta || Number( jetpackCarouselStrings.display_exif ) !== 1 ) {
1057 1119 return false;
1058 1120 }
1059 1121
1060 - 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 +
1061 1128 var html = '';
1062 1129
1063 1130 for ( var key in meta ) {
1064 1131 var val = meta[ key ];
@@ -1082,10 +1149,31 @@
1082 1149
1083 1150 html += '<li><h5>' + jetpackCarouselStrings[ key ] + '</h5>' + val + '</li>';
1084 1151 }
1085 1152
1086 - ul.innerHTML = html;
1087 - 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 + }
1088 1176 }
1089 1177
1090 1178 // Update the contents of the jp-carousel-image-download link
1091 1179 function updateFullSizeLink( currentSlide ) {
@@ -1116,9 +1204,9 @@
1116 1204 permalink.style.removeProperty( 'display' );
1117 1205 }
1118 1206
1119 1207 function testCommentsOpened( opened ) {
1120 - var commentForm = carousel.container.querySelector( '.jp-carousel-comment-form-container' );
1208 + var commentForm = carousel.info.querySelector( '#jp-carousel-comment-form-container' );
1121 1209 var isOpened = parseInt( opened, 10 ) === 1;
1122 1210
1123 1211 if ( isOpened ) {
1124 1212 domUtil.fadeIn( commentForm );
@@ -1184,9 +1272,9 @@
1184 1272 var isSuccess = xhr.status >= 200 && xhr.status < 300;
1185 1273 var data;
1186 1274 try {
1187 1275 data = JSON.parse( xhr.responseText );
1188 - } catch ( e ) {
1276 + } catch {
1189 1277 // Do nothing.
1190 1278 }
1191 1279
1192 1280 if ( ! isSuccess || ! data || ! Array.isArray( data ) ) {
@@ -1241,52 +1329,137 @@
1241 1329 xhr.send();
1242 1330 }
1243 1331
1244 1332 function loadFullImage( slide ) {
1245 - var el = slide.el;
1246 1333 var attrs = slide.attrs;
1247 - var image = el.querySelector( 'img' );
1334 + var image = slide.el.querySelector( 'img' );
1248 1335
1249 - if ( ! image.hasAttribute( 'data-loaded' ) ) {
1250 - var hasPreview = !! attrs.previewImage;
1251 - var thumbSize = attrs.thumbSize;
1336 + if ( image.hasAttribute( 'data-loaded' ) ) {
1337 + return;
1338 + }
1252 1339
1253 - 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.
1254 1370 image.src = attrs.src;
1255 - } else {
1256 - image.src = attrs.previewImage;
1257 - }
1371 + image.style.filter = '';
1372 + },
1373 + { once: true }
1374 + );
1258 1375
1259 - image.setAttribute( 'itemprop', 'image' );
1260 - 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 );
1261 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 + } );
1262 1417 }
1263 1418
1264 1419 function loadBackgroundImage( slide ) {
1265 - var currentSlide = slide.el;
1420 + var image = slide.attrs.originalElement;
1266 1421
1267 - if ( swiper && swiper.slides ) {
1268 - currentSlide = swiper.slides[ swiper.activeIndex ];
1422 + if ( ! image ) {
1423 + return;
1269 1424 }
1270 1425
1271 - var image = slide.attrs.originalElement;
1272 - var isLoaded = image.complete && image.naturalHeight !== 0;
1273 -
1274 - if ( isLoaded ) {
1275 - applyBackgroundImage( slide, currentSlide, image );
1426 + if ( image.complete && image.naturalHeight !== 0 ) {
1427 + applyBackgroundImage( slide, image );
1276 1428 return;
1277 1429 }
1278 1430
1279 - image.onload = function () {
1280 - 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 );
1281 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 } );
1282 1445 }
1283 1446
1284 - function applyBackgroundImage( slide, currentSlide, image ) {
1447 + function applyBackgroundImage( slide, image ) {
1285 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.
1286 1458 slide.backgroundImage = url;
1287 - currentSlide.style.backgroundImage = 'url(' + url + ')';
1288 - currentSlide.style.backgroundSize = 'cover';
1459 + slide.el.style.backgroundImage = 'url(' + url + ')';
1460 + slide.el.style.backgroundSize = 'cover';
1461 + slide.el.style.backgroundPosition = 'center';
1289 1462 }
1290 1463
1291 1464 function clearCommentTextAreaValue() {
1292 1465 if ( carousel.commentField ) {
@@ -1299,16 +1472,14 @@
1299 1472
1300 1473 if ( size ) {
1301 1474 var parts = size.split( ',' );
1302 1475 return { width: parseInt( parts[ 0 ], 10 ), height: parseInt( parts[ 1 ], 10 ) };
1303 - } else {
1304 - return {
1305 - width:
1306 - el.getAttribute( 'data-original-width' ) || el.getAttribute( 'width' ) || undefined,
1307 - height:
1308 - el.getAttribute( 'data-original-height' ) || el.getAttribute( 'height' ) || undefined,
1309 - };
1310 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 + };
1311 1482 }
1312 1483
1313 1484 function initCarouselSlides( items, startIndex ) {
1314 1485 carousel.slides = [];
@@ -1315,15 +1486,13 @@
1315 1486
1316 1487 var max = calculateMaxSlideDimensions();
1317 1488
1318 1489 // If the startIndex is not 0 then preload the clicked image first.
1319 - if ( startIndex !== 0 ) {
1490 + if ( startIndex !== 0 && items[ startIndex ].getAttribute( 'data-gallery-src' ) !== null ) {
1320 1491 var img = new Image();
1321 1492 img.src = items[ startIndex ].getAttribute( 'data-gallery-src' );
1322 1493 }
1323 1494
1324 - var useInPageThumbnails = !! domUtil.closest( items[ 0 ], '.tiled-gallery.type-rectangular' );
1325 -
1326 1495 // create the 'slide'
1327 1496 Array.prototype.forEach.call( items, function ( item, i ) {
1328 1497 var permalinkEl = domUtil.closest( item, 'a' );
1329 1498 var origFile = item.getAttribute( 'data-orig-file' ) || item.getAttribute( 'src-orig' );
@@ -1345,9 +1514,8 @@
1345 1514 commentsOpened: item.getAttribute( 'data-comments-opened' ) || '0',
1346 1515 imageMeta: domUtil.getJSONAttribute( item, 'data-image-meta' ) || {},
1347 1516 title: item.getAttribute( 'data-image-title' ) || '',
1348 1517 desc: item.getAttribute( 'data-image-description' ) || '',
1349 - mediumFile: item.getAttribute( 'data-medium-file' ) || '',
1350 1518 largeFile: item.getAttribute( 'data-large-file' ) || '',
1351 1519 origFile: origFile || '',
1352 1520 thumbSize: { width: item.naturalWidth, height: item.naturalHeight },
1353 1521 caption: caption || '',
@@ -1376,9 +1544,8 @@
1376 1544 origWidth: attrs.origWidth,
1377 1545 origHeight: attrs.origHeight,
1378 1546 maxWidth: max.width,
1379 1547 maxHeight: max.height,
1380 - mediumFile: attrs.mediumFile,
1381 1548 largeFile: attrs.largeFile,
1382 1549 } );
1383 1550 }
1384 1551
@@ -1392,9 +1559,8 @@
1392 1559
1393 1560 // Initially, the image is a 1x1 transparent gif.
1394 1561 // The preview is shown as a background image on the slide itself.
1395 1562 var image = new Image();
1396 - image.src = attrs.src;
1397 1563
1398 1564 var slideEl = document.createElement( 'div' );
1399 1565 slideEl.classList.add( 'swiper-slide' );
1400 1566 slideEl.setAttribute( 'itemprop', 'associatedMedia' );
@@ -1410,12 +1576,12 @@
1410 1576 slideEl.setAttribute( 'data-attachment-id', attrs.attachmentId );
1411 1577 slideEl.setAttribute( 'data-permalink', attrs.permalink );
1412 1578 slideEl.setAttribute( 'data-orig-file', attrs.origFile );
1413 1579
1414 - if ( useInPageThumbnails ) {
1415 - // Use the image already loaded in the gallery as a preview.
1416 - attrs.previewImage = attrs.src;
1417 - }
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' ) || '';
1418 1584
1419 1585 var slide = { el: slideEl, attrs: attrs, index: i };
1420 1586 carousel.slides.push( slide );
1421 1587 }
@@ -1422,9 +1588,9 @@
1422 1588 } );
1423 1589 }
1424 1590
1425 1591 function loadSwiper( gallery, options ) {
1426 - if ( ! window.Swiper670 ) {
1592 + if ( ! window.JetpackSwiper ) {
1427 1593 var loader = document.querySelector( '#jp-carousel-loading-overlay' );
1428 1594 domUtil.show( loader );
1429 1595 var jsScript = document.createElement( 'script' );
1430 1596 jsScript.id = 'jetpack-carousel-swiper-js';
@@ -1499,9 +1665,9 @@
1499 1665 carousel.overlay.style.display = 'block';
1500 1666
1501 1667 initCarouselSlides( images, settings.startIndex );
1502 1668
1503 - swiper = new window.Swiper670( '.jp-carousel-swiper-container', {
1669 + swiper = new window.JetpackSwiper( '.jp-carousel-swiper-container', {
1504 1670 centeredSlides: true,
1505 1671 zoom: true,
1506 1672 loop: carousel.slides.length > 1,
1507 1673 // Turn off interactions and hide navigation arrows if there is only one slide.
@@ -1526,21 +1692,12 @@
1526 1692 threshold: 5,
1527 1693 } );
1528 1694
1529 1695 swiper.on( 'slideChange', function ( swiper ) {
1530 - var index;
1531 - // Swiper indexes slides from 1, plus when looping to left last slide ends up
1532 - // as 0 and looping to right first slide as total slides + 1. These are adjusted
1533 - // here to match index of carousel.slides.
1534 - if ( swiper.activeIndex === 0 ) {
1535 - index = carousel.slides.length - 1;
1536 - } else if ( swiper.activeIndex === carousel.slides.length + 1 ) {
1537 - index = 0;
1538 - } else {
1539 - index = swiper.activeIndex - 1;
1696 + if ( ! carousel.isOpen ) {
1697 + return;
1540 1698 }
1541 - selectSlideAtIndex( index );
1542 -
1699 + selectSlideAtIndex( swiper.realIndex );
1543 1700 carousel.overlay.classList.remove( 'jp-carousel-hide-controls' );
1544 1701 } );
1545 1702
1546 1703 swiper.on( 'zoomChange', function ( swiper, scale ) {
@@ -1575,10 +1732,86 @@
1575 1732 domUtil.emitEvent( carousel.overlay, 'jp_carousel.afterOpen' );
1576 1733 } );
1577 1734 }
1578 1735
1579 - // Register the event listener for starting the gallery
1580 - 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 ) {
1581 1814 var isCompatible =
1582 1815 window.CSS && window.CSS.supports && window.CSS.supports( 'display', 'grid' );
1583 1816
1584 1817 // IE11 support is being dropped in August 2021. The new swiper.js libray is not IE11 compat
@@ -1594,46 +1827,12 @@
1594 1827 if ( ! testForData( gallery ) ) {
1595 1828 return;
1596 1829 }
1597 1830
1598 - var parent = target.parentElement;
1599 - var grandparent = parent.parentElement;
1600 -
1601 - // If Gallery is made up of individual Image blocks check for custom link before
1602 - // loading carousel. The custom link may be the parent or could be a descendant
1603 - // of the parent if the image has rounded corners.
1604 - var parentHref = null;
1605 - if ( grandparent && grandparent.classList.contains( 'wp-block-image' ) ) {
1606 - parentHref = parent.getAttribute( 'href' );
1607 - } else if (
1608 - parent &&
1609 - parent.classList.contains( 'wp-block-image' ) &&
1610 - parent.querySelector( ':scope > a' )
1611 - ) {
1612 - parentHref = parent.querySelector( ':scope > a' ).getAttribute( 'href' );
1613 - }
1614 -
1615 - // If the link does not point to the attachment or media file then assume Image has
1616 - // a custom link so don't load the carousel.
1617 - if (
1618 - parentHref &&
1619 - parentHref.split( '?' )[ 0 ] !==
1620 - target.getAttribute( 'data-orig-file' ).split( '?' )[ 0 ] &&
1621 - parentHref !== target.getAttribute( 'data-permalink' )
1622 - ) {
1831 + if ( ! shouldOpenModal( target ) ) {
1623 1832 return;
1624 1833 }
1625 1834
1626 - // Do not open the modal if we are looking at a gallery caption from before WP5, which may contain a link.
1627 - if ( parent.classList.contains( 'gallery-caption' ) ) {
1628 - return;
1629 - }
1630 -
1631 - // Do not open the modal if we are looking at a caption of a gallery block, which may contain a link.
1632 - if ( domUtil.matches( parent, 'figcaption' ) ) {
1633 - return;
1634 - }
1635 -
1636 1835 // Set height to auto.
1637 1836 // Fix some themes where closing carousel brings view back to top.
1638 1837 document.documentElement.style.height = 'auto';
1639 1838
@@ -1646,9 +1845,9 @@
1646 1845 var item = domUtil.closest( target, itemSelector );
1647 1846 var index = Array.prototype.indexOf.call( gallery.querySelectorAll( itemSelector ), item );
1648 1847 loadSwiper( gallery, { startIndex: index } );
1649 1848 }
1650 - } );
1849 + }
1651 1850
1652 1851 // Handle lightbox (single image gallery) for images linking to 'Attachment Page'.
1653 1852 if ( Number( jetpackCarouselStrings.single_image_gallery ) === 1 ) {
1654 1853 processSingleImageGallery();