diff --git a/wordpress.org/public_html/wp-content/plugins/gallery-lightbox-enhancements/assets/lightbox-captions.css b/wordpress.org/public_html/wp-content/plugins/gallery-lightbox-enhancements/assets/lightbox-captions.css index 2ddd30920c..2883d85a46 100644 --- a/wordpress.org/public_html/wp-content/plugins/gallery-lightbox-enhancements/assets/lightbox-captions.css +++ b/wordpress.org/public_html/wp-content/plugins/gallery-lightbox-enhancements/assets/lightbox-captions.css @@ -1,14 +1,10 @@ /** * Caption styling for the core image lightbox overlay. * - * Polyfill for https://github.com/WordPress/gutenberg/pull/77477. - * - * Caption is mounted inside the visible `.lightbox-image-container` - * (the JS picks the one whose is on screen) so it pins to the - * bottom edge of the picture frame, not the bottom of the viewport. - * The visual treatment matches the PR #77477 reference exactly: - * white text with a soft drop shadow on a barely-there bottom-up - * scrim, anchored flush to the image. + * The gallery keeps the core lightbox animation and navigation but + * renders the caption as a dedicated bar beneath the active image. + * This avoids competing with text-heavy screenshots while still keeping + * short captions compact and visually centered. */ /** @@ -25,52 +21,72 @@ } /* - * Do NOT set `.lightbox-image-container` to `position: relative`. Core - * stacks its two containers (zoom-thumbnail + full-res) as absolutely - * positioned, overlapping siblings; forcing them into normal flow stacks - * the images vertically and renders the picture twice once both hold a - * real image (e.g. plugin screenshots). The absolute container is already - * the caption's containing block — no override needed. + * The caption stays at the overlay level. Core already exposes the + * active image dimensions through CSS custom properties on the overlay, + * so the bar can align itself below the current image without changing + * the lightbox container flow. */ -.wp-lightbox-overlay .lightbox-image-container > figcaption.wp-lightbox-caption { +.wp-lightbox-overlay > figcaption.wp-lightbox-caption { position: absolute; - bottom: 0; - left: 0; - right: 0; + top: var(--wporg-lightbox-caption-top, calc(50% + ( var(--wp--lightbox-container-height) / 2 ) + 0.5rem)); + left: 50%; + transform: translateX(-50%); z-index: 3000001; + box-sizing: border-box; + width: min(var(--wporg-lightbox-caption-width, var(--wp--lightbox-container-width)), calc(100% - 1rem)); + max-width: calc(100% - 1rem); margin: 0; - padding: 3.5em 1.25em 1em; - color: #fff; - text-align: start; - font-size: 0.95rem; - font-weight: 500; - line-height: 1.45; - letter-spacing: 0.005em; - text-shadow: 0 1px 2px rgba(0, 0, 0, 0.55); - - /* Almost invisible scrim — present only to guarantee contrast on - bright photos. The text-shadow does most of the legibility work. */ - background: linear-gradient(to top, rgba(0, 0, 0, 0.45) 0%, rgba(0, 0, 0, 0) 100%); + padding: 0.75rem 1rem; + color: #1e1e1e; + text-align: center; + font-size: 0.875rem; + font-weight: 400; + line-height: 1.4; + letter-spacing: 0; + text-shadow: none; + background: rgba(255, 255, 255, 0.98); + border: 0; + border-radius: 12px; + overflow-wrap: anywhere; pointer-events: none; opacity: 0; - transition: opacity 160ms ease-in; + transition: opacity 180ms ease-out; } -.wp-lightbox-overlay.active .lightbox-image-container > figcaption.wp-lightbox-caption.has-caption { +.wp-lightbox-overlay.active > figcaption.wp-lightbox-caption.has-caption.is-ready { opacity: 1; - transition: opacity 220ms ease-out 200ms; } /* Hide the caption when there is no text to show. */ -.wp-lightbox-overlay .lightbox-image-container > figcaption.wp-lightbox-caption:not(.has-caption) { +.wp-lightbox-overlay > figcaption.wp-lightbox-caption:not(.has-caption) { visibility: hidden; } +@media (min-width: 960px) { + + .wp-lightbox-overlay > figcaption.wp-lightbox-caption { + top: var(--wporg-lightbox-caption-top, calc(50% + ( var(--wp--lightbox-container-height) / 2 ) + 0.75rem)); + padding: 0.875rem 1.25rem; + font-size: 0.95rem; + } +} + +@media (max-width: 480px), (max-height: 700px) { + + .wp-lightbox-overlay > figcaption.wp-lightbox-caption { + top: var(--wporg-lightbox-caption-top, calc(50% + ( var(--wp--lightbox-container-height) / 2 ) + 0.375rem)); + padding: 0.625rem 0.875rem; + font-size: 0.8125rem; + border-radius: 10px; + } + +} + @media (prefers-reduced-motion: reduce) { - .wp-lightbox-overlay .lightbox-image-container > figcaption.wp-lightbox-caption, - .wp-lightbox-overlay.active .lightbox-image-container > figcaption.wp-lightbox-caption.has-caption { + .wp-lightbox-overlay > figcaption.wp-lightbox-caption, + .wp-lightbox-overlay.active > figcaption.wp-lightbox-caption.has-caption { transition: none; } } diff --git a/wordpress.org/public_html/wp-content/plugins/gallery-lightbox-enhancements/assets/lightbox-captions.js b/wordpress.org/public_html/wp-content/plugins/gallery-lightbox-enhancements/assets/lightbox-captions.js index 158d58cfe7..b5cf1df2ad 100644 --- a/wordpress.org/public_html/wp-content/plugins/gallery-lightbox-enhancements/assets/lightbox-captions.js +++ b/wordpress.org/public_html/wp-content/plugins/gallery-lightbox-enhancements/assets/lightbox-captions.js @@ -1,16 +1,12 @@ /** - * Augments the core lightbox overlay with a figcaption mounted inside - * the *visible* enlarged-image container, so the caption sits flush - * against the bottom edge of the picture frame — exactly the visual - * contract of https://github.com/WordPress/gutenberg/pull/77477. + * Augments the core lightbox overlay with a figcaption mounted at the + * overlay level, beneath the active image. * - * Why "visible": core renders two center-overlapping - * `.lightbox-image-container` siblings. One holds the small thumbnail - * used for the zoom animation hand-off, the other the full-resolution - * image, which paints on top. We mount the caption into that top - * container — the last one on screen — so it pins to the bottom of the - * displayed picture and is not occluded by the enlarged image, then let - * CSS anchor it to the absolutely-positioned container frame. + * Core still owns the zoom animation, navigation buttons, and the + * active image container geometry. This layer only reads the active + * image's caption text, renders a dedicated caption bar below the + * image, and switches the text alignment when the rendered caption + * grows beyond a short-label treatment. * * `data-wp-text` / `data-wp-bind` attributes added after the * Interactivity runtime parsed the page do not bind, so the caption is @@ -18,6 +14,9 @@ */ ( function () { + var CAPTION_REVEAL_DELAY = 430; + var SCREENSHOT_ID_OFFSET = 9000000; + /** * Whether the element overlaps the current viewport, even partially. * @@ -57,29 +56,723 @@ return containers[ i ]; } } - // Fallback: last container — wrong-spot caption beats no caption at all. + // Fallback to the last container because a misplaced caption is better than none. return containers[ containers.length - 1 ] || null; } /** - * Moves the caption node into the target container if it isn't - * already attached there. + * Keeps the caption as a direct child of the overlay so it can sit in + * a dedicated bar beneath the active image without disturbing core's + * overlapping lightbox-image containers. + * + * @param {HTMLElement} overlay The `.wp-lightbox-overlay` element. + * @param {HTMLElement} caption The figcaption element to move. + */ + function moveCaptionIntoOverlay( overlay, caption ) { + var insertBefore = overlay.querySelector( '.screen-reader-text, .scrim' ); + if ( caption.parentNode !== overlay ) { + overlay.insertBefore( caption, insertBefore || null ); + } + } + + /** + * Extracts the attachment-style id from a lightbox image class. + * + * Screenshot gallery images use a high synthetic id range so they can be + * distinguished from ordinary site images that should keep core behavior. + * + * @param {HTMLImageElement|null} img Lightbox image element. + * @return {number} + */ + function getImageBlockId( img ) { + var match; + + if ( ! img || ! img.className ) { + return 0; + } + + match = img.className.match( /\bwp-image-(\d+)\b/ ); + + if ( ! match ) { + return 0; + } + + return parseInt( match[1], 10 ) || 0; + } + + /** + * Whether the current lightbox image belongs to the Plugin Directory + * screenshots gallery rather than some other site lightbox. + * + * @param {HTMLImageElement|null} img Lightbox image element. + * @return {boolean} + */ + function isManagedImage( img ) { + return getImageBlockId( img ) >= SCREENSHOT_ID_OFFSET; + } + + /** + * Whether the active overlay currently shows a managed screenshot image. + * + * @param {HTMLElement} overlay The `.wp-lightbox-overlay` element. + * @return {boolean} + */ + function isManagedOverlay( overlay ) { + var target = getVisibleContainer( overlay ); + var img = target ? target.querySelector( 'img' ) : null; + + return isManagedImage( img ); + } + + /** + * Returns a stable key for the active managed screenshot. + * + * @param {HTMLElement} overlay The `.wp-lightbox-overlay` element. + * @return {string} + */ + function getActiveImageKey( overlay ) { + var target = getVisibleContainer( overlay ); + var img = target ? target.querySelector( 'img' ) : null; + + if ( ! isManagedImage( img ) ) { + return ''; + } + + return [ + getImageBlockId( img ), + img.currentSrc || img.src || img.getAttribute( 'src' ) || '', + ].join( '|' ); + } + + /** + * Whether the lightbox overlay is currently active. + * + * @param {HTMLElement} overlay The `.wp-lightbox-overlay` element. + * @return {boolean} + */ + function isOverlayActive( overlay ) { + return overlay.classList.contains( 'active' ); + } + + /** + * Whether the overlay has finished its current transition. + * + * @param {HTMLElement} overlay The `.wp-lightbox-overlay` element. + * @return {boolean} + */ + function isOverlaySettled( overlay ) { + return overlay.dataset.wporgCaptionSettled === '1'; + } + + /** + * Marks the overlay as still transitioning. + * + * @param {HTMLElement} overlay The `.wp-lightbox-overlay` element. + */ + function markOverlayUnsettled( overlay ) { + overlay.dataset.wporgCaptionSettled = '0'; + } + + /** + * Marks the overlay as stable enough for geometry-sensitive caption work. + * + * @param {HTMLElement} overlay The `.wp-lightbox-overlay` element. + */ + function markOverlaySettled( overlay ) { + overlay.dataset.wporgCaptionSettled = '1'; + } + + /** + * Returns whether the first caption reveal is still waiting for core zoom. + * + * @param {HTMLElement} overlay The `.wp-lightbox-overlay` element. + * @return {boolean} + */ + function isCaptionRevealPending( overlay ) { + return overlay.dataset.wporgCaptionRevealPending === '1'; + } + + /** + * Marks the caption as waiting for the first reveal. + * + * @param {HTMLElement} overlay The `.wp-lightbox-overlay` element. + */ + function markCaptionRevealPending( overlay ) { + overlay.dataset.wporgCaptionRevealPending = '1'; + overlay.dataset.wporgCaptionRevealKey = getActiveImageKey( overlay ); + } + + /** + * Clears the first reveal waiting state. + * + * @param {HTMLElement} overlay The `.wp-lightbox-overlay` element. + */ + function clearCaptionRevealPending( overlay ) { + delete overlay.dataset.wporgCaptionRevealPending; + delete overlay.dataset.wporgCaptionRevealKey; + } + + /** + * Clears cached core lightbox dimensions so the next settled sync starts + * from the current image rather than a stale transition frame. + * + * @param {HTMLElement} overlay The `.wp-lightbox-overlay` element. + */ + function clearOverlayMetricCache( overlay ) { + delete overlay.dataset.wporgLightboxBaseKey; + delete overlay.dataset.wporgLightboxContainerWidth; + delete overlay.dataset.wporgLightboxContainerHeight; + delete overlay.dataset.wporgLightboxImageWidth; + delete overlay.dataset.wporgLightboxImageHeight; + delete overlay.dataset.wporgLightboxScale; + delete overlay.dataset.wporgLightboxGeometryApplied; + } + + /** + * Clears any pending deferred caption sync. + * + * @param {HTMLElement} overlay The `.wp-lightbox-overlay` element. + */ + function clearDeferredSync( overlay ) { + if ( overlay.wporgCaptionSyncTimer ) { + window.clearTimeout( overlay.wporgCaptionSyncTimer ); + overlay.wporgCaptionSyncTimer = null; + } + } + + /** + * Clears any pending caption reveal. + * + * @param {HTMLElement} overlay The `.wp-lightbox-overlay` element. + */ + function clearCaptionReveal( overlay ) { + if ( overlay.wporgCaptionRevealTimer ) { + window.clearTimeout( overlay.wporgCaptionRevealTimer ); + overlay.wporgCaptionRevealTimer = null; + } + + clearCaptionRevealPending( overlay ); + } + + /** + * Reveals the caption after layout-sensitive sizing has settled. + * + * Delaying the opacity flip avoids showing the caption while the + * image and caption widths are still being recomputed. * - * @param {HTMLElement|null} target Container that should host the caption. - * @param {HTMLElement} caption The figcaption element to move. + * @param {HTMLElement} overlay The `.wp-lightbox-overlay` element. + * @param {HTMLElement} caption The figcaption element. */ - function moveCaptionInto( target, caption ) { - if ( target && caption.parentNode !== target ) { - target.appendChild( caption ); + function scheduleCaptionReveal( overlay, caption ) { + clearCaptionReveal( overlay ); + + if ( ! isOverlayActive( overlay ) || ! caption.classList.contains( 'has-caption' ) ) { + return; } + + markCaptionRevealPending( overlay ); + + overlay.wporgCaptionRevealTimer = window.setTimeout( function () { + overlay.wporgCaptionRevealTimer = null; + clearCaptionRevealPending( overlay ); + + if ( ! isOverlayActive( overlay ) || ! caption.classList.contains( 'has-caption' ) ) { + return; + } + + caption.classList.add( 'is-ready' ); + }, 24 ); + } + + /** + * Hides the caption without touching the current lightbox geometry. + * + * We intentionally leave core sizing variables as-is during the close + * animation so the zoom-back transition does not snap. + * + * @param {HTMLElement} overlay The `.wp-lightbox-overlay` element. + * @param {HTMLElement} caption The figcaption element. + */ + function hideCaption( overlay, caption ) { + clearCaptionReveal( overlay ); + caption.classList.remove( 'has-caption', 'is-long-caption', 'is-ready' ); + caption.style.removeProperty( '--wporg-lightbox-caption-width' ); + caption.style.removeProperty( '--wporg-lightbox-caption-top' ); + caption.style.removeProperty( '--wporg-lightbox-caption-max-height' ); + } + + /** + * Returns the number of rendered text lines inside the caption. + * + * The CSS keeps a stable width for the caption bar, so the rendered + * line count reflects the real lightbox layout on both desktop and + * narrow mobile viewports. + * + * @param {HTMLElement} caption The figcaption element. + * @return {number} + */ + function getRenderedLineCount( caption ) { + var styles = window.getComputedStyle( caption ); + var lineHeight = parseFloat( styles.lineHeight ); + var paddingTop = parseFloat( styles.paddingTop ) || 0; + var paddingBottom = parseFloat( styles.paddingBottom ) || 0; + + if ( ! lineHeight ) { + return 0; + } + + return Math.max( + 0, + ( caption.getBoundingClientRect().height - paddingTop - paddingBottom ) / lineHeight + ); + } + + /** + * Whether two rectangles overlap. + * + * @param {DOMRect} a First rectangle. + * @param {DOMRect|null} b Second rectangle. + * @return {boolean} + */ + function rectsOverlap( a, b ) { + return !! b && ! ( + a.right <= b.left || + a.left >= b.right || + a.bottom <= b.top || + a.top >= b.bottom + ); + } + + /** + * Reads a numeric lightbox size variable off the overlay. + * + * @param {HTMLElement} overlay The `.wp-lightbox-overlay` element. + * @param {string} variableName CSS custom property name. + * @return {number} + */ + function getOverlayMetric( overlay, variableName ) { + var inlineValue = overlay.style.getPropertyValue( variableName ); + var computedValue = inlineValue || window.getComputedStyle( overlay ).getPropertyValue( variableName ); + var parsedValue = parseFloat( computedValue ); + + return Number.isFinite( parsedValue ) ? parsedValue : 0; + } + + /** + * Stores the current lightbox image dimensions for the active image and viewport. + * + * Core recalculates these values when the user navigates between images or + * resizes the viewport. We cache the current set so the caption logic can + * temporarily shrink the visible image and still restore the original values + * before the next measurement pass. + * + * @param {HTMLElement} overlay The `.wp-lightbox-overlay` element. + * @param {HTMLImageElement|null} img The active lightbox image. + */ + function cacheOverlayMetrics( overlay, img ) { + var key = ( img ? img.getAttribute( 'src' ) || '' : '' ) + + '@' + window.innerWidth + 'x' + window.innerHeight; + + if ( overlay.dataset.wporgLightboxBaseKey === key ) { + return; + } + + overlay.dataset.wporgLightboxBaseKey = key; + overlay.dataset.wporgLightboxContainerWidth = String( + getOverlayMetric( overlay, '--wp--lightbox-container-width' ) + ); + overlay.dataset.wporgLightboxContainerHeight = String( + getOverlayMetric( overlay, '--wp--lightbox-container-height' ) + ); + overlay.dataset.wporgLightboxImageWidth = String( + getOverlayMetric( overlay, '--wp--lightbox-image-width' ) + ); + overlay.dataset.wporgLightboxImageHeight = String( + getOverlayMetric( overlay, '--wp--lightbox-image-height' ) + ); + overlay.dataset.wporgLightboxScale = String( + getOverlayMetric( overlay, '--wp--lightbox-scale' ) + ); + } + + /** + * Restores the lightbox image dimensions captured from core. + * + * @param {HTMLElement} overlay The `.wp-lightbox-overlay` element. + */ + function resetOverlayMetrics( overlay ) { + var metrics = { + '--wp--lightbox-container-width': overlay.dataset.wporgLightboxContainerWidth, + '--wp--lightbox-container-height': overlay.dataset.wporgLightboxContainerHeight, + '--wp--lightbox-image-width': overlay.dataset.wporgLightboxImageWidth, + '--wp--lightbox-image-height': overlay.dataset.wporgLightboxImageHeight, + '--wp--lightbox-scale': overlay.dataset.wporgLightboxScale, + }; + + Object.keys( metrics ).forEach( function ( variableName ) { + if ( ! metrics[ variableName ] ) { + return; + } + + if ( variableName === '--wp--lightbox-scale' ) { + overlay.style.setProperty( variableName, metrics[ variableName ] ); + return; + } + + overlay.style.setProperty( variableName, metrics[ variableName ] + 'px' ); + } ); + } + + /** + * Reads the rendered image dimensions from the active lightbox image. + * + * @param {HTMLElement} overlay The `.wp-lightbox-overlay` element. + * @param {HTMLImageElement|null} img The active lightbox image. + * @return {{width:number,height:number}} + */ + function getRenderedImageMetrics( overlay, img ) { + var overlayWidth = getOverlayMetric( overlay, '--wp--lightbox-image-width' ); + var overlayHeight = getOverlayMetric( overlay, '--wp--lightbox-image-height' ); + var imageRect = img ? img.getBoundingClientRect() : null; + + if ( overlayWidth && overlayHeight ) { + return { + width: overlayWidth, + height: overlayHeight, + }; + } + + return { + width: imageRect ? imageRect.width : getOverlayMetric( overlay, '--wp--lightbox-image-width' ), + height: imageRect ? imageRect.height : getOverlayMetric( overlay, '--wp--lightbox-image-height' ), + }; + } + + /** + * Whether the active screenshot is portrait-shaped or otherwise very narrow. + * + * Those screenshots already get a wider caption bar and need the fitting + * math to use the intended lightbox geometry, not the current animation + * frame, otherwise the open transition can visibly jump mid-flight. + * + * @param {HTMLElement} overlay The `.wp-lightbox-overlay` element. + * @param {HTMLImageElement|null} img The active lightbox image. + * @return {boolean} + */ + function isPortraitOrNarrowImage( overlay, img ) { + var imageMetrics = getRenderedImageMetrics( overlay, img ); + var aspectRatio = imageMetrics.width ? imageMetrics.height / imageMetrics.width : 0; + + return aspectRatio > 1.2 || ( imageMetrics.width > 0 && imageMetrics.width < 320 ); + } + + /** + * Shrinks the lightbox image proportionally when a below-image caption would + * otherwise fall out of the viewport or into the navigation controls. + * + * @param {HTMLElement} overlay The `.wp-lightbox-overlay` element. + * @param {HTMLElement} caption The figcaption element. + * @param {HTMLImageElement|null} img The active lightbox image. + */ + function fitCaptionWithinViewport( overlay, caption, img ) { + var bottomPadding = 16; + var attempt = 0; + var minContainerScale = 0.5; + var nextButton = overlay.querySelector( '.wp-lightbox-navigation-button-next' ); + var prevButton = overlay.querySelector( '.wp-lightbox-navigation-button-prev' ); + var maxCaptionBottom = window.innerHeight - bottomPadding; + var nextRect = nextButton ? nextButton.getBoundingClientRect() : null; + var prevRect = prevButton ? prevButton.getBoundingClientRect() : null; + var isPortraitOrNarrow = isPortraitOrNarrowImage( overlay, img ); + var overflow; + + if ( isPortraitOrNarrow ) { + if ( window.innerWidth <= 480 ) { + minContainerScale = 0.2; + } else if ( window.innerWidth <= 960 || window.innerHeight <= 700 ) { + minContainerScale = 0.24; + } else { + minContainerScale = 0.28; + } + } else if ( window.innerWidth <= 480 ) { + minContainerScale = 0.28; + } else if ( window.innerWidth <= 960 || window.innerHeight <= 700 ) { + minContainerScale = 0.35; + } + + overflow = Math.max( + 0, + caption.getBoundingClientRect().bottom - maxCaptionBottom + ); + + if ( rectsOverlap( caption.getBoundingClientRect(), nextRect ) ) { + overflow = Math.max( + overflow, + caption.getBoundingClientRect().bottom - ( nextRect.top - 12 ) + ); + } + + if ( rectsOverlap( caption.getBoundingClientRect(), prevRect ) ) { + overflow = Math.max( + overflow, + caption.getBoundingClientRect().bottom - ( prevRect.top - 12 ) + ); + } + + while ( overflow > 0 && attempt < 8 ) { + var currentContainerWidth = getOverlayMetric( overlay, '--wp--lightbox-container-width' ); + var currentContainerHeight = getOverlayMetric( overlay, '--wp--lightbox-container-height' ); + var currentImageWidth = getOverlayMetric( overlay, '--wp--lightbox-image-width' ); + var currentImageHeight = getOverlayMetric( overlay, '--wp--lightbox-image-height' ); + var currentLightboxScale = getOverlayMetric( overlay, '--wp--lightbox-scale' ); + var minContainerHeight = parseFloat( overlay.dataset.wporgLightboxContainerHeight ) * minContainerScale; + var newContainerHeight = Math.max( + minContainerHeight, + currentContainerHeight - ( overflow * 2 ) + ); + var scale; + + if ( + ! currentContainerWidth || + ! currentContainerHeight || + ! currentImageWidth || + ! currentImageHeight || + ! currentLightboxScale || + newContainerHeight === currentContainerHeight + ) { + return; + } + + scale = newContainerHeight / currentContainerHeight; + + overlay.style.setProperty( + '--wp--lightbox-container-width', + ( currentContainerWidth * scale ) + 'px' + ); + overlay.style.setProperty( + '--wp--lightbox-container-height', + newContainerHeight + 'px' + ); + overlay.style.setProperty( + '--wp--lightbox-image-width', + ( currentImageWidth * scale ) + 'px' + ); + overlay.style.setProperty( + '--wp--lightbox-image-height', + ( currentImageHeight * scale ) + 'px' + ); + overlay.style.setProperty( + '--wp--lightbox-scale', + currentLightboxScale / scale + ); + + overflow = Math.max( + 0, + caption.getBoundingClientRect().bottom - maxCaptionBottom + ); + + if ( rectsOverlap( caption.getBoundingClientRect(), nextRect ) ) { + overflow = Math.max( + overflow, + caption.getBoundingClientRect().bottom - ( nextRect.top - 12 ) + ); + } + + if ( rectsOverlap( caption.getBoundingClientRect(), prevRect ) ) { + overflow = Math.max( + overflow, + caption.getBoundingClientRect().bottom - ( prevRect.top - 12 ) + ); + } + + attempt++; + } + } + + /** + * Keeps portrait and very narrow screenshots readable by letting a + * text-heavy caption grow wider than the rendered image. + * + * Short labels stay aligned to the image width. Longer copy can use a + * wider bar when the screenshot itself is too narrow to carry a + * paragraph-shaped caption comfortably. + * + * @param {HTMLElement} overlay The `.wp-lightbox-overlay` element. + * @param {HTMLElement} caption The figcaption element. + * @param {HTMLImageElement|null} img The active lightbox image. + * @param {string} text The caption text. + * @param {boolean} promoteForLongCaption Whether a measured paragraph + * caption should get extra width. + */ + function applyCaptionWidth( overlay, caption, img, text, promoteForLongCaption ) { + var imageMetrics = getRenderedImageMetrics( overlay, img ); + var imageWidth = imageMetrics.width; + var viewportWidth = Math.max( 0, window.innerWidth - 16 ); + var isPortraitOrNarrow = isPortraitOrNarrowImage( overlay, img ); + var preferredWidth = imageWidth; + var relaxedWidth; + + if ( ! imageWidth || ! viewportWidth ) { + caption.style.removeProperty( '--wporg-lightbox-caption-width' ); + return; + } + + if ( isPortraitOrNarrow && ( text.length > 60 || promoteForLongCaption ) ) { + relaxedWidth = Math.max( + imageWidth, + Math.min( 420, imageWidth * 2 ), + 300 + ); + + if ( promoteForLongCaption ) { + relaxedWidth = Math.max( + relaxedWidth, + Math.min( 460, imageWidth * 2.3 ), + 340 + ); + } + + preferredWidth = Math.min( viewportWidth, relaxedWidth ); + } + + caption.style.setProperty( '--wporg-lightbox-caption-width', preferredWidth + 'px' ); + } + + /** + * Syncs caption width and long-text detection against the current + * rendered image size. + * + * The image may shrink after viewport-fitting, so this pass can be + * repeated after geometry changes to keep the final caption width in + * step with the visible screenshot. + * + * @param {HTMLElement} overlay The `.wp-lightbox-overlay` element. + * @param {HTMLElement} caption The figcaption element. + * @param {HTMLImageElement|null} img The active lightbox image. + * @param {string} text The caption text. + */ + function syncCaptionPresentation( overlay, caption, img, text ) { + var isLongCaption; + + caption.classList.remove( 'is-long-caption' ); + applyCaptionWidth( overlay, caption, img, text, false ); + + isLongCaption = isParagraphCaption( text, caption ); + if ( isLongCaption ) { + applyCaptionWidth( overlay, caption, img, text, true ); + isLongCaption = isParagraphCaption( text, caption ); + } + + caption.classList.toggle( 'is-long-caption', isLongCaption ); + } + + /** + * Decides whether the caption should be treated as long-form copy. + * + * Short product-style captions keep the same centered treatment even if a + * narrow mobile viewport wraps them. Longer descriptions still keep that + * centered treatment, but we track them separately for width heuristics. + * + * @param {string} text The caption text. + * @param {HTMLElement} caption The figcaption element. + * @return {boolean} + */ + function isParagraphCaption( text, caption ) { + var renderedLineCount = getRenderedLineCount( caption ); + + return renderedLineCount > 2.5 || ( text.length > 220 && renderedLineCount > 2 ); + } + + /** + * Reveals the caption after core's opening zoom has finished. + * + * Geometry is applied before the first visible zoom frame so the image + * does not jump at the end of the animation. Text waits until the zoom + * completes, keeping the opening motion focused on the screenshot. + * + * @param {HTMLElement} overlay The `.wp-lightbox-overlay` element. + */ + function scheduleDeferredSync( overlay ) { + clearDeferredSync( overlay ); + + if ( ! isOverlayActive( overlay ) ) { + return; + } + + markCaptionRevealPending( overlay ); + + overlay.wporgCaptionSyncTimer = window.setTimeout( function () { + overlay.wporgCaptionSyncTimer = null; + markOverlaySettled( overlay ); + clearOverlayMetricCache( overlay ); + syncCaption( overlay, false ); + scheduleCaptionReveal( overlay, ensureCaption( overlay ) ); + }, CAPTION_REVEAL_DELAY ); + } + + /** + * Queues a two-phase sync so captions do not mutate the lightbox geometry + * while core is still animating the current image. + * + * @param {HTMLElement} overlay The `.wp-lightbox-overlay` element. + */ + function requestSettledSync( overlay ) { + var shouldSyncImmediately = isOverlaySettled( overlay ); + + if ( ! isOverlayActive( overlay ) ) { + clearDeferredSync( overlay ); + markOverlayUnsettled( overlay ); + clearOverlayMetricCache( overlay ); + hideCaption( overlay, ensureCaption( overlay ) ); + return; + } + + if ( ! isManagedOverlay( overlay ) ) { + clearDeferredSync( overlay ); + markOverlayUnsettled( overlay ); + clearOverlayMetricCache( overlay ); + + if ( overlay.querySelector( 'figcaption.wp-lightbox-caption' ) ) { + hideCaption( overlay, ensureCaption( overlay ) ); + } + + return; + } + + if ( shouldSyncImmediately ) { + if ( isCaptionRevealPending( overlay ) ) { + var pendingKey = overlay.dataset.wporgCaptionRevealKey || ''; + var currentKey = getActiveImageKey( overlay ); + + clearOverlayMetricCache( overlay ); + syncCaption( overlay, false ); + + if ( currentKey !== pendingKey ) { + clearCaptionReveal( overlay ); + scheduleDeferredSync( overlay ); + } + + return; + } + + clearDeferredSync( overlay ); + clearCaptionReveal( overlay ); + clearOverlayMetricCache( overlay ); + syncCaption( overlay, true ); + return; + } + + clearOverlayMetricCache( overlay ); + syncCaption( overlay, false ); + scheduleDeferredSync( overlay ); } /** * Returns the overlay's caption node, creating it on first call. * * The caption lives at the overlay level so it survives container - * shuffles between open / close. It is moved into whichever - * container is visible at sync time. + * shuffles between open / close and can be positioned outside the + * image frame. * * @param {HTMLElement} overlay The `.wp-lightbox-overlay` element. * @return {HTMLElement} @@ -95,21 +788,86 @@ } /** - * Reads the active image's alt text and writes it into the caption, - * mounted under the visible container. + * Reads the active image's alt text and writes it into the caption + * bar below the visible image container. * * @param {HTMLElement} overlay The `.wp-lightbox-overlay` element. + * @param {boolean} keepReady Whether an already-visible caption should stay visible. */ - function syncCaption( overlay ) { - var caption = ensureCaption( overlay ); + function syncCaption( overlay, keepReady ) { var target = getVisibleContainer( overlay ); - moveCaptionInto( target, caption ); - // Read alt off the visible image (the off-screen sibling holds stale state). var img = target ? target.querySelector( 'img' ) : null; var text = img ? img.getAttribute( 'alt' ) || '' : ''; + var caption; + + if ( ! isOverlayActive( overlay ) ) { + clearDeferredSync( overlay ); + markOverlayUnsettled( overlay ); + clearOverlayMetricCache( overlay ); + caption = overlay.querySelector( 'figcaption.wp-lightbox-caption' ); + + if ( caption ) { + hideCaption( overlay, caption ); + } + + return; + } + + if ( ! isManagedImage( img ) ) { + clearDeferredSync( overlay ); + markOverlayUnsettled( overlay ); + clearOverlayMetricCache( overlay ); + caption = overlay.querySelector( 'figcaption.wp-lightbox-caption' ); + + if ( caption ) { + hideCaption( overlay, caption ); + } + + return; + } + + caption = ensureCaption( overlay ); + + if ( ! isOverlayActive( overlay ) ) { + clearDeferredSync( overlay ); + markOverlayUnsettled( overlay ); + clearOverlayMetricCache( overlay ); + hideCaption( overlay, caption ); + return; + } + + moveCaptionIntoOverlay( overlay, caption ); caption.textContent = text; - caption.classList.toggle( 'has-caption', text !== '' ); + + if ( text === '' ) { + hideCaption( overlay, caption ); + return; + } + + caption.classList.add( 'has-caption' ); + if ( ! keepReady ) { + caption.classList.remove( 'is-ready' ); + } + + if ( ! isOverlaySettled( overlay ) ) { + return; + } + + if ( overlay.dataset.wporgLightboxGeometryApplied !== '1' ) { + cacheOverlayMetrics( overlay, img ); + resetOverlayMetrics( overlay ); + syncCaptionPresentation( overlay, caption, img, text ); + fitCaptionWithinViewport( overlay, caption, img ); + syncCaptionPresentation( overlay, caption, img, text ); + overlay.dataset.wporgLightboxGeometryApplied = '1'; + } + + syncCaptionPresentation( overlay, caption, img, text ); + + if ( keepReady ) { + caption.classList.add( 'is-ready' ); + } } /** @@ -121,7 +879,8 @@ return; } - syncCaption( overlay ); + markOverlayUnsettled( overlay ); + requestSettledSync( overlay ); /* * Watch every image inside the overlay; core flips alt/src on @@ -130,7 +889,7 @@ var imgs = overlay.querySelectorAll( '.lightbox-image-container img' ); imgs.forEach( function ( img ) { var observer = new MutationObserver( function () { - syncCaption( overlay ); + requestSettledSync( overlay ); } ); observer.observe( img, { attributes: true, attributeFilter: [ 'alt', 'src' ] } ); } ); @@ -140,9 +899,24 @@ * transitions can move which container is on screen). */ var classObserver = new MutationObserver( function () { - syncCaption( overlay ); + if ( isOverlayActive( overlay ) ) { + requestSettledSync( overlay ); + return; + } + + requestSettledSync( overlay ); } ); classObserver.observe( overlay, { attributes: true, attributeFilter: [ 'class' ] } ); + + window.addEventListener( 'resize', function () { + requestSettledSync( overlay ); + } ); + + if ( window.visualViewport ) { + window.visualViewport.addEventListener( 'resize', function () { + requestSettledSync( overlay ); + } ); + } } if ( document.readyState === 'loading' ) { diff --git a/wordpress.org/public_html/wp-content/plugins/gallery-lightbox-enhancements/assets/masonry.css b/wordpress.org/public_html/wp-content/plugins/gallery-lightbox-enhancements/assets/masonry.css index 6c221657c9..f7b8dd1ba3 100644 --- a/wordpress.org/public_html/wp-content/plugins/gallery-lightbox-enhancements/assets/masonry.css +++ b/wordpress.org/public_html/wp-content/plugins/gallery-lightbox-enhancements/assets/masonry.css @@ -96,6 +96,11 @@ .wp-block-gallery.has-nested-images.is-style-masonry figure.wp-block-image:not(#individual-image) button.lightbox-trigger { cursor: zoom-in; + /* Limit hover work to opacity so tall masonry tiles do not visually + twitch when the trigger fades in. */ + -webkit-backdrop-filter: none; + backdrop-filter: none; + will-change: opacity; } /** diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/class-plugin-directory.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/class-plugin-directory.php index 105c678dfc..5a33a8bb26 100644 --- a/wordpress.org/public_html/wp-content/plugins/plugin-directory/class-plugin-directory.php +++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/class-plugin-directory.php @@ -468,8 +468,6 @@ public function register_shortcodes() { add_shortcode( Shortcodes\Release_Confirmation::SHORTCODE, array( __NAMESPACE__ . '\Shortcodes\Release_Confirmation', 'display' ) ); add_action( 'template_redirect', array( __NAMESPACE__ . '\Shortcodes\Release_Confirmation', 'template_redirect' ) ); - - add_filter( 'wp_resource_hints', array( __NAMESPACE__ . '\Shortcodes\Screenshots', 'add_resource_hints' ), 10, 2 ); } /** diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/css/screenshots.css b/wordpress.org/public_html/wp-content/plugins/plugin-directory/css/screenshots.css index 418085e036..95bade1d0c 100644 --- a/wordpress.org/public_html/wp-content/plugins/plugin-directory/css/screenshots.css +++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/css/screenshots.css @@ -217,6 +217,12 @@ #screenshots .wp-block-gallery.is-style-screenshots figure.wp-block-image button.lightbox-trigger { cursor: zoom-in; + /* Core's frosted trigger repaints the underlying thumbnail on hover. + Keep the control, but make it a plain opacity change for smoother + screenshot-grid interaction. */ + -webkit-backdrop-filter: none; + backdrop-filter: none; + will-change: opacity; } /** diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/js/screenshots.js b/wordpress.org/public_html/wp-content/plugins/plugin-directory/js/screenshots.js index 231d6bc6df..ae79dcee3b 100644 --- a/wordpress.org/public_html/wp-content/plugins/plugin-directory/js/screenshots.js +++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/js/screenshots.js @@ -77,9 +77,9 @@ /* * Some screenshots end up "loaded" by the browser but with zero - * natural dimensions — typically Photon returning an empty 200, - * or a partial / pending response that the network stack never - * settles. The load / error events don't always fire for those, + * natural dimensions, usually after a zero-byte or partial response + * that the network stack never settles. The load / error events + * don't always fire for those, * so re-sweep after a short delay and hide anything that's still * degenerate. The `markBroken` guard against `!complete` keeps * lazy figures past the fold safe from this sweep. diff --git a/wordpress.org/public_html/wp-content/plugins/plugin-directory/shortcodes/class-screenshots.php b/wordpress.org/public_html/wp-content/plugins/plugin-directory/shortcodes/class-screenshots.php index 75fbf13d73..fc4038465b 100644 --- a/wordpress.org/public_html/wp-content/plugins/plugin-directory/shortcodes/class-screenshots.php +++ b/wordpress.org/public_html/wp-content/plugins/plugin-directory/shortcodes/class-screenshots.php @@ -180,34 +180,6 @@ public static function strip_layout_classes( $content ) { return $processor->get_updated_html(); } - /** - * Adds preconnect / dns-prefetch hints to the Photon CDN host on - * single-plugin pages so the browser can warm up the TLS handshake - * while the page HTML is still streaming. Saves ~50–150 ms on the - * first thumbnail paint for cold visitors. Hooked from - * `class-plugin-directory.php` via the `wp_resource_hints` filter. - * - * @param array $urls Resource hint URLs already queued for $relation_type. - * @param string $relation_type One of preconnect / dns-prefetch / prerender / prefetch. - * @return array - */ - public static function add_resource_hints( $urls, $relation_type ) { - if ( ! is_singular( 'plugin' ) ) { - return $urls; - } - - if ( 'preconnect' === $relation_type ) { - $urls[] = array( - 'href' => 'https://i0.wp.com', - 'crossorigin' => 'anonymous', - ); - } elseif ( 'dns-prefetch' === $relation_type ) { - $urls[] = 'https://i0.wp.com'; - } - - return $urls; - } - /** * Enqueues the shortcode's own CSS and toggle script. Assets live * under the plugin's `css/` and `js/` directories alongside the @@ -363,14 +335,12 @@ protected static function build_image_block( $screenshot, $id, $above_fold = fal ) ); - $srcset = self::photon_srcset( $src ); - $class = 'wp-block-image size-large'; + $class = 'wp-block-image size-large'; // Record the full-resolution source and intrinsic dimensions for the - // lightbox-state repair in fix_lightbox_metadata(). The grid thumbnail - // loads a Photon-shrunk srcset candidate, so core (which has no real - // attachment to query) would otherwise enlarge that small image; this - // hands the lightbox the lossless original at its true size. + // lightbox-state repair in fix_lightbox_metadata(). Screenshot assets + // have no real attachment for core to query, so this hands the + // lightbox the original `ps.w.org` image at its true size. self::$lightbox_meta[ (int) $id ] = array( 'url' => $src, 'width' => ( is_array( $dimensions ) && ! empty( $dimensions[0] ) ) ? (int) $dimensions[0] : 'none', @@ -411,11 +381,10 @@ protected static function build_image_block( $screenshot, $id, $above_fold = fal $src ); $figure .= sprintf( - '%2$s', + '%2$s', $src, esc_attr( $alt ), $id, - $srcset, $dim_attrs, esc_attr( $priority ) ); @@ -449,11 +418,9 @@ protected static function build_image_block( $screenshot, $id, $above_fold = fal * * The empty `uploadedSrc` leaves the lightbox with no full-resolution * image to enlarge, and the `'none'` dimensions make core's view - * script fall back to the *thumbnail's* natural size — which on - * production is a Photon-shrunk srcset candidate (≤900px, often the - * 300px tile). The enlarged view therefore renders tiny. On - * environments without Photon the thumbnail is the full-resolution - * original, which is why the bug is invisible on local / staging. + * script fall back to the thumbnail's natural size. The rendered + * thumbnail may be constrained by layout, but the enlarged view should + * always use the original screenshot and its recorded dimensions. * * Core keys its lightbox metadata by a per-render `uniqid()` (exposed * on the figure's `data-wp-context`), not by the attachment id, so the @@ -461,9 +428,8 @@ protected static function build_image_block( $screenshot, $id, $above_fold = fal * rendered markup and re-set the affected fields. `wp_interactivity_state()` * merges with `array_replace_recursive()` (later call wins), and this * filter runs at priority 20 — after core's priority-15 pass — so the - * corrected values override the broken ones. `lightboxSrcset` is - * cleared so the enlarged image loads the lossless original rather than - * a capped Photon candidate. + * corrected values override the broken ones. `lightboxSrcset` is cleared + * so the enlarged image loads the original screenshot URL. * * @param string $block_content Rendered Image block markup. * @param array $parsed_block Parsed block, including `attrs['id']`. @@ -554,54 +520,6 @@ protected static function wrap_with_show_all_button( $rendered_gallery, $count ) . ''; } - /** - * Builds a Photon-powered `srcset` (and matching `sizes`) attribute string - * for a `ps.w.org` screenshot URL. Returns an empty string when the source - * URL is not on `ps.w.org`, so the original `src` is used unchanged. - * - * Plugin authors upload screenshots at full resolution but we render them - * inside a 3-column grid, so the browser otherwise downloads the full - * asset (often 300–800 KB) only to scale it down to a ~250 px tile. - * Routing the URL through `i0.wp.com` (Photon) returns a re-encoded, - * width-bound copy at ~10× smaller payload — see - * https://developer.wordpress.com/docs/photon/ for the resize/optim - * options. The grid thumbnail therefore loads a small Photon candidate; - * the lightbox is pointed back at the full-resolution `ps.w.org` original - * separately by {@see self::fix_lightbox_metadata()} so users still get - * the lossless image when they enlarge a screenshot. - * - * @param string $src Original asset URL. - * @return string Attribute fragment ready to interpolate into ``, - * including the leading space, or empty string. - */ - protected static function photon_srcset( $src ) { - if ( ! preg_match( '#^https?://ps\.w\.org/#', $src ) ) { - return ''; - } - - // Photon (i0.wp.com) only runs on production and staging. In local - // or other environments the proxy may not be reachable, which would - // leave the gallery silently empty until the cold cache warmed up. - // Fall back to the unoptimised `ps.w.org` URL there. - $env = function_exists( 'wp_get_environment_type' ) ? wp_get_environment_type() : 'production'; - if ( 'production' !== $env && 'staging' !== $env ) { - return ''; - } - - $photon_base = preg_replace( '#^https?://#', 'https://i0.wp.com/', $src ); - $widths = array( 300, 600, 900 ); - $srcset = array(); - - foreach ( $widths as $width ) { - $srcset[] = add_query_arg( 'w', $width, $photon_base ) . ' ' . $width . 'w'; - } - - return sprintf( - ' srcset="%1$s" sizes="(max-width: 599px) 50vw, 33vw"', - esc_attr( implode( ', ', $srcset ) ) - ); - } - /** * Whether the gallery should swap from brick masonry to row-aligned grid. *