the home site for me: also iteration 3 or 4 of my site

feat: move lightbox over to web components

dunkirk.sh 8cd4e7f4 481559f0

verified
+132 -72
+130 -70
static/js/lightbox.js
··· 1 - let currentLightboxImages = []; 2 - let currentLightboxIndex = 0; 3 4 - function openLightbox(src) { 5 - currentLightboxImages = [src]; 6 - currentLightboxIndex = 0; 7 - showLightbox(); 8 - } 9 10 - function openLightboxGroup(element) { 11 - const group = element.closest('.img-group'); 12 - const images = Array.from(group.querySelectorAll('img')).map(img => img.src); 13 - const clickedImg = element.querySelector('img'); 14 - 15 - currentLightboxImages = images; 16 - currentLightboxIndex = images.indexOf(clickedImg.src); 17 - showLightbox(); 18 - } 19 20 - function showLightbox() { 21 - let lightbox = document.getElementById('lightbox'); 22 - 23 - if (!lightbox) { 24 - lightbox = document.createElement('div'); 25 - lightbox.id = 'lightbox'; 26 - lightbox.innerHTML = ` 27 <div class="lightbox-content"> 28 - <button class="lightbox-close" onclick="closeLightbox()">&times;</button> 29 - <img id="lightbox-img" src="" alt=""> 30 <div class="lightbox-controls"> 31 - <button class="lightbox-prev" onclick="prevImage()">←</button> 32 - <button class="lightbox-next" onclick="nextImage()">→</button> 33 </div> 34 </div> 35 `; 36 - document.body.appendChild(lightbox); 37 - 38 - lightbox.addEventListener('click', (e) => { 39 - if (e.target === lightbox) closeLightbox(); 40 }); 41 - 42 - document.addEventListener('keydown', handleKeyPress); 43 } 44 - 45 - updateLightboxImage(); 46 - lightbox.style.display = 'flex'; 47 - document.body.style.overflow = 'hidden'; 48 - } 49 50 - function closeLightbox() { 51 - const lightbox = document.getElementById('lightbox'); 52 - if (lightbox) { 53 - lightbox.style.display = 'none'; 54 document.body.style.overflow = ''; 55 } 56 - } 57 58 - function updateLightboxImage() { 59 - const img = document.getElementById('lightbox-img'); 60 - const controls = document.querySelector('.lightbox-controls'); 61 - 62 - img.src = currentLightboxImages[currentLightboxIndex]; 63 - 64 - if (currentLightboxImages.length === 1) { 65 - controls.style.display = 'none'; 66 - } else { 67 - controls.style.display = 'flex'; 68 } 69 - } 70 71 - function prevImage() { 72 - currentLightboxIndex = (currentLightboxIndex - 1 + currentLightboxImages.length) % currentLightboxImages.length; 73 - updateLightboxImage(); 74 } 75 76 - function nextImage() { 77 - currentLightboxIndex = (currentLightboxIndex + 1) % currentLightboxImages.length; 78 - updateLightboxImage(); 79 } 80 81 - function handleKeyPress(e) { 82 - const lightbox = document.getElementById('lightbox'); 83 - if (!lightbox || lightbox.style.display !== 'flex') return; 84 - 85 - if (e.key === 'Escape') { 86 - closeLightbox(); 87 - } else if (e.key === 'ArrowLeft') { 88 - prevImage(); 89 - } else if (e.key === 'ArrowRight') { 90 - nextImage(); 91 } 92 }
··· 1 + class ImageLightbox extends HTMLElement { 2 + constructor() { 3 + super(); 4 + this.currentImages = []; 5 + this.currentIndex = 0; 6 + this.handleImageClick = this.handleImageClick.bind(this); 7 + this.handleKeyPress = this.handleKeyPress.bind(this); 8 + } 9 10 + connectedCallback() { 11 + this.render(); 12 + this.setupEventListeners(); 13 + } 14 15 + disconnectedCallback() { 16 + this.cleanup(); 17 + } 18 19 + render() { 20 + this.id = 'lightbox'; 21 + this.style.display = 'none'; 22 + this.innerHTML = ` 23 <div class="lightbox-content"> 24 + <button class="lightbox-close" aria-label="Close lightbox">&times;</button> 25 + <img id="lightbox-img" src="" alt="" /> 26 <div class="lightbox-controls"> 27 + <button class="lightbox-prev" aria-label="Previous image">←</button> 28 + <button class="lightbox-next" aria-label="Next image">→</button> 29 </div> 30 </div> 31 `; 32 + } 33 + 34 + setupEventListeners() { 35 + // Delegate clicks on images with data-lightbox attribute 36 + document.addEventListener('click', this.handleImageClick); 37 + 38 + // Lightbox controls 39 + this.querySelector('.lightbox-close').addEventListener('click', () => this.close()); 40 + this.querySelector('.lightbox-prev').addEventListener('click', () => this.prev()); 41 + this.querySelector('.lightbox-next').addEventListener('click', () => this.next()); 42 + 43 + // Click backdrop to close 44 + this.addEventListener('click', (e) => { 45 + if (e.target === this) this.close(); 46 }); 47 + 48 + // Keyboard navigation 49 + document.addEventListener('keydown', this.handleKeyPress); 50 } 51 + 52 + handleImageClick(e) { 53 + // Check if clicked element or its parent has data-lightbox 54 + const target = e.target.closest('[data-lightbox]'); 55 + if (!target) return; 56 + 57 + e.preventDefault(); 58 + 59 + const group = target.dataset.lightboxGroup; 60 + 61 + if (group) { 62 + // Find all images in the same group 63 + const groupElements = Array.from( 64 + document.querySelectorAll(`[data-lightbox-group="${group}"]`) 65 + ); 66 67 + // Extract image sources 68 + this.currentImages = groupElements.map(el => { 69 + const img = el.tagName === 'IMG' ? el : el.querySelector('img'); 70 + return img ? img.src : null; 71 + }).filter(Boolean); 72 + 73 + // Find the index of the clicked image 74 + this.currentIndex = groupElements.indexOf(target); 75 + } else { 76 + // Single image 77 + const img = target.tagName === 'IMG' ? target : target.querySelector('img'); 78 + this.currentImages = img ? [img.src] : []; 79 + this.currentIndex = 0; 80 + } 81 + 82 + if (this.currentImages.length > 0) { 83 + this.open(); 84 + } 85 + } 86 + 87 + handleKeyPress(e) { 88 + if (this.style.display !== 'flex') return; 89 + 90 + if (e.key === 'Escape') { 91 + this.close(); 92 + } else if (e.key === 'ArrowLeft') { 93 + this.prev(); 94 + } else if (e.key === 'ArrowRight') { 95 + this.next(); 96 + } 97 + } 98 + 99 + open() { 100 + this.style.display = 'flex'; 101 + const controls = this.querySelector('.lightbox-controls'); 102 + if (this.currentImages.length > 1) { 103 + controls.style.display = 'flex'; 104 + } else { 105 + controls.style.display = 'none'; 106 + } 107 + document.body.style.overflow = 'hidden'; 108 + this.updateImage(); 109 + } 110 + 111 + close() { 112 + this.style.display = 'none'; 113 document.body.style.overflow = ''; 114 } 115 116 + updateImage() { 117 + const img = this.querySelector('#lightbox-img'); 118 + img.src = this.currentImages[this.currentIndex]; 119 + } 120 + 121 + prev() { 122 + this.currentIndex = (this.currentIndex - 1 + this.currentImages.length) % this.currentImages.length; 123 + this.updateImage(); 124 + } 125 + 126 + next() { 127 + this.currentIndex = (this.currentIndex + 1) % this.currentImages.length; 128 + this.updateImage(); 129 } 130 131 + cleanup() { 132 + document.removeEventListener('click', this.handleImageClick); 133 + document.removeEventListener('keydown', this.handleKeyPress); 134 + document.body.style.overflow = ''; 135 + } 136 } 137 138 + customElements.define('image-lightbox', ImageLightbox); 139 + 140 + // Auto-initialize: add lightbox to page if not already present 141 + if (document.readyState === 'loading') { 142 + document.addEventListener('DOMContentLoaded', initLightbox); 143 + } else { 144 + initLightbox(); 145 } 146 147 + function initLightbox() { 148 + if (!document.querySelector('image-lightbox')) { 149 + const lightbox = document.createElement('image-lightbox'); 150 + document.body.appendChild(lightbox); 151 } 152 }
+1 -1
templates/shortcodes/img.html
··· 1 <figure {% if class %}class="{{class}}" {% else %}class="center" {% endif %}> 2 - <div class="img-container" onclick="openLightbox('{{id}}')"> 3 <img src="{{id}}" {% if alt %}alt="{{alt}}" {% endif %} /> 4 </div> 5 {% if caption %}
··· 1 <figure {% if class %}class="{{class}}" {% else %}class="center" {% endif %}> 2 + <div class="img-container" data-lightbox> 3 <img src="{{id}}" {% if alt %}alt="{{alt}}" {% endif %} /> 4 </div> 5 {% if caption %}
+1 -1
templates/shortcodes/imgs.html
··· 3 {% set images = id | split(pat=",") %} 4 {% set alts = alt | default(value="") | split(pat=",") %} 5 {% for image in images %} 6 - <div class="img-container" onclick="openLightboxGroup(this)"> 7 <img src="{{image | trim}}" {% if alts[loop.index0] %}alt="{{alts[loop.index0] | trim}}" {% endif %} /> 8 </div> 9 {% endfor %}
··· 3 {% set images = id | split(pat=",") %} 4 {% set alts = alt | default(value="") | split(pat=",") %} 5 {% for image in images %} 6 + <div class="img-container" data-lightbox data-lightbox-group="group-{{ id | slugify }}"> 7 <img src="{{image | trim}}" {% if alts[loop.index0] %}alt="{{alts[loop.index0] | trim}}" {% endif %} /> 8 </div> 9 {% endfor %}