import { useEffect, useMemo, useRef, useState } from 'react'; import type { CSSProperties, JSX, MutableRefObject } from 'react'; import { getAssetFromVersion, resolveDocumentAssetUrl, createAssetView, } from '../asset_manager'; import type { DocumentLike as AssetManagerDocumentLike, AssetLike as AssetManagerAssetLike, EnsureAssetUrl as AssetManagerEnsureAssetUrl, GetAsset as AssetManagerGetAsset, } from '../asset_manager'; const DEFAULT_THUMBNAIL_SIZE = 48; // Detect when an element becomes visible within a scroll container so we can delay loading. const useLazyVisibility = ( rootRef: MutableRefObject | null, resetKey: string | number | null | undefined, ) => { const targetRef = useRef(null); const [isVisible, setIsVisible] = useState(false); useEffect(() => { setIsVisible(false); }, [resetKey]); const rootNode = rootRef?.current || null; useEffect(() => { if (isVisible) { return undefined; } const element = targetRef.current; if (!element) { return undefined; } if (typeof window === 'undefined' || !('IntersectionObserver' in window)) { setIsVisible(true); return undefined; } const observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { setIsVisible(true); observer.disconnect(); } }); }, { root: rootNode, rootMargin: '200px 0px', threshold: 0.01, }, ); observer.observe(element); return () => observer.disconnect(); }, [isVisible, rootNode, resetKey]); return { ref: targetRef, isVisible }; }; const getPageCount = (doc: DocumentLike | null | undefined) => Number.isFinite(doc?.current_version?.metadata?.page_count) ? (doc?.current_version?.metadata?.page_count as number) : null; type Identifier = string | number; interface DocumentVersionLike { metadata?: { page_count?: number; width?: number; height?: number; [key: string]: unknown; }; assets?: unknown; [key: string]: unknown; } type DocumentLike = AssetManagerDocumentLike; type AssetLike = AssetManagerAssetLike; type EnsureAssetUrl = AssetManagerEnsureAssetUrl; type GetDocumentAsset = AssetManagerGetAsset; interface DocumentThumbnailImageProps { document?: DocumentLike | null; ensureAssetUrl?: EnsureAssetUrl; getDocumentAsset?: GetDocumentAsset; alt?: string; maxSize?: number; scrollRootRef?: MutableRefObject | null; } const DocumentThumbnailImage = ({ document, ensureAssetUrl, getDocumentAsset, alt = '', maxSize = DEFAULT_THUMBNAIL_SIZE, scrollRootRef = null, }: DocumentThumbnailImageProps): JSX.Element => { const documentId = document?.id; const { ref: visibilityRef, isVisible } = useLazyVisibility(scrollRootRef, documentId); const resolvedMaxSize = Math.max(1, Math.round(maxSize || 1)); const thumbnailAsset = useMemo( () => getAssetFromVersion(document?.current_version, 'thumbnail'), [document?.current_version], ); const thumbnailView = useMemo(() => createAssetView(thumbnailAsset), [thumbnailAsset]); const primaryMetadata = thumbnailView.getPrimaryMetadata() || {}; const assetWidth = Number(primaryMetadata?.width); const assetHeight = Number(primaryMetadata?.height); const dimensions = useMemo(() => { if (!Number.isFinite(assetWidth) || assetWidth <= 0 || !Number.isFinite(assetHeight) || assetHeight <= 0) { return { width: resolvedMaxSize, height: resolvedMaxSize }; } const scale = Math.min(1, resolvedMaxSize / assetWidth, resolvedMaxSize / assetHeight); return { width: Math.max(1, Math.round(assetWidth * scale)), height: Math.max(1, Math.round(assetHeight * scale)), }; }, [assetWidth, assetHeight, resolvedMaxSize]); const innerStyle = useMemo( () => ({ width: `${dimensions.width}px`, height: `${dimensions.height}px` }), [dimensions.height, dimensions.width], ); const url = useMemo(() => { if (!isVisible) { return null; } const options: { ensureAssetUrl?: EnsureAssetUrl; getAsset?: GetDocumentAsset; } = {}; if (ensureAssetUrl) { options.ensureAssetUrl = ensureAssetUrl; } if (getDocumentAsset) { options.getAsset = getDocumentAsset; } return resolveDocumentAssetUrl(document, 'thumbnail', options); }, [document, ensureAssetUrl, getDocumentAsset, isVisible]); const pageCount = getPageCount(document); const showMultiPageBadge = Number.isFinite(pageCount) && pageCount > 1; const innerClasses = ['document-thumbnail-inner']; if (showMultiPageBadge) { innerClasses.push('document-thumbnail-inner--multipage'); } const aspectRatio = useMemo(() => { if (Number.isFinite(assetWidth) && Number.isFinite(assetHeight) && assetWidth > 0 && assetHeight > 0) { return assetWidth / assetHeight; } return null; }, [assetWidth, assetHeight]); useEffect(() => { const node = visibilityRef.current; if (!node) { return; } if (aspectRatio) { node.dataset.thumbnailAspect = String(aspectRatio); } else { delete node.dataset.thumbnailAspect; } }, [aspectRatio, visibilityRef]); return (
{url ? ( {alt} event.preventDefault()} /> ) : (
DOC
)}
); }; export default DocumentThumbnailImage; export { DEFAULT_THUMBNAIL_SIZE };