1/* Athens Building Company â lightbox.js 2 Vanilla, dependency-free image lightbox. 3 - Trigger: any [data-lightbox-index] inside [data-lightbox-gallery] 4 - Keys: Arrow Left / Right navigates, Esc closes 5 - Click X to close, click backdrop to close 6 - Focus trap while open, restores focus on close 7*/ 8 9(function () { 10 'use strict'; 11 12 const gallery = document.querySelector('[data-lightbox-gallery]'); 13 const lightbox = document.getElementById('lightbox'); 14 if (!gallery || !lightbox) return; 15 16 const items = Array.from(gallery.querySelectorAll('[data-lightbox-index]')); 17 const sources = items.map(el => el.getAttribute('data-src')); 18 if (sources.length === 0) return; 19 20 const imgEl = lightbox.querySelector('.lightbox__img'); 21 const closeBtn = lightbox.querySelector('.lightbox__close'); 22 const prevBtn = lightbox.querySelector('.lightbox__nav--prev'); 23 const nextBtn = lightbox.querySelector('.lightbox__nav--next'); 24 const counter = lightbox.querySelector('.lightbox__counter'); 25 26 let currentIndex = 0; 27 let lastFocused = null; 28 29 function open(idx) { 30 currentIndex = idx; 31 lastFocused = document.activeElement; 32 lightbox.hidden = false; 33 document.body.style.overflow = 'hidden'; 34 requestAnimationFrame(() => lightbox.classList.add('is-open')); 35 update(); 36 // Send focus to close button so keyboard users start there 37 closeBtn.focus({ preventScroll: true }); 38 } 39 40 function close() { 41 lightbox.classList.remove('is-open'); 42 setTimeout(() => { 43 lightbox.hidden = true; 44 document.body.style.overflow = ''; 45 if (lastFocused && lastFocused.focus) lastFocused.focus({ preventScroll: true }); 46 }, 280); 47 } 48 49 function update() { 50 imgEl.src = sources[currentIndex]; 51 imgEl.alt = items[currentIndex].querySelector('img')?.alt || ''; 52 counter.textContent = `Photo ${currentIndex + 1} of ${sources.length}`; 53 // Disable nav at the ends? No â wrap around feels nicer in a portfolio. 54 } 55 56 function next() { 57 currentIndex = (currentIndex + 1) % sources.length; 58 update(); 59 } 60 61 function prev() { 62 currentIndex = (currentIndex - 1 + sources.length) % sources.length; 63 update(); 64 } 65 66 // Click triggers 67 items.forEach((item, idx) => { 68 item.addEventListener('click', () => open(idx)); 69 }); 70 71 closeBtn.addEventListener('click', close); 72 nextBtn.addEventListener('click', next); 73 prevBtn.addEventListener('click', prev); 74 75 // Click outside the image (on the backdrop) closes 76 lightbox.addEventListener('click', (e) => { 77 if (e.target === lightbox) close(); 78 }); 79 80 // Keyboard 81 document.addEventListener('keydown', (e) => { 82 if (lightbox.hidden) return; 83 if (e.key === 'Escape') { 84 e.preventDefault(); 85 close(); 86 } else if (e.key === 'ArrowRight') { 87 e.preventDefault(); 88 next(); 89 } else if (e.key === 'ArrowLeft') { 90 e.preventDefault(); 91 prev(); 92 } else if (e.key === 'Tab') { 93 // Simple focus trap â keep focus on the three controls 94 const focusables = [closeBtn, prevBtn, nextBtn]; 95 const idx = focusables.indexOf(document.activeElement); 96 if (idx === -1) { 97 e.preventDefault(); 98 closeBtn.focus(); 99 } else { 100 e.preventDefault(); 101 const dir = e.shiftKey ? -1 : 1; 102 const nextIdx = (idx + dir + focusables.length) % focusables.length; 103 focusables[nextIdx].focus(); 104 } 105 } 106 }); 107 108 // Preload neighbors for snappier nav 109 function preload(idx) { 110 if (idx < 0 || idx >= sources.length) return; 111 const im = new Image(); 112 im.src = sources[idx]; 113 } 114 // Preload all once the page is idle 115 if ('requestIdleCallback' in window) { 116 requestIdleCallback(() => sources.forEach((_, i) => preload(i))); 117 } 118})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.