PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.5
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.5
16.3-a.5 16.3-a.7 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 All 506 releases
← All changes | modules/carousel/jetpack-carousel.js +294 -31 16.3-a.1 → 16.3-a.5 View file →
@@ -205,8 +205,14 @@
205 205 'ontouchstart' in window || ( window.DocumentTouch && document instanceof DocumentTouch )
206 206 );
207 207 }
208 208
209 + function prefersReducedMotion() {
210 + return (
211 + !! window.matchMedia && window.matchMedia( '(prefers-reduced-motion: reduce)' ).matches
212 + );
213 + }
214 +
209 215 function scrollToElement( el, container, callback ) {
210 216 if ( ! el || ! container ) {
211 217 if ( callback ) {
212 218 return callback();
@@ -298,8 +304,9 @@
298 304 convertToPlainText: convertToPlainText,
299 305 stripHTML: stripHTML,
300 306 emitEvent: emitEvent,
301 307 isTouch: isTouch,
308 + prefersReducedMotion: prefersReducedMotion,
302 309 };
303 310 } )();
304 311
305 312 /////////////////////////////////////
@@ -306,8 +313,14 @@
306 313 // Carousel implementation
307 314 /////////////////////////////////////
308 315 function init() {
309 316 var commentInterval;
317 + // Rendered comments per attachment, kept for the lifetime of one overlay session.
318 + var commentsCache = {};
319 + // Attachment IDs with a comments request in flight, to prevent overlapping fetches.
320 + var commentsFetching = {};
321 + // Comments the server returns per page; mirrors the `number` arg in get_attachment_comments().
322 + var COMMENTS_PER_PAGE = 10;
310 323 var screenPadding;
311 324 var originalOverflow;
312 325 var originalHOverflow;
313 326 var scrollPos;
@@ -601,8 +614,20 @@
601 614 return;
602 615 }
603 616 if ( response.comment_status === 'approved' ) {
604 617 updatePostResults( jetpackCarouselStrings.comment_approved, true );
618 + /*
619 + Reflect the new comment in the badge total straight away. The refetch
620 + below only returns the first page, so on attachments with a full page
621 + of comments it cannot see the increment on its own. Bump the slide the
622 + comment was posted to -- captured as `attachmentId` -- not whichever
623 + slide happens to be current when this async response lands.
624 + */
625 + var postedSlide = getSlideByAttachmentId( attachmentId );
626 + if ( postedSlide ) {
627 + postedSlide.attrs.commentsCount =
628 + ( parseInt( postedSlide.attrs.commentsCount, 10 ) || 0 ) + 1;
629 + }
605 630 } else if ( response.comment_status === 'unapproved' ) {
606 631 updatePostResults( jetpackCarouselStrings.comment_unapproved, true );
607 632 } else {
608 633 // 'deleted', 'spam', false
@@ -608,9 +633,13 @@
608 633 // 'deleted', 'spam', false
609 634 updatePostResults( jetpackCarouselStrings.comment_post_error, false );
610 635 }
611 636 clearCommentTextAreaValue();
612 - fetchComments( attachmentId );
637 + // The new comment invalidates whatever we cached for this attachment.
638 + delete commentsCache[ attachmentId ];
639 + // Force past the in-flight guard so a still-pending first page can't
640 + // suppress this refresh.
641 + fetchComments( attachmentId, undefined, true );
613 642 submit.value = jetpackCarouselStrings.post_comment;
614 643 domUtil.hide( spinner );
615 644 form.classList.remove( 'jp-carousel-is-disabled' );
616 645 } else {
@@ -679,8 +708,19 @@
679 708 if ( commentsContainer ) {
680 709 commentsContainer.classList.toggle( 'jp-carousel-show' );
681 710 if ( commentsContainer.classList.contains( 'jp-carousel-show' ) ) {
682 711 extraInfoContainer.classList.add( 'jp-carousel-show' );
712 + // The panel is only now on screen, so this is when the comments are worth fetching.
713 + var current = carousel.currentSlide;
714 + if ( current ) {
715 + var currentCache = commentsCache[ current.attrs.attachmentId ];
716 + if ( ! currentCache ) {
717 + fetchComments( current.attrs.attachmentId );
718 + } else if ( currentCache.hasMore ) {
719 + // Cached page is already shown; re-arm paging for the rest.
720 + scheduleNextCommentsPage( current.attrs.attachmentId, currentCache.nextOffset );
721 + }
722 + }
683 723 } else {
684 724 extraInfoContainer.classList.remove( 'jp-carousel-show' );
685 725 }
686 726 }
@@ -778,8 +818,17 @@
778 818 swiper.slideTo( index + 1 );
779 819 }
780 820 }
781 821
822 + function getSlideByAttachmentId( attachmentId ) {
823 + for ( var i = 0; i < carousel.slides.length; i++ ) {
824 + if ( carousel.slides[ i ].attrs.attachmentId === attachmentId ) {
825 + return carousel.slides[ i ];
826 + }
827 + }
828 + return null;
829 + }
830 +
782 831 function selectSlideAtIndex( index ) {
783 832 if ( ! index || index < 0 || index > carousel.slides.length ) {
784 833 index = 0;
785 834 }
@@ -813,9 +862,9 @@
813 862 updateFullSizeLink( current );
814 863
815 864 if ( Number( jetpackCarouselStrings.display_comments ) === 1 ) {
816 865 testCommentsOpened( carousel.slides[ index ].attrs.commentsOpened );
817 - fetchComments( attachmentId );
866 + showCommentsForSlide( current );
818 867 domUtil.hide( carousel.info.querySelector( '#jp-carousel-comment-post-results' ) );
819 868 }
820 869
821 870 // Update pagination in footer.
@@ -862,8 +911,10 @@
862 911 carousel.isOpen = false;
863 912 swiper.destroy();
864 913 // Clear slide data for DOM garbage collection.
865 914 carousel.slides = [];
915 + commentsCache = {};
916 + commentsFetching = {};
866 917 carousel.currentSlide = undefined;
867 918 carousel.gallery.innerHTML = '';
868 919
869 920 domUtil.fadeOut( carousel.overlay, function () {
@@ -955,8 +1006,21 @@
955 1006 args.maxWidth = args.maxWidth * window.devicePixelRatio;
956 1007 args.maxHeight = args.maxHeight * window.devicePixelRatio;
957 1008 }
958 1009
1010 + /*
1011 + `maxWidth`/`maxHeight` now describe the device resolution. Anything beyond that is
1012 + headroom for zooming, so only the zoom request asks for it. The multiplier is folded
1013 + in here -- ahead of the size checks below -- so the zoom rendition keeps its extra
1014 + resolution even when the unzoomed one is already covered by `data-large-file`.
1015 + Without it, every slide would fetch ~4x the pixels the screen can show.
1016 + */
1017 + var headroom = args.zoomHeadroom || 1;
1018 + if ( headroom > 1 ) {
1019 + args.maxWidth = args.maxWidth * headroom;
1020 + args.maxHeight = args.maxHeight * headroom;
1021 + }
1022 +
959 1023 if ( largeWidth >= args.maxWidth || largeHeight >= args.maxHeight ) {
960 1024 return args.largeFile;
961 1025 }
962 1026
@@ -972,11 +1036,10 @@
972 1036
973 1037 // If we have a really large image load a smaller version
974 1038 // that is closer to the viewable size
975 1039 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;
1040 + args.origMaxWidth = args.maxWidth;
1041 + args.origMaxHeight = args.maxHeight;
979 1042 // Add the fit arg to the list of Photon args.
980 1043 sanitizedUrl.searchParams.set( 'fit', args.origMaxWidth + ',' + args.origMaxHeight );
981 1044 }
982 1045
@@ -1214,22 +1277,111 @@
1214 1277 domUtil.fadeOut( commentForm );
1215 1278 }
1216 1279 }
1217 1280
1218 - function fetchComments( attachmentId, offset ) {
1219 - var shouldClear = offset === undefined;
1220 - var commentsIndicator = carousel.info.querySelector(
1281 + function isCommentsPanelOpen() {
1282 + var wrapper = carousel.info.querySelector( '.jp-carousel-comments-wrapper' );
1283 + return !! wrapper && wrapper.classList.contains( 'jp-carousel-show' );
1284 + }
1285 +
1286 + function updateCommentsIndicator( count ) {
1287 + var indicator = carousel.info.querySelector(
1221 1288 '.jp-carousel-icon-comments .jp-carousel-has-comments-indicator'
1222 1289 );
1223 1290
1224 - commentsIndicator.classList.remove( 'jp-carousel-show' );
1291 + if ( ! indicator ) {
1292 + return;
1293 + }
1225 1294
1295 + if ( count > 0 ) {
1296 + indicator.innerText = count;
1297 + indicator.classList.add( 'jp-carousel-show' );
1298 + } else {
1299 + indicator.classList.remove( 'jp-carousel-show' );
1300 + }
1301 + }
1302 +
1303 + /**
1304 + * Show what we already know about a slide's comments, and fetch the rest only if the
1305 + * comments panel is actually on screen. The comment list used to be re-fetched from
1306 + * admin-ajax on every single slide change, even though it is hidden by default.
1307 + * @param {object} slide - The slide being shown.
1308 + */
1309 + function showCommentsForSlide( slide ) {
1310 + var attachmentId = slide.attrs.attachmentId;
1311 + var comments = carousel.info.querySelector( '.jp-carousel-comments' );
1312 + var cached = commentsCache[ attachmentId ];
1313 +
1226 1314 clearInterval( commentInterval );
1315 + domUtil.hide( carousel.info.querySelector( '#jp-carousel-comments-loading' ) );
1227 1316
1317 + if ( cached ) {
1318 + comments.innerHTML = cached.html;
1319 + updateCommentsIndicator( cached.count );
1320 + domUtil.show( comments );
1321 + /*
1322 + Resume paging if the cached run ended on a full page, so returning to a slide
1323 + does not strand the comments past the first page. Only while the panel is on
1324 + screen, mirroring the fresh-fetch path below.
1325 + */
1326 + if ( cached.hasMore && isCommentsPanelOpen() ) {
1327 + scheduleNextCommentsPage( attachmentId, cached.nextOffset );
1328 + }
1329 + return;
1330 + }
1331 +
1332 + comments.innerHTML = '';
1333 + domUtil.hide( comments );
1334 + // The server tells us the count up front, so the badge costs no request.
1335 + updateCommentsIndicator( parseInt( slide.attrs.commentsCount, 10 ) || 0 );
1336 +
1337 + if ( isCommentsPanelOpen() ) {
1338 + fetchComments( attachmentId );
1339 + }
1340 + }
1341 +
1342 + /**
1343 + * Fetch the next page of comments once the reader scrolls near the bottom.
1344 + * @param {string} attachmentId - The attachment whose comments are shown.
1345 + * @param {number} nextOffset - Offset of the page to load next.
1346 + */
1347 + function scheduleNextCommentsPage( attachmentId, nextOffset ) {
1348 + clearInterval( commentInterval );
1349 + commentInterval = setInterval( function () {
1350 + if ( carousel.container.scrollTop + 150 > window.innerHeight ) {
1351 + clearInterval( commentInterval );
1352 + fetchComments( attachmentId, nextOffset );
1353 + }
1354 + }, 300 );
1355 + }
1356 +
1357 + function fetchComments( attachmentId, offset, force ) {
1358 + var shouldClear = offset === undefined;
1359 +
1360 + clearInterval( commentInterval );
1361 +
1228 1362 if ( ! attachmentId ) {
1229 1363 return;
1230 1364 }
1231 1365
1366 + /*
1367 + The comments UI is shared and only ever shows the current slide, so a request for
1368 + any other slide (e.g. a post-submit refetch after the reader has navigated on) must
1369 + not touch it.
1370 + */
1371 + if ( ! carousel.currentSlide || carousel.currentSlide.attrs.attachmentId !== attachmentId ) {
1372 + return;
1373 + }
1374 +
1375 + /*
1376 + One request per attachment at a time, so a reopen mid-flight can't double-load.
1377 + `force` (a post-submit refresh) supersedes instead: the newest request is recorded
1378 + below and any older one's response bails on the identity check.
1379 + */
1380 + if ( commentsFetching[ attachmentId ] && ! force ) {
1381 + return;
1382 + }
1383 +
1232 1384 if ( ! offset || offset < 1 ) {
1233 1385 offset = 0;
1234 1386 }
1235 1387
@@ -1242,8 +1394,16 @@
1242 1394 comments.innerHTML = '';
1243 1395 }
1244 1396
1245 1397 var xhr = new XMLHttpRequest();
1398 + // Record this as the live request for the attachment; a later `force` fetch replaces it.
1399 + commentsFetching[ attachmentId ] = xhr;
1400 +
1401 + // True while `xhr` is still the attachment's live request (not superseded).
1402 + var isLiveRequest = function () {
1403 + return commentsFetching[ attachmentId ] === xhr;
1404 + };
1405 +
1246 1406 var url =
1247 1407 jetpackCarouselStrings.ajaxurl +
1248 1408 '?action=get_attachment_comments' +
1249 1409 '&nonce=' +
@@ -1254,14 +1414,40 @@
1254 1414 offset;
1255 1415 xhr.open( 'GET', url );
1256 1416 xhr.setRequestHeader( 'X-Requested-With', 'XMLHttpRequest' );
1257 1417
1258 - var onError = function () {
1418 + // Reveal the comments and clear the loading state; callers gate this themselves.
1419 + var revealComments = function () {
1259 1420 domUtil.fadeIn( comments );
1260 1421 domUtil.fadeOut( commentsLoading );
1261 1422 };
1262 1423
1424 + // Network-level failure (fires without onload); guard it like a late response.
1425 + var onError = function () {
1426 + // A newer (forced) request has superseded this one; leave the UI to it.
1427 + if ( ! isLiveRequest() ) {
1428 + return;
1429 + }
1430 + delete commentsFetching[ attachmentId ];
1431 +
1432 + // Only touch the shared UI if this attachment is still on screen.
1433 + if (
1434 + ! carousel.currentSlide ||
1435 + carousel.currentSlide.attrs.attachmentId !== attachmentId
1436 + ) {
1437 + return;
1438 + }
1439 + revealComments();
1440 + };
1441 +
1263 1442 xhr.onload = function () {
1443 + // A newer (forced) request has superseded this one; drop its result.
1444 + if ( ! isLiveRequest() ) {
1445 + return;
1446 + }
1447 + // The request is done, whatever the outcome below.
1448 + delete commentsFetching[ attachmentId ];
1449 +
1264 1450 // Ignore the results if they arrive late and we're now on a different slide.
1265 1451 if (
1266 1452 ! carousel.currentSlide ||
1267 1453 carousel.currentSlide.attrs.attachmentId !== attachmentId
@@ -1277,9 +1463,11 @@
1277 1463 // Do nothing.
1278 1464 }
1279 1465
1280 1466 if ( ! isSuccess || ! data || ! Array.isArray( data ) ) {
1281 - return onError();
1467 + // Already past the identity/current-slide guards above.
1468 + revealComments();
1469 + return;
1282 1470 }
1283 1471
1284 1472 if ( shouldClear ) {
1285 1473 comments.innerHTML = '';
@@ -1286,8 +1474,13 @@
1286 1474 }
1287 1475
1288 1476 for ( var i = 0; i < data.length; i++ ) {
1289 1477 var entry = data[ i ];
1478 + // Skip anything already on screen: a page can be requested twice if the
1479 + // reader re-opens a slide while its previous request is still in flight.
1480 + if ( comments.querySelector( '#jp-carousel-comment-' + entry.id ) ) {
1481 + continue;
1482 + }
1290 1483 var comment = document.createElement( 'div' );
1291 1484 comment.classList.add( 'jp-carousel-comment' );
1292 1485 comment.setAttribute( 'id', 'jp-carousel-comment-' + entry.id );
1293 1486 comment.innerHTML =
@@ -1303,25 +1496,46 @@
1303 1496 '</div>' +
1304 1497 entry.content +
1305 1498 '</div>';
1306 1499 comments.appendChild( comment );
1500 + }
1307 1501
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 );
1502 + /*
1503 + A full page back means there may be more; watch for a scroll to the bottom to
1504 + load the next one. A short page means we have reached the end.
1505 + */
1506 + var hasMore = data.length >= COMMENTS_PER_PAGE;
1507 + var nextOffset = offset + COMMENTS_PER_PAGE;
1508 + if ( hasMore ) {
1509 + scheduleNextCommentsPage( attachmentId, nextOffset );
1316 1510 }
1317 1511
1512 + /*
1513 + The endpoint returns at most one page, so prefer the server's total where we
1514 + have it and only count what we rendered as a fallback.
1515 + */
1516 + var rendered = comments.querySelectorAll( '.jp-carousel-comment' ).length;
1517 + var count = Math.max(
1518 + parseInt( carousel.currentSlide.attrs.commentsCount, 10 ) || 0,
1519 + rendered
1520 + );
1521 +
1318 1522 if ( data.length > 0 ) {
1319 1523 domUtil.show( comments );
1320 - commentsIndicator.innerText = data.length;
1321 - commentsIndicator.classList.add( 'jp-carousel-show' );
1524 + updateCommentsIndicator( count );
1322 1525 }
1323 1526
1527 + /*
1528 + Keep what we rendered for as long as the overlay is open, so going back to a
1529 + slide costs nothing -- including where to resume paging from.
1530 + */
1531 + commentsCache[ attachmentId ] = {
1532 + html: comments.innerHTML,
1533 + count: count,
1534 + hasMore: hasMore,
1535 + nextOffset: nextOffset,
1536 + };
1537 +
1324 1538 domUtil.hide( commentsLoading );
1325 1539 };
1326 1540
1327 1541 xhr.onerror = onError;
@@ -1365,10 +1579,16 @@
1365 1579
1366 1580 fullImage.addEventListener(
1367 1581 'load',
1368 1582 function () {
1369 - // Cached by this point, so swapping it in is effectively instant.
1370 - image.src = attrs.src;
1583 + /*
1584 + If the visitor zoomed while this was still downloading, a larger rendition
1585 + is already in place -- don't downgrade it back to the fit-to-screen src.
1586 + */
1587 + if ( ! image.hasAttribute( 'data-zoom-loaded' ) ) {
1588 + // Cached by this point, so swapping it in is effectively instant.
1589 + image.src = attrs.src;
1590 + }
1371 1591 image.style.filter = '';
1372 1592 },
1373 1593 { once: true }
1374 1594 );
@@ -1383,8 +1603,30 @@
1383 1603
1384 1604 fullImage.src = attrs.src;
1385 1605 }
1386 1606
1607 + /**
1608 + * Swap in a higher-resolution rendition now that the visitor is zoomed in.
1609 + *
1610 + * The zoomed-out slide only ever loads what the screen can actually show. The browser
1611 + * keeps painting the image it already has until this one decodes, so the picture
1612 + * sharpens rather than blanking.
1613 + * @param {object} slide - The slide being zoomed.
1614 + */
1615 + function loadZoomImage( slide ) {
1616 + if ( ! slide || ! slide.attrs.zoomSrc || slide.attrs.zoomSrc === slide.attrs.src ) {
1617 + return;
1618 + }
1619 +
1620 + var image = slide.el.querySelector( 'img' );
1621 + if ( ! image || image.hasAttribute( 'data-zoom-loaded' ) ) {
1622 + return;
1623 + }
1624 +
1625 + image.setAttribute( 'data-zoom-loaded', 1 );
1626 + image.src = slide.attrs.zoomSrc;
1627 + }
1628 +
1387 1629 function preloadAdjacentImages( currentIndex ) {
1388 1630 var indicesToPreload = [];
1389 1631 var totalSlides = carousel.slides.length;
1390 1632
@@ -1511,8 +1753,9 @@
1511 1753 var attrs = {
1512 1754 originalElement: item,
1513 1755 attachmentId: attrID,
1514 1756 commentsOpened: item.getAttribute( 'data-comments-opened' ) || '0',
1757 + commentsCount: item.getAttribute( 'data-comments-count' ) || '0',
1515 1758 imageMeta: domUtil.getJSONAttribute( item, 'data-image-meta' ) || {},
1516 1759 title: item.getAttribute( 'data-image-title' ) || '',
1517 1760 desc: item.getAttribute( 'data-image-description' ) || '',
1518 1761 largeFile: item.getAttribute( 'data-large-file' ) || '',
@@ -1538,16 +1781,28 @@
1538 1781
1539 1782 if ( typeof wpcom !== 'undefined' && wpcom.carousel && wpcom.carousel.generateImgSrc ) {
1540 1783 attrs.src = wpcom.carousel.generateImgSrc( item, max );
1541 1784 } 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 - } );
1785 + var urlArgs = function ( zoomHeadroom ) {
1786 + // A fresh object each time: selectBestImageUrl() mutates what it is given.
1787 + return {
1788 + origFile: attrs.src,
1789 + origWidth: attrs.origWidth,
1790 + origHeight: attrs.origHeight,
1791 + maxWidth: max.width,
1792 + maxHeight: max.height,
1793 + largeFile: attrs.largeFile,
1794 + zoomHeadroom: zoomHeadroom,
1795 + };
1796 + };
1797 +
1798 + /*
1799 + The zoom rendition is kept aside until the visitor actually zooms. On sources
1800 + that cannot serve a larger one it comes back identical to `src`, and nothing
1801 + ever happens.
1802 + */
1803 + attrs.zoomSrc = selectBestImageUrl( urlArgs( 2 ) );
1804 + attrs.src = selectBestImageUrl( urlArgs( 1 ) );
1550 1805 }
1551 1806
1552 1807 // Set the final src.
1553 1808 item.setAttribute( 'data-gallery-src', attrs.src );
@@ -1658,8 +1913,10 @@
1658 1913 }
1659 1914
1660 1915 domUtil.emitEvent( carousel.overlay, 'jp_carousel.beforeOpen' );
1661 1916 carousel.gallery.innerHTML = '';
1917 + commentsCache = {};
1918 + commentsFetching = {};
1662 1919
1663 1920 // Need to set the overlay manually to block or swiper does't initialise properly.
1664 1921 carousel.overlay.style.opacity = 1;
1665 1922 carousel.overlay.style.display = 'block';
@@ -1667,8 +1924,13 @@
1667 1924 initCarouselSlides( images, settings.startIndex );
1668 1925
1669 1926 swiper = new window.JetpackSwiper( '.jp-carousel-swiper-container', {
1670 1927 centeredSlides: true,
1928 + /*
1929 + Swiper's 300ms default is most of what makes slide-to-slide feel slow. Keep a
1930 + visible movement -- the slide is a real affordance on touch -- but a shorter one.
1931 + */
1932 + speed: domUtil.prefersReducedMotion() ? 0 : 250,
1671 1933 zoom: true,
1672 1934 loop: carousel.slides.length > 1,
1673 1935 // Turn off interactions and hide navigation arrows if there is only one slide.
1674 1936 enabled: carousel.slides.length > 1,
@@ -1701,8 +1963,9 @@
1701 1963 } );
1702 1964
1703 1965 swiper.on( 'zoomChange', function ( swiper, scale ) {
1704 1966 if ( scale > 1 ) {
1967 + loadZoomImage( carousel.currentSlide );
1705 1968 carousel.overlay.classList.add( 'jp-carousel-hide-controls' );
1706 1969 }
1707 1970
1708 1971 if ( scale === 1 ) {