Download web/src/components/image-lightbox.tsx from zlcy/study: direct link, hf CLI and curl.
- Browser
- Download file 5.52 kB
-
https://huggingface.co/spaces/zlcy/study/resolve/main/web/src/components/image-lightbox.tsx
- Command line
-
hf download hf://spaces/zlcy/study/web/src/components/image-lightbox.tsx
-
curl -L -o image-lightbox.tsx https://huggingface.co/spaces/zlcy/study/resolve/main/web/src/components/image-lightbox.tsx
5.52 kB
| "use client"; | |
| import { useEffect, useRef } from "react"; | |
| type LightboxImage = { | |
| id: string; | |
| src: string; | |
| sizeLabel?: string; | |
| dimensions?: string; | |
| }; | |
| type ImageLightboxProps = { | |
| images: LightboxImage[]; | |
| currentIndex: number; | |
| open: boolean; | |
| onOpenChange: (open: boolean) => void; | |
| onIndexChange: (index: number) => void; | |
| }; | |
| type FancyboxItem = { | |
| src: string; | |
| type: "image"; | |
| caption?: string; | |
| thumb?: string; | |
| }; | |
| type FancyboxInstance = { | |
| close: () => void; | |
| getSlide?: () => { index?: number } | undefined; | |
| }; | |
| type FancyboxGlobal = { | |
| show: ( | |
| items: FancyboxItem[], | |
| options?: { | |
| startIndex?: number; | |
| Hash?: false; | |
| closeExisting?: boolean; | |
| Carousel?: { infinite?: boolean }; | |
| on?: { | |
| close?: () => void; | |
| "Carousel.change"?: (fancyboxRef: FancyboxInstance) => void; | |
| }; | |
| }, | |
| ) => FancyboxInstance; | |
| }; | |
| declare global { | |
| interface Window { | |
| Fancybox?: FancyboxGlobal; | |
| } | |
| } | |
| const FANCYBOX_SCRIPT_URL = "https://cdn.jsdelivr.net/npm/@fancyapps/ui@6.1.14/dist/fancybox/fancybox.umd.js"; | |
| const FANCYBOX_STYLE_URL = "https://cdn.jsdelivr.net/npm/@fancyapps/ui@6.1.14/dist/fancybox/fancybox.css"; | |
| let fancyboxLoader: Promise<FancyboxGlobal> | null = null; | |
| function escapeHtml(value: string) { | |
| return value.replace(/[&<>"']/g, (char) => { | |
| switch (char) { | |
| case "&": | |
| return "&"; | |
| case "<": | |
| return "<"; | |
| case ">": | |
| return ">"; | |
| case "\"": | |
| return """; | |
| case "'": | |
| return "'"; | |
| default: | |
| return char; | |
| } | |
| }); | |
| } | |
| function getCaption(image: LightboxImage) { | |
| const parts = [image.sizeLabel, image.dimensions].filter(Boolean); | |
| return parts.length ? escapeHtml(parts.join(" · ")) : undefined; | |
| } | |
| function ensureFancyboxStyle() { | |
| if (document.querySelector(`link[href="${FANCYBOX_STYLE_URL}"]`)) { | |
| return; | |
| } | |
| const link = document.createElement("link"); | |
| link.rel = "stylesheet"; | |
| link.href = FANCYBOX_STYLE_URL; | |
| document.head.appendChild(link); | |
| } | |
| function loadFancybox() { | |
| if (typeof window === "undefined") { | |
| return Promise.reject(new Error("Fancybox can only be loaded in the browser")); | |
| } | |
| ensureFancyboxStyle(); | |
| if (window.Fancybox) { | |
| return Promise.resolve(window.Fancybox); | |
| } | |
| if (fancyboxLoader) { | |
| return fancyboxLoader; | |
| } | |
| fancyboxLoader = new Promise<FancyboxGlobal>((resolve, reject) => { | |
| const existingScript = document.querySelector<HTMLScriptElement>(`script[src="${FANCYBOX_SCRIPT_URL}"]`); | |
| const resolveLoaded = () => { | |
| if (window.Fancybox) { | |
| resolve(window.Fancybox); | |
| } else { | |
| reject(new Error("Fancybox loaded but did not expose window.Fancybox")); | |
| } | |
| }; | |
| if (existingScript) { | |
| existingScript.addEventListener("load", resolveLoaded, { once: true }); | |
| existingScript.addEventListener("error", () => reject(new Error("Failed to load Fancybox")), { once: true }); | |
| return; | |
| } | |
| const script = document.createElement("script"); | |
| script.src = FANCYBOX_SCRIPT_URL; | |
| script.async = true; | |
| script.onload = resolveLoaded; | |
| script.onerror = () => reject(new Error("Failed to load Fancybox")); | |
| document.body.appendChild(script); | |
| }); | |
| return fancyboxLoader; | |
| } | |
| function clampIndex(index: number, length: number) { | |
| if (length <= 0) return 0; | |
| return Math.max(0, Math.min(index, length - 1)); | |
| } | |
| export function ImageLightbox({ | |
| images, | |
| currentIndex, | |
| open, | |
| onOpenChange, | |
| onIndexChange, | |
| }: ImageLightboxProps) { | |
| const instanceRef = useRef<FancyboxInstance | null>(null); | |
| const isClosingRef = useRef(false); | |
| useEffect(() => { | |
| if (!open) { | |
| if (instanceRef.current && !isClosingRef.current) { | |
| isClosingRef.current = true; | |
| instanceRef.current.close(); | |
| instanceRef.current = null; | |
| isClosingRef.current = false; | |
| } | |
| return; | |
| } | |
| if (images.length === 0) { | |
| onOpenChange(false); | |
| return; | |
| } | |
| if (instanceRef.current) { | |
| return; | |
| } | |
| let cancelled = false; | |
| void loadFancybox() | |
| .then((Fancybox) => { | |
| if (cancelled) return; | |
| const items = images.map((image) => ({ | |
| src: image.src, | |
| type: "image" as const, | |
| thumb: image.src, | |
| caption: getCaption(image), | |
| })); | |
| instanceRef.current = Fancybox.show(items, { | |
| startIndex: clampIndex(currentIndex, images.length), | |
| Hash: false, | |
| closeExisting: true, | |
| Carousel: { | |
| infinite: images.length > 1, | |
| }, | |
| on: { | |
| close: () => { | |
| instanceRef.current = null; | |
| isClosingRef.current = false; | |
| onOpenChange(false); | |
| }, | |
| "Carousel.change": (fancyboxRef) => { | |
| const nextIndex = fancyboxRef.getSlide?.()?.index; | |
| if (typeof nextIndex === "number") { | |
| onIndexChange(clampIndex(nextIndex, images.length)); | |
| } | |
| }, | |
| }, | |
| }); | |
| }) | |
| .catch((error) => { | |
| console.error(error); | |
| onOpenChange(false); | |
| }); | |
| return () => { | |
| cancelled = true; | |
| }; | |
| }, [open, images, currentIndex, onOpenChange, onIndexChange]); | |
| useEffect(() => { | |
| return () => { | |
| if (instanceRef.current) { | |
| isClosingRef.current = true; | |
| instanceRef.current.close(); | |
| instanceRef.current = null; | |
| } | |
| }; | |
| }, []); | |
| return null; | |
| } | |