Files
Eidolon/src/components/ImageLightbox.astro
T

235 lines
7.7 KiB
Plaintext
Raw Normal View History

2026-08-12 01:11:32 +08:00
<script>
import 'photoswipe/style.css';
const imageSelector = '.prose img';
const excludedImageAncestors = '.mermaid, .katex, pre, [data-lightbox-ignore]';
let lightboxPromise: Promise<import('photoswipe/lightbox').default> | undefined;
const isLightboxImage = (image: HTMLImageElement, prose: HTMLElement) =>
prose.contains(image) && !image.closest(excludedImageAncestors) && !image.closest('a');
const getImages = (prose: HTMLElement) => [...prose.querySelectorAll<HTMLImageElement>('img')]
.filter((image) => isLightboxImage(image, prose));
const getSize = (image: HTMLImageElement) => {
const width = Number(image.dataset.originalWidth) || Number(image.getAttribute('width')) || image.naturalWidth || image.clientWidth || 1200;
const height = Number(image.dataset.originalHeight) || Number(image.getAttribute('height')) || image.naturalHeight || image.clientHeight || Math.round(width * 2 / 3);
return { width, height };
};
const open = async (image: HTMLImageElement, images: HTMLImageElement[]) => {
const index = images.indexOf(image);
const src = image.currentSrc || image.src;
if (index < 0 || !src) return;
const items = images.map((item) => ({
...getSize(item),
src: item.dataset.originalSrc || item.currentSrc || item.src,
msrc: item.currentSrc || item.src,
alt: item.alt,
element: item
2026-08-12 01:11:32 +08:00
}));
lightboxPromise ??= import('photoswipe/lightbox').then(({ default: PhotoSwipeLightbox }) => {
const lightbox = new PhotoSwipeLightbox({
pswpModule: () => import('photoswipe'),
bgClickAction: 'close',
wheelToZoom: true,
allowPanToNext: true,
pinchToClose: true,
showHideAnimationType: 'zoom',
showAnimationDuration: 360,
hideAnimationDuration: 300,
easing: 'cubic-bezier(0.22, 1, 0.36, 1)',
paddingFn: (viewportSize) => ({
top: 16,
right: 16,
bottom: viewportSize.x < 640 ? 78 : 92,
left: 16
})
});
lightbox.on('uiRegister', () => {
lightbox.pswp?.ui?.registerElement({
name: 'thumbnails',
className: 'pswp__thumbnails',
appendTo: 'root',
order: 20,
onInit: (element, pswp) => {
element.setAttribute('role', 'toolbar');
element.setAttribute('aria-label', '图片预览');
const buttons = Array.from({ length: pswp.getNumItems() }, (_, itemIndex) => {
const item = pswp.getItemData(itemIndex);
const button = document.createElement('button');
const thumbnail = document.createElement('img');
const label = item.alt ? `查看图片:${item.alt}` : `查看第 ${itemIndex + 1} 张图片`;
button.type = 'button';
button.className = 'pswp__thumbnail';
button.title = label;
button.setAttribute('aria-label', label);
button.addEventListener('click', () => pswp.goTo(itemIndex));
thumbnail.src = item.msrc || item.src || '';
thumbnail.alt = '';
thumbnail.draggable = false;
button.append(thumbnail);
element.append(button);
return button;
});
const updateCurrentThumbnail = () => {
buttons.forEach((button, itemIndex) => {
const isCurrent = itemIndex === pswp.currIndex;
button.classList.toggle('is-current', isCurrent);
if (isCurrent) {
button.setAttribute('aria-current', 'true');
element.scrollTo({
behavior: matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth',
left: button.offsetLeft - (element.clientWidth - button.clientWidth) / 2
});
} else {
button.removeAttribute('aria-current');
}
});
};
pswp.on('change', updateCurrentThumbnail);
updateCurrentThumbnail();
}
});
2026-08-12 01:11:32 +08:00
});
lightbox.init();
return lightbox;
});
try {
const lightbox = await lightboxPromise;
lightbox.loadAndOpen(index, items);
} catch (error) {
lightboxPromise = undefined;
console.error('Unable to open image lightbox', error);
}
};
const init = () => {
document.querySelectorAll<HTMLElement>('.prose').forEach((prose) => {
if (prose.dataset.lightboxReady === 'true') return;
prose.dataset.lightboxReady = 'true';
const prepareImage = (image: HTMLImageElement) => {
if (!isLightboxImage(image, prose)) return;
image.classList.add('lightbox-image');
image.tabIndex = image.tabIndex >= 0 ? image.tabIndex : 0;
image.setAttribute('role', 'button');
image.setAttribute('aria-label', image.alt ? `查看大图:${image.alt}` : '查看大图');
};
getImages(prose).forEach(prepareImage);
const observer = new MutationObserver(() => getImages(prose).forEach(prepareImage));
observer.observe(prose, { childList: true, subtree: true });
prose.addEventListener('click', (event) => {
const target = event.target;
if (!(target instanceof Element)) return;
const image = target.closest<HTMLImageElement>(imageSelector);
if (!image || !isLightboxImage(image, prose)) return;
event.preventDefault();
event.stopPropagation();
void open(image, getImages(prose));
});
prose.addEventListener('keydown', (event) => {
const target = event.target;
if (!(target instanceof Element)) return;
const image = target.closest<HTMLImageElement>(imageSelector);
if (!image || !isLightboxImage(image, prose) || (event.key !== 'Enter' && event.key !== ' ')) return;
event.preventDefault();
void open(image, getImages(prose));
});
});
};
init();
document.addEventListener('astro:page-load', init);
</script>
<style is:global>
.prose img.lightbox-image { cursor: zoom-in; }
.prose img.lightbox-image:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 4px; }
.pswp__thumbnails {
position: absolute;
z-index: 10;
right: 64px;
bottom: 12px;
left: 64px;
display: flex;
gap: 8px;
align-items: center;
width: max-content;
max-width: calc(100% - 128px);
height: 64px;
margin-inline: auto;
padding: 6px;
overflow-x: auto;
overscroll-behavior-x: contain;
scrollbar-width: none;
touch-action: pan-x;
}
.pswp__thumbnails::-webkit-scrollbar { display: none; }
.pswp__thumbnail {
flex: 0 0 64px;
width: 64px;
height: 52px;
padding: 2px;
overflow: hidden;
cursor: pointer;
background: rgb(20 20 20 / 72%);
border: 2px solid transparent;
border-radius: 4px;
opacity: .58;
transition: border-color 160ms ease, opacity 160ms ease, transform 160ms ease;
}
.pswp__thumbnail:hover { opacity: .88; }
.pswp__thumbnail.is-current {
border-color: #fff;
opacity: 1;
transform: translateY(-2px);
}
.pswp__thumbnail:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 2px;
}
.pswp__thumbnail img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 2px;
}
@media (max-width: 639px) {
.pswp__thumbnails {
right: 8px;
bottom: 8px;
left: 8px;
gap: 6px;
max-width: calc(100% - 16px);
height: 56px;
padding: 5px;
}
.pswp__thumbnail {
flex-basis: 54px;
width: 54px;
height: 44px;
}
}
@media (prefers-reduced-motion: reduce) {
.pswp__thumbnail { transition: none; }
}
2026-08-12 01:11:32 +08:00
</style>