1import React, { CSSProperties, SyntheticEvent, useEffect, Ref, useMemo } from 'react'; 2 3import { 4 ImageContentPositionObject, 5 ImageContentPositionValue, 6 ImageNativeProps, 7 ImageSource, 8} from '../Image.types'; 9import { useBlurhash } from '../utils/blurhash/useBlurhash'; 10import { isBlurhashString, isThumbhashString } from '../utils/resolveSources'; 11import { thumbHashStringToDataURL } from '../utils/thumbhash/thumbhash'; 12 13function ensureUnit(value: string | number) { 14 const trimmedValue = String(value).trim(); 15 if (trimmedValue.endsWith('%')) { 16 return trimmedValue; 17 } 18 return `${trimmedValue}px`; 19} 20 21type KeysOfUnion<T> = T extends T ? keyof T : never; 22 23function getObjectPositionFromContentPositionObject( 24 contentPosition?: ImageContentPositionObject 25): string { 26 const resolvedPosition = { ...contentPosition } as Record< 27 KeysOfUnion<ImageContentPositionObject>, 28 ImageContentPositionValue 29 >; 30 if (!resolvedPosition) { 31 return '50% 50%'; 32 } 33 if (resolvedPosition.top == null && resolvedPosition.bottom == null) { 34 resolvedPosition.top = '50%'; 35 } 36 if (resolvedPosition.left == null && resolvedPosition.right == null) { 37 resolvedPosition.left = '50%'; 38 } 39 40 return ( 41 ['top', 'bottom', 'left', 'right'] 42 .map((key) => { 43 if (key in resolvedPosition) { 44 return `${key} ${ensureUnit(resolvedPosition[key])}`; 45 } 46 return ''; 47 }) 48 .join(' ') || '50% 50%' 49 ); 50} 51 52function getFetchPriorityFromImagePriority(priority: ImageNativeProps['priority'] = 'normal') { 53 return priority && ['low', 'high'].includes(priority) ? priority : 'auto'; 54} 55 56const ImageWrapper = React.forwardRef( 57 ( 58 { 59 source, 60 events, 61 contentPosition, 62 hashPlaceholderContentPosition, 63 priority, 64 style, 65 hashPlaceholderStyle, 66 className, 67 accessibilityLabel, 68 ...props 69 }: { 70 source?: ImageSource | null; 71 events?: { 72 onLoad?: (((event: SyntheticEvent<HTMLImageElement, Event>) => void) | undefined | null)[]; 73 onError?: ((({ source }: { source: ImageSource | null }) => void) | undefined | null)[]; 74 onTransitionEnd?: ((() => void) | undefined | null)[]; 75 onMount?: ((() => void) | undefined | null)[]; 76 }; 77 contentPosition?: ImageContentPositionObject; 78 hashPlaceholderContentPosition?: ImageContentPositionObject; 79 priority?: string | null; 80 style: CSSProperties; 81 hashPlaceholderStyle?: CSSProperties; 82 className?: string; 83 accessibilityLabel?: string; 84 }, 85 ref: Ref<HTMLImageElement> 86 ) => { 87 useEffect(() => { 88 events?.onMount?.forEach((e) => e?.()); 89 }, []); 90 const isBlurhash = isBlurhashString(source?.uri || ''); 91 const isThumbhash = isThumbhashString(source?.uri || ''); 92 const isHash = isBlurhash || isThumbhash; 93 94 // Thumbhash uri always has to start with 'thumbhash:/' 95 const thumbhash = source?.uri?.replace(/thumbhash:\//, ''); 96 const thumbhashUri = useMemo( 97 () => (isThumbhash ? thumbHashStringToDataURL(thumbhash ?? '') : null), 98 [thumbhash] 99 ); 100 101 const blurhashUri = useBlurhash(isBlurhash ? source?.uri : null, source?.width, source?.height); 102 const objectPosition = getObjectPositionFromContentPositionObject( 103 isHash ? hashPlaceholderContentPosition : contentPosition 104 ); 105 106 const uri = isHash ? blurhashUri ?? thumbhashUri : source?.uri; 107 if (!uri) return null; 108 return ( 109 <img 110 ref={ref} 111 alt={accessibilityLabel} 112 className={className} 113 src={uri || undefined} 114 key={source?.uri} 115 {...props} 116 style={{ 117 width: '100%', 118 height: '100%', 119 position: 'absolute', 120 left: 0, 121 right: 0, 122 objectPosition, 123 ...style, 124 ...(isHash ? hashPlaceholderStyle : {}), 125 }} 126 // @ts-ignore 127 // eslint-disable-next-line react/no-unknown-property 128 fetchpriority={getFetchPriorityFromImagePriority(priority || 'normal')} 129 onLoad={(event) => { 130 if (typeof window !== 'undefined') { 131 // this ensures the animation will run, since the starting class is applied at least 1 frame before the target class set in the onLoad event callback 132 window.requestAnimationFrame(() => { 133 events?.onLoad?.forEach((e) => e?.(event)); 134 }); 135 } else { 136 events?.onLoad?.forEach((e) => e?.(event)); 137 } 138 }} 139 onTransitionEnd={() => events?.onTransitionEnd?.forEach((e) => e?.())} 140 onError={() => events?.onError?.forEach((e) => e?.({ source: source || null }))} 141 /> 142 ); 143 } 144); 145export default ImageWrapper; 146