|
| 1 | +// Blog image lightbox — zoom, drag, navigation, thumbnails |
| 2 | + |
| 3 | +export function initLightbox(signal: AbortSignal) { |
| 4 | + const lightbox = document.getElementById('lightbox'); |
| 5 | + const lbImg = document.getElementById('lightbox-img') as HTMLImageElement; |
| 6 | + const thumbsEl = document.getElementById('lightbox-thumbs'); |
| 7 | + if (!lightbox || !lbImg) return; |
| 8 | + |
| 9 | + const images = Array.from(document.querySelectorAll<HTMLImageElement>('.article-body img')); |
| 10 | + if (!images.length) return; |
| 11 | + |
| 12 | + let idx = 0; |
| 13 | + let scale = 1, tx = 0, ty = 0; |
| 14 | + let dragging = false, hasDragged = false; |
| 15 | + let dragStartX = 0, dragStartY = 0, startTx = 0, startTy = 0; |
| 16 | + |
| 17 | + // ── Thumbnails ── |
| 18 | + if (thumbsEl) { |
| 19 | + thumbsEl.innerHTML = ''; |
| 20 | + if (images.length > 1) { |
| 21 | + images.forEach((img, i) => { |
| 22 | + const thumb = document.createElement('img'); |
| 23 | + thumb.src = img.src; |
| 24 | + thumb.alt = ''; |
| 25 | + thumb.className = 'lb-thumb'; |
| 26 | + thumb.addEventListener('click', (e) => { e.stopPropagation(); showImage(i); }, { signal }); |
| 27 | + thumbsEl.appendChild(thumb); |
| 28 | + }); |
| 29 | + } |
| 30 | + } |
| 31 | + |
| 32 | + function updateThumbs() { |
| 33 | + if (!thumbsEl) return; |
| 34 | + thumbsEl.querySelectorAll('.lb-thumb').forEach((t, i) => { |
| 35 | + t.classList.toggle('active', i === idx); |
| 36 | + }); |
| 37 | + const active = thumbsEl.children[idx] as HTMLElement; |
| 38 | + if (active) active.scrollIntoView({ inline: 'center', block: 'nearest', behavior: 'smooth' }); |
| 39 | + } |
| 40 | + |
| 41 | + // ── Transform ── |
| 42 | + function apply() { |
| 43 | + lbImg.style.transform = `translate(${tx}px,${ty}px) scale(${scale})`; |
| 44 | + } |
| 45 | + function reset() { |
| 46 | + scale = 1; tx = 0; ty = 0; |
| 47 | + lbImg.style.transform = ''; |
| 48 | + lbImg.style.cursor = 'zoom-in'; |
| 49 | + } |
| 50 | + const isZoomed = () => scale > 1.05; |
| 51 | + |
| 52 | + // ── Navigation ── |
| 53 | + function showImage(i: number) { |
| 54 | + idx = i; |
| 55 | + lbImg.src = images[i].src; |
| 56 | + lbImg.alt = images[i].alt; |
| 57 | + reset(); |
| 58 | + updateThumbs(); |
| 59 | + const counter = document.getElementById('lightbox-counter'); |
| 60 | + if (counter) { |
| 61 | + if (images.length > 1) { |
| 62 | + counter.textContent = `${idx + 1} / ${images.length}`; |
| 63 | + counter.style.display = ''; |
| 64 | + } else { |
| 65 | + counter.style.display = 'none'; |
| 66 | + } |
| 67 | + } |
| 68 | + // Show/hide nav arrows |
| 69 | + const prev = document.getElementById('lightbox-prev'); |
| 70 | + const next = document.getElementById('lightbox-next'); |
| 71 | + if (prev) prev.style.display = images.length > 1 ? '' : 'none'; |
| 72 | + if (next) next.style.display = images.length > 1 ? '' : 'none'; |
| 73 | + } |
| 74 | + |
| 75 | + function open(i: number) { |
| 76 | + showImage(i); |
| 77 | + lightbox.classList.add('open'); |
| 78 | + document.body.style.overflow = 'hidden'; |
| 79 | + } |
| 80 | + |
| 81 | + function close() { |
| 82 | + lightbox.classList.remove('open'); |
| 83 | + document.body.style.overflow = ''; |
| 84 | + } |
| 85 | + |
| 86 | + function goPrev() { if (idx > 0) showImage(idx - 1); } |
| 87 | + function goNext() { if (idx < images.length - 1) showImage(idx + 1); } |
| 88 | + |
| 89 | + // ── Wheel zoom ── |
| 90 | + lightbox.addEventListener('wheel', (e) => { |
| 91 | + if (!lightbox.classList.contains('open')) return; |
| 92 | + e.preventDefault(); |
| 93 | + const factor = e.deltaY > 0 ? 0.9 : 1.1; |
| 94 | + scale = Math.min(Math.max(scale * factor, 0.5), 8); |
| 95 | + const rect = lbImg.getBoundingClientRect(); |
| 96 | + tx += (e.clientX - rect.left - rect.width / 2) * (1 - factor); |
| 97 | + ty += (e.clientY - rect.top - rect.height / 2) * (1 - factor); |
| 98 | + if (!isZoomed()) { tx = 0; ty = 0; scale = 1; } |
| 99 | + lbImg.style.cursor = isZoomed() ? 'grab' : 'zoom-in'; |
| 100 | + apply(); |
| 101 | + }, { passive: false, signal }); |
| 102 | + |
| 103 | + // ── Drag pan ── |
| 104 | + lbImg.addEventListener('pointerdown', (e) => { |
| 105 | + if (!isZoomed()) return; |
| 106 | + e.preventDefault(); |
| 107 | + dragging = true; hasDragged = false; |
| 108 | + dragStartX = e.clientX; dragStartY = e.clientY; |
| 109 | + startTx = tx; startTy = ty; |
| 110 | + lbImg.style.cursor = 'grabbing'; |
| 111 | + lbImg.setPointerCapture(e.pointerId); |
| 112 | + }, { signal }); |
| 113 | + |
| 114 | + window.addEventListener('pointermove', (e) => { |
| 115 | + if (!dragging) return; |
| 116 | + const dx = e.clientX - dragStartX, dy = e.clientY - dragStartY; |
| 117 | + if (Math.abs(dx) > 3 || Math.abs(dy) > 3) hasDragged = true; |
| 118 | + tx = startTx + dx; ty = startTy + dy; |
| 119 | + apply(); |
| 120 | + }, { signal }); |
| 121 | + |
| 122 | + window.addEventListener('pointerup', () => { |
| 123 | + if (!dragging) return; |
| 124 | + dragging = false; |
| 125 | + lbImg.style.cursor = isZoomed() ? 'grab' : 'zoom-in'; |
| 126 | + }, { signal }); |
| 127 | + |
| 128 | + // ── Bind article images ── |
| 129 | + images.forEach((img, i) => { |
| 130 | + img.style.cursor = 'zoom-in'; |
| 131 | + img.addEventListener('click', () => open(i), { signal }); |
| 132 | + }); |
| 133 | + |
| 134 | + // ── Prev / Next buttons ── |
| 135 | + document.getElementById('lightbox-prev')?.addEventListener('click', (e) => { e.stopPropagation(); goPrev(); }, { signal }); |
| 136 | + document.getElementById('lightbox-next')?.addEventListener('click', (e) => { e.stopPropagation(); goNext(); }, { signal }); |
| 137 | + |
| 138 | + // ── Backdrop click ── |
| 139 | + lightbox.addEventListener('click', (e) => { |
| 140 | + if (hasDragged) { hasDragged = false; return; } |
| 141 | + const target = e.target as HTMLElement; |
| 142 | + // Close if clicking backdrop, image (when not zoomed), or counter |
| 143 | + if (target === lightbox || target.classList.contains('lightbox-main') || (target === lbImg && !isZoomed())) close(); |
| 144 | + }, { signal }); |
| 145 | + |
| 146 | + document.getElementById('lightbox-close')?.addEventListener('click', close, { signal }); |
| 147 | + |
| 148 | + // ── Keyboard ── |
| 149 | + document.addEventListener('keydown', (e) => { |
| 150 | + if (!lightbox.classList.contains('open')) return; |
| 151 | + if (e.key === 'Escape') close(); |
| 152 | + if (e.key === 'ArrowLeft') goPrev(); |
| 153 | + if (e.key === 'ArrowRight') goNext(); |
| 154 | + if (e.key === '0') { reset(); apply(); } |
| 155 | + }, { signal }); |
| 156 | +} |
0 commit comments