// Gallery shortcode: opens the full image in a lightbox (a modal ) with its alt text // and credit. Arrow buttons, arrow keys and swiping move between the images of the gallery; // Escape, the close button or a click beside the image close it. { const button = (className, label, symbol) => { const element = document.createElement('button'); element.type = 'button'; element.className = `lightbox__button ${className}`; element.setAttribute('aria-label', label); element.innerHTML = ``; return element; }; const lightbox = (gallery) => { const links = [...gallery.querySelectorAll('.gallery__link')]; const labels = gallery.dataset; let index = 0; const dialog = document.createElement('dialog'); dialog.className = 'lightbox'; dialog.setAttribute('aria-label', labels.labelDialog); dialog.innerHTML = ` `; const close = button('lightbox__close', labels.labelClose, '×'); const previous = button('lightbox__previous', labels.labelPrevious, '‹'); const next = button('lightbox__next', labels.labelNext, '›'); dialog.append(close); if (links.length > 1) dialog.append(previous, next); document.body.append(dialog); const image = dialog.querySelector('.lightbox__image'); const preload = (link) => { new Image().src = link.href; }; const show = (newIndex) => { index = (newIndex + links.length) % links.length; const link = links[index]; image.src = link.href; image.width = link.dataset.width; image.height = link.dataset.height; image.alt = link.dataset.alt; // The alt text is shown as the caption, so screen readers get it once, from the image. dialog.querySelector('.lightbox__alt').textContent = link.dataset.alt; dialog.querySelector('.lightbox__alt').setAttribute('aria-hidden', 'true'); dialog.querySelector('.lightbox__credit').textContent = link.dataset.credit ?? ''; dialog.querySelector('.lightbox__counter').textContent = links.length > 1 ? `${index + 1} / ${links.length}` : ''; if (links.length > 1) { preload(links[(index + 1) % links.length]); preload(links[(index - 1 + links.length) % links.length]); } }; links.forEach((link, i) => { link.addEventListener('click', (event) => { // Let modified clicks open the image in a new tab or window as usual. if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return; event.preventDefault(); show(i); dialog.showModal(); close.focus(); }); }); // Closing goes through here, so that focus goes back to the thumbnail of the image that was // shown (the browser would restore it to where it was before the lightbox opened). const hide = () => { dialog.close(); links[index].focus(); }; close.addEventListener('click', hide); // Escape, or the back gesture on Android. dialog.addEventListener('cancel', (event) => { event.preventDefault(); hide(); }); previous.addEventListener('click', () => show(index - 1)); next.addEventListener('click', () => show(index + 1)); // A click on the dark area around the image closes the lightbox. dialog.addEventListener('click', (event) => { if (event.target === dialog || event.target.classList.contains('lightbox__figure')) hide(); }); dialog.addEventListener('keydown', (event) => { if (links.length < 2) return; if (event.key === 'ArrowLeft') show(index - 1); else if (event.key === 'ArrowRight') show(index + 1); else return; event.preventDefault(); }); // Swiping left or right on touch screens. let startX = null; dialog.addEventListener('pointerdown', (event) => { if (event.pointerType !== 'mouse') startX = event.clientX; }); dialog.addEventListener('pointerup', (event) => { if (startX === null || links.length < 2) return; const distance = event.clientX - startX; startX = null; if (Math.abs(distance) > 50) show(index + (distance < 0 ? 1 : -1)); }); }; document.querySelectorAll('[data-gallery]').forEach(lightbox); }