main
dependabot/npm_and_yarn/eslint-9.39.1
dependabot/npm_and_yarn/eslint-plugin-astro-1.5.0
dependabot/npm_and_yarn/eslint/js-9.39.1
dependabot/npm_and_yarn/katex-0.16.25
dependabot/npm_and_yarn/sharp-0.34.5
dependabot/npm_and_yarn/tailwindcss/vite-4.1.17
dependabot/npm_and_yarn/tsx-4.20.6
dependabot/npm_and_yarn/typescript-eslint-8.46.4
dependabot/npm_and_yarn/typescript-eslint/eslint-plugin-8.36.0
dependabot/npm_and_yarn/typescript-eslint/parser-8.46.4
main
| 1 | <div id="image-viewer" class="image-viewer"> |
| 2 | <img id="image-viewer-img" src="" alt="" /> |
| 3 | </div> |
| 4 | |
| 5 | <script> |
| 6 | function initImageViewer() { |
| 7 | const viewer = document.getElementById('image-viewer') |
| 8 | const viewerImg = document.getElementById('image-viewer-img') as HTMLImageElement |
| 9 | |
| 10 | if (!viewer || !viewerImg || viewerImg.tagName !== 'IMG') return |
| 11 | |
| 12 | // Display image in fullscreen viewer overlay |
| 13 | function showImage(src: string, alt?: string) { |
| 14 | if (viewerImg && src) { |
| 15 | viewerImg.src = src |
| 16 | viewerImg.alt = alt || '' |
| 17 | } |
| 18 | viewer?.classList.add('active') |
| 19 | document.body.classList.add('image-viewer-open') |
| 20 | document.body.style.overflow = 'hidden' |
| 21 | document.body.dataset.scrollY = window.scrollY.toString() |
| 22 | if (viewer) { |
| 23 | viewer.style.cursor = 'auto' |
| 24 | setTimeout(() => { |
| 25 | viewer.style.cursor = '' |
| 26 | }, 10) |
| 27 | } |
| 28 | } |
| 29 | |
| 30 | // Hide the image viewer and restore page scroll |
| 31 | function hideImage() { |
| 32 | viewer?.classList.remove('active') |
| 33 | document.body.classList.remove('image-viewer-open') |
| 34 | document.body.style.overflow = '' |
| 35 | setTimeout(() => { |
| 36 | if (viewer && !viewer.classList.contains('active') && viewerImg) { |
| 37 | viewerImg.src = '' |
| 38 | viewerImg.alt = '' |
| 39 | viewer.style.cursor = 'auto' |
| 40 | setTimeout(() => { |
| 41 | viewer.style.cursor = '' |
| 42 | }, 10) |
| 43 | } |
| 44 | }, 300) |
| 45 | } |
| 46 | |
| 47 | // Bind click events to images with data-preview="true" attribute |
| 48 | function bindImageClickEvents() { |
| 49 | const previewImages = document.querySelectorAll('img[data-preview="true"]') |
| 50 | previewImages.forEach((img) => { |
| 51 | const imgElement = img as HTMLImageElement |
| 52 | imgElement.style.cursor = 'zoom-in' |
| 53 | imgElement.addEventListener('click', (e) => { |
| 54 | e.preventDefault() |
| 55 | const target = e.target as HTMLImageElement |
| 56 | showImage(target.src, target.alt || '') |
| 57 | }) |
| 58 | }) |
| 59 | } |
| 60 | |
| 61 | viewer?.addEventListener('click', hideImage) |
| 62 | |
| 63 | // Prevent touch scroll in image viewer |
| 64 | viewer?.addEventListener( |
| 65 | 'touchmove', |
| 66 | (e) => { |
| 67 | if (viewer.classList.contains('active')) { |
| 68 | e.preventDefault() |
| 69 | } |
| 70 | }, |
| 71 | { passive: false } |
| 72 | ) |
| 73 | |
| 74 | document.addEventListener('keydown', (e) => { |
| 75 | if (e.key === 'Escape' && viewer.classList.contains('active')) { |
| 76 | hideImage() |
| 77 | } |
| 78 | }) |
| 79 | |
| 80 | bindImageClickEvents() |
| 81 | |
| 82 | const observer = new MutationObserver(() => { |
| 83 | bindImageClickEvents() |
| 84 | }) |
| 85 | |
| 86 | observer.observe(document.body, { |
| 87 | childList: true, |
| 88 | subtree: true |
| 89 | }) |
| 90 | } |
| 91 | |
| 92 | if (document.readyState === 'loading') { |
| 93 | document.addEventListener('DOMContentLoaded', initImageViewer) |
| 94 | } else { |
| 95 | initImageViewer() |
| 96 | } |
| 97 | |
| 98 | document.addEventListener('astro:page-load', initImageViewer) |
| 99 | </script> |
| 100 | |
| 101 | <style> |
| 102 | .image-viewer { |
| 103 | position: fixed; |
| 104 | top: 0; |
| 105 | left: 0; |
| 106 | width: 100%; |
| 107 | height: 100%; |
| 108 | z-index: 9999; |
| 109 | display: flex; |
| 110 | align-items: center; |
| 111 | justify-content: center; |
| 112 | opacity: 0; |
| 113 | visibility: hidden; |
| 114 | transition: all 0.15s ease-in-out; |
| 115 | background: color-mix(in srgb, var(--bg) 90%, transparent); |
| 116 | cursor: zoom-out; |
| 117 | } |
| 118 | |
| 119 | .image-viewer.active { |
| 120 | opacity: 1; |
| 121 | visibility: visible; |
| 122 | } |
| 123 | |
| 124 | .image-viewer img { |
| 125 | min-width: 45rem; |
| 126 | max-width: 60vw; |
| 127 | max-height: 80vh; |
| 128 | object-fit: contain; |
| 129 | cursor: zoom-out; |
| 130 | } |
| 131 | |
| 132 | @media (max-width: 768px) { |
| 133 | .image-viewer img { |
| 134 | min-width: 100vw; |
| 135 | } |
| 136 | } |
| 137 | |
| 138 | body.image-viewer-open { |
| 139 | overflow: hidden; |
| 140 | } |
| 141 | </style> |