import React, { PropsWithChildren, useContext } from 'react' import { FallbackImageOverrideWrapper, ImageSetOverrideWrapper, } from '../../ImageOverrideWrappers' import type { ContentTree } from '@financial-times/content-tree' import { PresentationFlagsContext } from '../../../context' import { ContentProps } from '../../types' import { ContentTreeWorkarounds } from '@financial-times/cp-content-pipeline-schema' import { SetRequired } from 'type-fest' import type * as ComponentWorkarounds from '../Workarounds' type ImageWithSourceSet = SetRequired type ImageWithAliases = ContentTree.Image & { [alias: string]: ImageWithSourceSet['sourceSet'] } const formatSourceSet = (sourceSet: ContentTree.ImageSource[]) => sourceSet.map((src) => `${src.url} ${src.dpr}x`).join(',') const figureClassNameMap: Record< ContentTree.ImageSetPicture['layoutWidth'], string > = { 'full-grid': 'n-content-picture n-content-picture--wide n-content-layout__container', 'inset-left': 'n-content-image n-content-image--inline', standard: 'n-content-image n-content-image--full', } type Breakpoint = { minWidth?: number maxWidth?: number orientation?: string } export type AliasedBreakpoints = { [alias: string]: Breakpoint } const defaultGetBreakpoints = ( image: ComponentWorkarounds.Image ): Breakpoint | Array | AliasedBreakpoints | undefined => { switch (image.format) { case 'mobile': return { maxWidth: 490 } case 'desktop': return { minWidth: 980 } case 'standard': case 'standard-inline': return { minWidth: 700 } } } export type BreakpointGetter = typeof defaultGetBreakpoints const breakpointToMedia = (breakpoint: Breakpoint): string => [ breakpoint.minWidth && `(min-width: ${breakpoint.minWidth}px)`, breakpoint.maxWidth && `(max-width: ${breakpoint.maxWidth}px)`, breakpoint.orientation && `(orientation: ${breakpoint.orientation})`, ] .filter(Boolean) .join(' and ') function hasSourceSet( image: ComponentWorkarounds.Image | ContentTreeWorkarounds.RawImage ): image is ImageWithSourceSet { return image && 'sourceSet' in image } function isAliasedBreakpoints( breakpoints: ReturnType ): breakpoints is AliasedBreakpoints { return Boolean( breakpoints && Object.values(breakpoints)[0] instanceof Object && !Array.isArray(breakpoints) ) } function figureWidth(picture: ComponentWorkarounds.ImageSetPicture) { if (picture.layoutWidth === 'inset-left') { const DEFAULT_WIDTH = 700 const width = picture.images[0]?.width ?? DEFAULT_WIDTH const height = picture.images[0]?.height const isWidthWithinBoundary = height && width < height && width > 350 && width < 580 const calculatedWidth = isWidthWithinBoundary ? 350 : Math.floor(Math.min(width, DEFAULT_WIDTH)) return { width: calculatedWidth, maxWidth: '100%' } } } type SourceProps = { image: ComponentWorkarounds.Image getBreakpoints?: BreakpointGetter } const Source: React.FC = ({ image, getBreakpoints = defaultGetBreakpoints, }) => { const breakpoints = getBreakpoints(image) if (!breakpoints) { return null } const breakpointsDict = isAliasedBreakpoints(breakpoints) ? breakpoints : { sourceSet: breakpoints } return ( <> {Object.entries(breakpointsDict) .map(([key, breakpoint]) => { const sourceSet = (image as ImageWithAliases)[key] const sourceSetMap = Array.isArray(breakpoint) ? breakpoint : [breakpoint] return ( sourceSet && sourceSetMap.map((breakpoint) => { return ( ) }) ) }) .filter((elem) => elem)} ) } type SourcesProps = { images: readonly ComponentWorkarounds.Image[] getBreakpoints?: BreakpointGetter } export const Sources: React.FC = ({ images, getBreakpoints }) => { return ( <> {images.map((image, index) => ( ))} ) } // HACK:20240809:IM This props type is quite a mess as the FallbackImage // component can be used both directly in a RichTextChild with a Content Tree // node, but also from the Image component above which the Image type. Image is // defined in Content Tree but is _not_ a node so this is a bit icky :( export type FallbackImageProps = ( | ContentProps | { content: { image: ComponentWorkarounds.Image } } ) & { imageType?: string picture?: ComponentWorkarounds.ImageSetPicture className?: string inSourceSet?: boolean isMainImage?: boolean } // TODO should `fallback` be added to content-tree imageset? export const FallbackImage: React.FC = (props) => { const { content: { image }, imageType = 'image', picture, className, inSourceSet = false, isMainImage = false, } = props const Wrapper = inSourceSet ? React.Fragment : ({ children }: PropsWithChildren) => (
{children}
) return ( {picture?.alt ) } export interface ImageSetProps extends ContentProps< ComponentWorkarounds.ImageSet & { fragmentIdentifier?: string } > { isMainImage?: boolean } const ImageSet: React.FC = (props) => { const { content: imageSet, isMainImage } = props if (!imageSet.picture?.images) { return null } const { reduceFullBleedImages } = useContext(PresentationFlagsContext) const { fragmentIdentifier } = imageSet const Wrapper = // HACK:20240618:IM don't stretch out the main image to the full grid in // the app as it can clash with the right hand rail !reduceFullBleedImages && imageSet.picture!.layoutWidth === 'full-grid' ? ({ children }: PropsWithChildren) => (
{children}
) : React.Fragment return (
{(imageSet.picture.caption || imageSet.picture.credit) && (
{imageSet.picture.caption} {imageSet.picture.caption ? ' ' : ''} {imageSet.picture.credit}
)}
) } export default ImageSet