1import React, { CSSProperties, SyntheticEvent, useEffect, Ref } from 'react';
2
3import {
4  ImageContentPositionObject,
5  ImageContentPositionValue,
6  ImageNativeProps,
7  ImageSource,
8} from '../Image.types';
9import { useBlurhash } from '../utils/blurhash/useBlurhash';
10import { isBlurhashString } from '../utils/resolveSources';
11
12function ensureUnit(value: string | number) {
13  const trimmedValue = String(value).trim();
14  if (trimmedValue.endsWith('%')) {
15    return trimmedValue;
16  }
17  return `${trimmedValue}px`;
18}
19
20type KeysOfUnion<T> = T extends T ? keyof T : never;
21
22function getObjectPositionFromContentPositionObject(
23  contentPosition?: ImageContentPositionObject
24): string {
25  const resolvedPosition = { ...contentPosition } as Record<
26    KeysOfUnion<ImageContentPositionObject>,
27    ImageContentPositionValue
28  >;
29  if (!resolvedPosition) {
30    return '50% 50%';
31  }
32  if (resolvedPosition.top == null && resolvedPosition.bottom == null) {
33    resolvedPosition.top = '50%';
34  }
35  if (resolvedPosition.left == null && resolvedPosition.right == null) {
36    resolvedPosition.left = '50%';
37  }
38
39  return (
40    ['top', 'bottom', 'left', 'right']
41      .map((key) => {
42        if (key in resolvedPosition) {
43          return `${key} ${ensureUnit(resolvedPosition[key])}`;
44        }
45        return '';
46      })
47      .join(' ') || '50% 50%'
48  );
49}
50
51function getFetchPriorityFromImagePriority(priority: ImageNativeProps['priority'] = 'normal') {
52  return priority && ['low', 'high'].includes(priority) ? priority : 'auto';
53}
54
55const ImageWrapper = React.forwardRef(
56  (
57    {
58      source,
59      events,
60      contentPosition,
61      blurhashContentPosition,
62      priority,
63      style,
64      blurhashStyle,
65      className,
66      accessibilityLabel,
67    }: {
68      source?: ImageSource | null;
69      events?: {
70        onLoad?: (((event: SyntheticEvent<HTMLImageElement, Event>) => void) | undefined | null)[];
71        onError?: ((({ source }: { source: ImageSource | null }) => void) | undefined | null)[];
72        onTransitionEnd?: ((() => void) | undefined | null)[];
73        onMount?: ((() => void) | undefined | null)[];
74      };
75      contentPosition?: ImageContentPositionObject;
76      blurhashContentPosition?: ImageContentPositionObject;
77      priority?: string | null;
78      style: CSSProperties;
79      blurhashStyle?: CSSProperties;
80      className?: string;
81      accessibilityLabel?: string;
82    },
83    ref: Ref<HTMLImageElement>
84  ) => {
85    useEffect(() => {
86      events?.onMount?.forEach((e) => e?.());
87    }, []);
88    const isBlurhash = isBlurhashString(source?.uri || '');
89    const blurhashUri = useBlurhash(isBlurhash ? source?.uri : null, source?.width, source?.height);
90    const objectPosition = getObjectPositionFromContentPositionObject(
91      isBlurhash ? blurhashContentPosition : contentPosition
92    );
93    const uri = isBlurhash ? blurhashUri : source?.uri;
94    if (!uri) return null;
95    return (
96      <img
97        ref={ref}
98        alt={accessibilityLabel}
99        className={className}
100        src={uri || undefined}
101        key={source?.uri}
102        style={{
103          width: '100%',
104          height: '100%',
105          position: 'absolute',
106          left: 0,
107          right: 0,
108          objectPosition,
109          ...style,
110          ...(isBlurhash ? blurhashStyle : {}),
111        }}
112        // @ts-ignore
113        // eslint-disable-next-line react/no-unknown-property
114        fetchpriority={getFetchPriorityFromImagePriority(priority || 'normal')}
115        onLoad={(event) => {
116          events?.onLoad?.forEach((e) => e?.(event));
117        }}
118        onTransitionEnd={() => events?.onTransitionEnd?.forEach((e) => e?.())}
119        onError={() => events?.onError?.forEach((e) => e?.({ source: source || null }))}
120      />
121    );
122  }
123);
124export default ImageWrapper;
125