export interface LightboxImage {
src: string;
alt: string;
width?: number;
height?: number;
origin?: HTMLElement;
class?: string;
}
class LightboxState {
images = $state.raw<LightboxImage[]>([]);
index = $state(0);
open = $state(false);
readonly current = $derived<LightboxImage | undefined>(this.images[this.index]);
show(images: LightboxImage[], index: number): void {
if (images.length === 0) return;
this.images = images;
this.index = Math.min(Math.max(index, 0), images.length - 1);
this.open = true;
}
close(): void {
this.open = false;
}
clear(): void {
if (this.open) return;
this.images = [];
this.index = 0;
}
step(delta: number): void {
const count = this.images.length;
if (count < 2) return;
this.index = (this.index + delta + count) % count;
}
}
export const lightbox = new LightboxState();
const zoomable = (image: HTMLImageElement): boolean => image.closest("a") === null;
const describe = (image: HTMLImageElement): LightboxImage => ({
src: image.currentSrc || image.src,
alt: image.alt,
width: image.naturalWidth || undefined,
height: image.naturalHeight || undefined,
origin: image
});
export const zoomMarkupImages = (): (() => void) => {
const onClick = (event: MouseEvent): void => {
if (event.defaultPrevented || event.button !== 0) return;
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
const image = event.target;
if (!(image instanceof HTMLImageElement) || !zoomable(image)) return;
const markup = image.closest(".markup");
if (!markup) return;
const gallery = [...markup.querySelectorAll("img")].filter(zoomable);
const index = gallery.indexOf(image);
if (index === -1) return;
event.preventDefault();
lightbox.show(gallery.map(describe), index);
};
document.addEventListener("click", onClick);
return () => document.removeEventListener("click", onClick);
};