{"version":3,"file":"image.mjs","names":[],"sources":["../../../../package/common/image/image.tsx"],"sourcesContent":["import { useEffect, useLayoutEffect, useRef, useState, type CSSProperties, type ReactNode } from 'react';\nimport cx from 'classnames';\nimport { faImage } from '@fortawesome/free-solid-svg-icons';\n\nimport Skeleton from '../skeleton';\nimport { SkeletonVariant } from '../skeleton/skeleton.types';\nimport { useComponentDefaults } from '../configProvider';\nimport { mergeRefs } from '../utils/mergeRefs';\nimport { renderIcon } from '../utils/renderIcon';\nimport type { ImageProps } from './image.types';\nimport styles from './image.module.pcss';\n\n/** `cached` was complete before the first paint, so it neither stands in nor fades. */\ntype Status = 'loading' | 'cached' | 'arrived' | 'failed';\n\n/** Never wraps the `<img>`: PanZoom fits and measures its direct children, and a wrapper would hide the image. */\nfunction Image(props: ImageProps) {\n  const {\n    src,\n    alt,\n    thumbnail,\n    placeholder,\n    fallback,\n    width,\n    height,\n    className,\n    style,\n    classNames,\n    styles: slotStyles,\n    onLoad,\n    onError,\n    ref,\n    ...rest\n  } = useComponentDefaults('Image', props);\n\n  const imageRef = useRef<HTMLImageElement>(null);\n  const thumbnailRef = useRef<HTMLImageElement>(null);\n  const [status, setStatus] = useState<Status>('loading');\n  const [hasThumbnail, setHasThumbnail] = useState(false);\n  const [ownSize, setOwnSize] = useState<{ width: number; height: number } | null>(null);\n  const [statusOf, setStatusOf] = useState(`${src} ${thumbnail}`);\n  if (`${src} ${thumbnail}` !== statusOf) {\n    setStatusOf(`${src} ${thumbnail}`);\n    setStatus('loading');\n    setHasThumbnail(false);\n    setOwnSize(null);\n  }\n\n  useLayoutEffect(() => {\n    const image = imageRef.current;\n    const small = thumbnailRef.current;\n    if (image?.complete && image.naturalWidth) setStatus('cached');\n    if (small?.complete && small.naturalWidth) setHasThumbnail(true);\n  }, [src, thumbnail]);\n\n  const hasGivenSize = width !== undefined && height !== undefined;\n  const isLoading = status === 'loading';\n\n  // Polled: the browser knows the size from the first bytes but fires no event for it. Only near the\n  // screen, so a lazy image further down is not polled for nothing.\n  useEffect(() => {\n    const image = imageRef.current;\n    if (!isLoading || hasGivenSize || ownSize || !image || typeof IntersectionObserver === 'undefined') return;\n    let frame = 0;\n    const check = () => {\n      // A new source on the same element reports the old image's size until it has loaded.\n      if (image.naturalWidth && (image.currentSrc === image.src || image.srcset)) {\n        setOwnSize({ width: image.naturalWidth, height: image.naturalHeight });\n      } else {\n        frame = requestAnimationFrame(check);\n      }\n    };\n    const observer = new IntersectionObserver(\n      ([entry]) => {\n        cancelAnimationFrame(frame);\n        if (entry.isIntersecting) frame = requestAnimationFrame(check);\n      },\n      { rootMargin: '200px' },\n    );\n    observer.observe(image);\n    return () => {\n      observer.disconnect();\n      cancelAnimationFrame(frame);\n    };\n  }, [isLoading, hasGivenSize, ownSize, src]);\n\n  const size = hasGivenSize || !ownSize ? intrinsicSize(width, height) : ownSizeOf(ownSize);\n\n  if (status === 'failed') {\n    if (fallback !== undefined) return fallback;\n    return (\n      <span\n        {...(alt ? { role: 'img', 'aria-label': alt } : { 'aria-hidden': true })}\n        className={cx(styles.imageStandIn, styles.imageFallback, className, classNames?.fallback)}\n        style={{ ...size, ...style, ...slotStyles?.fallback }}\n      >\n        {/* A box of its own to centre in: the image's class sets the fallback's `display`. */}\n        <span className={styles.imageFallbackIcon}>{renderIcon(faImage)}</span>\n      </span>\n    );\n  }\n\n  const showsThumbnail = isLoading && hasThumbnail;\n  let earlyStandIn: ReactNode = null;\n  if (isLoading && !hasThumbnail) {\n    earlyStandIn =\n      placeholder !== undefined ? (\n        placeholder\n      ) : (\n        <Skeleton\n          variant={SkeletonVariant.Rectangular}\n          className={cx(styles.imageStandIn, className, classNames?.placeholder)}\n          style={{ ...size, ...style, ...slotStyles?.placeholder }}\n        />\n      );\n  }\n  const holdsBack = isLoading && (earlyStandIn !== null || !!thumbnail);\n\n  return (\n    <>\n      {earlyStandIn}\n      {isLoading && thumbnail && (\n        <img\n          ref={thumbnailRef}\n          src={thumbnail}\n          alt=''\n          width={width}\n          height={height}\n          className={cx(!showsThumbnail && styles.imageLoading, className, classNames?.placeholder)}\n          style={{ ...size, ...style, ...slotStyles?.placeholder }}\n          onLoad={() => setHasThumbnail(true)}\n        />\n      )}\n      <img\n        {...rest}\n        ref={mergeRefs(imageRef, ref)}\n        src={src}\n        alt={alt}\n        width={width}\n        height={height}\n        // For PanZoom: a new `src` on the same element reports the old size until it has loaded.\n        aria-busy={isLoading || undefined}\n        className={cx(\n          holdsBack && styles.imageLoading,\n          // Not over its own thumbnail, which already shows the picture and would flash.\n          status === 'arrived' && !hasThumbnail && styles.imageArrived,\n          className,\n        )}\n        style={style}\n        onLoad={(event) => {\n          onLoad?.(event);\n          setStatus((current) => (current === 'loading' ? 'arrived' : current));\n        }}\n        onError={(event) => {\n          onError?.(event);\n          setStatus('failed');\n        }}\n      />\n    </>\n  );\n}\n\n/** No height: under CSS that sets only a width the image keeps its proportions, which a height would override. */\nfunction ownSizeOf({ width, height }: { width: number; height: number }): CSSProperties {\n  return { '--custom-image-width': `${width}px`, '--custom-image-ratio': `${width} / ${height}` } as CSSProperties;\n}\n\nfunction intrinsicSize(width: ImageProps['width'], height: ImageProps['height']): CSSProperties {\n  const length = (value: ImageProps['width']) =>\n    value === undefined ? undefined : /^\\d+(\\.\\d+)?$/.test(String(value)) ? `${value}px` : String(value);\n  const size: Record<string, string | undefined> = {\n    '--custom-image-width': length(width),\n    '--custom-image-height': length(height),\n  };\n  if (width !== undefined && height !== undefined && !Number.isNaN(Number(width)) && !Number.isNaN(Number(height))) {\n    size['--custom-image-ratio'] = `${width} / ${height}`;\n  }\n  return size as CSSProperties;\n}\n\nexport default Image;\n"],"mappings":"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"}