UNPKG

react-photo-album

Version:

Responsive photo gallery component for React

184 lines (183 loc) 5.66 kB
"use client"; import { jsx, jsxs, Fragment } from "react/jsx-runtime"; import { useMemo, useState, useCallback, cloneElement, useLayoutEffect as useLayoutEffect$1, useEffect, useRef, Children, isValidElement } from "react"; const observers = /* @__PURE__ */ new Map(); function createObserver(rootMargin) { let instance = observers.get(rootMargin); if (!instance) { const listeners = /* @__PURE__ */ new Map(); const observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { listeners.get(entry.target)?.forEach((callback) => { callback(entry); }); }); }, { rootMargin } ); instance = { observer, listeners }; observers.set(rootMargin, instance); } return instance; } function useIntersectionObserver(rootMargin) { return useMemo(() => { const cleanup = []; const observe = (target, callback) => { const { observer, listeners } = createObserver(rootMargin); const callbacks = listeners.get(target) || []; callbacks.push(callback); listeners.set(target, callbacks); observer.observe(target); cleanup.push(() => { callbacks.splice(callbacks.indexOf(callback), 1); if (callbacks.length === 0) { listeners.delete(target); observer.unobserve(target); } if (listeners.size === 0) { observer.disconnect(); observers.delete(rootMargin); } }); }; const unobserve = () => { cleanup.forEach((callback) => callback()); cleanup.splice(0, cleanup.length); }; return { observe, unobserve }; }, [rootMargin]); } function Offscreen({ rootMargin, children }) { const [placeholder, setPlaceholder] = useState(); const { observe, unobserve } = useIntersectionObserver(rootMargin); const ref = useCallback( (node) => { unobserve(); if (node) { observe(node, ({ isIntersecting }) => { if (!isIntersecting) { const { width, height } = node.getBoundingClientRect(); const { margin } = getComputedStyle(node); setPlaceholder({ width, height, margin }); } else { setPlaceholder(void 0); } }); } }, [observe, unobserve] ); return !placeholder ? cloneElement(children, { ref }) : jsx("div", { ref, style: placeholder }); } const useLayoutEffect = typeof window !== "undefined" ? useLayoutEffect$1 : useEffect; function useEventCallback(fn) { const ref = useRef(fn); useLayoutEffect(() => { ref.current = fn; }); return useCallback((...args) => ref.current?.(...args), []); } var Status = ((Status2) => { Status2[Status2["IDLE"] = 0] = "IDLE"; Status2[Status2["LOADING"] = 1] = "LOADING"; Status2[Status2["ERROR"] = 2] = "ERROR"; Status2[Status2["FINISHED"] = 3] = "FINISHED"; return Status2; })(Status || {}); function InfiniteScroll({ photos: initialPhotos, onClick, fetch, retries = 0, singleton, error, loading, finished, children, fetchRootMargin = "800px", offscreenRootMargin = "2000px" }) { const [status, setStatus] = useState(0); const [photos, setPhotos] = useState(() => initialPhotos ? [initialPhotos] : []); const { observe, unobserve } = useIntersectionObserver(fetchRootMargin); const fetching = useRef(false); const fetchWithRetry = useEventCallback((index) => { let attempt = 1; const execute = async () => { try { return await fetch(index); } catch (err) { if (attempt > retries) { throw err; } await new Promise((resolve) => { setTimeout(resolve, 1e3 * 2 ** (attempt - 1)); }); attempt += 1; return execute(); } }; return execute(); }); const handleFetch = useCallback(async () => { if (fetching.current) return; const updateStatus = (newStatus) => { fetching.current = newStatus === 1; setStatus(newStatus); }; updateStatus(1); try { const batch = await fetchWithRetry(photos.length); if (batch === null) { updateStatus(3); return; } setPhotos((prev) => [...prev, batch]); updateStatus(0); } catch (_) { updateStatus(2); } }, [photos.length, fetchWithRetry]); const sentinelRef = useCallback( (node) => { unobserve(); if (node) { observe(node, ({ isIntersecting }) => { if (isIntersecting) { handleFetch(); } }); } }, [observe, unobserve, handleFetch] ); const photosArray = photos.flatMap((batch) => batch); const handleClick = onClick ? ({ photo, event }) => { onClick({ photos: photosArray, index: photosArray.findIndex((item) => item === photo), photo, event }); } : void 0; return jsxs(Fragment, { children: [ singleton ? cloneElement(children, { photos: photosArray, onClick: handleClick, render: { ...children.props.render, track: ({ children: trackChildren, ...rest }) => jsx("div", { ...rest, children: Children.map( trackChildren, (child, index) => isValidElement(child) && jsx(Offscreen, { rootMargin: offscreenRootMargin, children: child }, index) ) }) } }) : photos.map((batch, index) => jsx(Offscreen, { rootMargin: offscreenRootMargin, children: cloneElement(children, { photos: batch, onClick: handleClick }) }, index)), status === 2 && error, status === 1 && loading, status === 3 && finished, jsx("span", { ref: sentinelRef }) ] }); } export { InfiniteScroll as default };