| @@ -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 ) { |