|
| 1 | +/** |
| 2 | + * Augments the core lightbox overlay with a figcaption mounted inside |
| 3 | + * the *visible* enlarged-image container, so the caption sits flush |
| 4 | + * against the bottom edge of the picture frame — exactly the visual |
| 5 | + * contract of https://github.com/WordPress/gutenberg/pull/77477. |
| 6 | + * |
| 7 | + * Why "visible": core renders two `.lightbox-image-container` siblings. |
| 8 | + * One holds the small thumbnail used for the zoom animation hand-off, |
| 9 | + * the other holds the full-resolution image. Which one is on screen |
| 10 | + * depends on the lifecycle stage (opening, navigating, closing). We |
| 11 | + * pick the container whose <img> is actually inside the viewport at |
| 12 | + * sync time, mount the caption there, and let CSS pin it to its |
| 13 | + * parent's bottom — which is the bottom of the displayed picture. |
| 14 | + * |
| 15 | + * `data-wp-text` / `data-wp-bind` attributes added after the |
| 16 | + * Interactivity runtime parsed the page do not bind, so the caption is |
| 17 | + * driven imperatively via a MutationObserver on every overlay <img>. |
| 18 | + */ |
| 19 | + |
| 20 | +( function () { |
| 21 | + /** |
| 22 | + * Whether the element overlaps the current viewport, even partially. |
| 23 | + * |
| 24 | + * Partial intersection counts; the off-screen sibling sits fully |
| 25 | + * past the viewport and gets ruled out cleanly. |
| 26 | + * |
| 27 | + * @param {HTMLElement} el Element under test. |
| 28 | + * @return {boolean} |
| 29 | + */ |
| 30 | + function isInViewport( el ) { |
| 31 | + var rect = el.getBoundingClientRect(); |
| 32 | + var vh = window.innerHeight || document.documentElement.clientHeight; |
| 33 | + var vw = window.innerWidth || document.documentElement.clientWidth; |
| 34 | + return ( |
| 35 | + rect.bottom > 0 && |
| 36 | + rect.top < vh && |
| 37 | + rect.right > 0 && |
| 38 | + rect.left < vw |
| 39 | + ); |
| 40 | + } |
| 41 | + |
| 42 | + /** |
| 43 | + * Returns the lightbox image container whose <img> is currently on |
| 44 | + * screen, falling back to the first container when nothing matches. |
| 45 | + * |
| 46 | + * @param {HTMLElement} overlay The `.wp-lightbox-overlay` element. |
| 47 | + * @return {HTMLElement|null} |
| 48 | + */ |
| 49 | + function getVisibleContainer( overlay ) { |
| 50 | + var containers = overlay.querySelectorAll( '.lightbox-image-container' ); |
| 51 | + for ( var i = 0; i < containers.length; i++ ) { |
| 52 | + var img = containers[ i ].querySelector( 'img' ); |
| 53 | + if ( img && isInViewport( img ) ) { |
| 54 | + return containers[ i ]; |
| 55 | + } |
| 56 | + } |
| 57 | + // Fallback: first container — wrong-spot caption beats no caption at all. |
| 58 | + return containers[ 0 ] || null; |
| 59 | + } |
| 60 | + |
| 61 | + /** |
| 62 | + * Moves the caption node into the target container if it isn't |
| 63 | + * already attached there. |
| 64 | + * |
| 65 | + * @param {HTMLElement|null} target Container that should host the caption. |
| 66 | + * @param {HTMLElement} caption The figcaption element to move. |
| 67 | + */ |
| 68 | + function moveCaptionInto( target, caption ) { |
| 69 | + if ( target && caption.parentNode !== target ) { |
| 70 | + target.appendChild( caption ); |
| 71 | + } |
| 72 | + } |
| 73 | + |
| 74 | + /** |
| 75 | + * Returns the overlay's caption node, creating it on first call. |
| 76 | + * |
| 77 | + * The caption lives at the overlay level so it survives container |
| 78 | + * shuffles between open / close. It is moved into whichever |
| 79 | + * container is visible at sync time. |
| 80 | + * |
| 81 | + * @param {HTMLElement} overlay The `.wp-lightbox-overlay` element. |
| 82 | + * @return {HTMLElement} |
| 83 | + */ |
| 84 | + function ensureCaption( overlay ) { |
| 85 | + var caption = overlay.querySelector( 'figcaption.wp-lightbox-caption' ); |
| 86 | + if ( ! caption ) { |
| 87 | + caption = document.createElement( 'figcaption' ); |
| 88 | + caption.className = 'wp-lightbox-caption'; |
| 89 | + overlay.appendChild( caption ); |
| 90 | + } |
| 91 | + return caption; |
| 92 | + } |
| 93 | + |
| 94 | + /** |
| 95 | + * Reads the active image's alt text and writes it into the caption, |
| 96 | + * mounted under the visible container. |
| 97 | + * |
| 98 | + * @param {HTMLElement} overlay The `.wp-lightbox-overlay` element. |
| 99 | + */ |
| 100 | + function syncCaption( overlay ) { |
| 101 | + var caption = ensureCaption( overlay ); |
| 102 | + var target = getVisibleContainer( overlay ); |
| 103 | + moveCaptionInto( target, caption ); |
| 104 | + |
| 105 | + // Read alt off the visible image (the off-screen sibling holds stale state). |
| 106 | + var img = target ? target.querySelector( 'img' ) : null; |
| 107 | + var text = img ? img.getAttribute( 'alt' ) || '' : ''; |
| 108 | + caption.textContent = text; |
| 109 | + caption.classList.toggle( 'has-caption', text !== '' ); |
| 110 | + } |
| 111 | + |
| 112 | + /** |
| 113 | + * Boots the caption pipeline once the overlay is in the DOM. |
| 114 | + */ |
| 115 | + function init() { |
| 116 | + var overlay = document.querySelector( '.wp-lightbox-overlay' ); |
| 117 | + if ( ! overlay ) { |
| 118 | + return; |
| 119 | + } |
| 120 | + |
| 121 | + syncCaption( overlay ); |
| 122 | + |
| 123 | + /* |
| 124 | + * Watch every image inside the overlay; core flips alt/src on |
| 125 | + * the active one when the user clicks triggers or arrow-navs. |
| 126 | + */ |
| 127 | + var imgs = overlay.querySelectorAll( '.lightbox-image-container img' ); |
| 128 | + imgs.forEach( function ( img ) { |
| 129 | + var observer = new MutationObserver( function () { |
| 130 | + syncCaption( overlay ); |
| 131 | + } ); |
| 132 | + observer.observe( img, { attributes: true, attributeFilter: [ 'alt', 'src' ] } ); |
| 133 | + } ); |
| 134 | + |
| 135 | + /* |
| 136 | + * Re-sync when the overlay's active class flips (open / close |
| 137 | + * transitions can move which container is on screen). |
| 138 | + */ |
| 139 | + var classObserver = new MutationObserver( function () { |
| 140 | + syncCaption( overlay ); |
| 141 | + } ); |
| 142 | + classObserver.observe( overlay, { attributes: true, attributeFilter: [ 'class' ] } ); |
| 143 | + } |
| 144 | + |
| 145 | + if ( document.readyState === 'loading' ) { |
| 146 | + document.addEventListener( 'DOMContentLoaded', init ); |
| 147 | + } else { |
| 148 | + init(); |
| 149 | + } |
| 150 | +} )(); |
0 commit comments