elements_Image_index.js

import { React, PropTypes, classnames } from '@gravityforms/libraries';
import { spacerClasses } from '@gravityforms/utils';

const { forwardRef } = React;

const possiblePositions = [
	'center',
	'top',
	'bottom',
	'left',
	'right',
	'top left',
	'top center',
	'top right',
	'center left',
	'center center',
	'center right',
	'bottom left',
	'bottom center',
	'bottom right',
];

/**
 * @module Image
 * @description An image component in react.
 *
 * @since 1.1.15
 *
 * @param {object}                     props                   Component props.
 * @param {string}                     props.altText           The alt text for the image.
 * @param {boolean}                    props.asBg              Whether the image is a background image or not.
 * @param {number}                     props.aspectRatio       The aspect ratio of the image, as width / height. This only applies to background images.
 * @param {JSX.Element|string}         props.caption           The caption for the image. This only applies if the image is a background image.
 * @param {object}                     props.captionAttributes Attributes for the image caption element.
 * @param {object}                     props.customAttributes  Custom attributes for the component.
 * @param {string|Array|object}        props.customClasses     Custom classes for the component.
 * @param {number}                     props.height            The height of the image, in px. This only applies if the image is a background image.
 * @param {object}                     props.imageAttributes   Attributes for the image element.
 * @param {string}                     props.imagePosition     The position of the image, if it is a background image.
 * @param {boolean}                    props.lazyload          Whether to lazyload the image or not.
 * @param {string|number|Array|object} props.spacing           The spacing for the component, as a string, number, array, or object.
 * @param {string}                     props.url               The url of the image.
 * @param {number}                     props.width             The width of the image, in px.
 * @param {object|null}                ref                     Ref to the component.
 *
 * @return {JSX.Element} Return the function image component in React.
 *
 * @example
 * import Image from '@gravityforms/components/react/admin/elements/Image';
 *
 * return (
 *     <Image
 *         asBg={ true }
 *         aspectRatio={ 1.5 }
 *         url="https://path.to.image/"
 *     />
 * );
 *
 */
const Image = forwardRef( ( {
	altText = '',
	asBg = false,
	aspectRatio = 0,
	caption = null,
	captionAttributes = {},
	customAttributes = {},
	customClasses = [],
	height = 0,
	imageAttributes = {},
	imagePosition = 'center',
	lazyload = false,
	spacing = '',
	url = '',
	width = 0,
}, ref ) => {
	/**
	 * @function getBackgroundPosition
	 * @description Get the background position for the background image.
	 *
	 * @param {string} position The background image position.
	 *
	 * @return {string} The position of the background image.
	 */
	const getBackgroundPosition = ( position ) => {
		if ( ! possiblePositions.includes( position ) ) {
			return '';
		}

		return position;
	};

	/**
	 * @function getBackgroundImage
	 * @description Get the markup in react for a background image.
	 *
	 * @since 1.1.15
	 *
	 * @param {object} props                 Props for the background image.
	 * @param {object} props.attributes      The attributes for the image wrapper.
	 * @param {object} props.imageAttributes The attributes for the background image.
	 *
	 * @return {JSX.Element} The background image.
	 */
	const getBackgroundImage = ( {
		attributes,
		imageAttributes: bgImageAttributes,
	} ) => {
		return (
			<div { ...attributes }>
				<div { ...bgImageAttributes } />
			</div>
		);
	};

	/**
	 * @function getImageCaption
	 * @description Get the markup in react for a caption for the image component.
	 *
	 * @since 1.1.15
	 *
	 * @param {object}              props                  Props for the caption.
	 * @param {JSX.Element|string}  props.children         React element children.
	 * @param {object}              props.customAttributes Custom attributes for the caption.
	 * @param {string|Array|object} props.customClasses    Custom classes for the caption.
	 *
	 * @return {JSX.Element} The caption for the image component.
	 */
	const getImageCaption = ( {
		children = null,
		customAttributes: captionCustomAttributes = {},
		customClasses: captionCustomClasses = [],
	} ) => {
		const captionAttrs = {
			className: classnames( {
				'gform-image__caption': true,
			}, captionCustomClasses ),
			...captionCustomAttributes,
		};

		return (
			<figcaption { ...captionAttrs }>
				{ children }
			</figcaption>
		);
	};

	/**
	 * @function getImageWithCaption
	 * @description Get the markup in react for an image with caption.
	 *
	 * @since 1.1.15
	 *
	 * @param {object}             props                   Props for the image with caption.
	 * @param {object}             props.attributes        The attributes for the image wrapper.
	 * @param {JSX.Element|string} props.caption           The caption for the image, can be a string or react children.
	 * @param {object}             props.captionAttributes The attributes for the caption element.
	 * @param {object}             props.imageAttributes   The attributes for the image.
	 *
	 * @return {JSX.Element} The image with caption.
	 */
	const getImageWithCaption = ( {
		attributes,
		caption: captionElement,
		captionAttributes: captionElementAttributes,
		imageAttributes: imgAttributes,
	} ) => {
		return (
			<figure { ...attributes }>
				<img { ...imgAttributes } />{ /* eslint-disable-line jsx-a11y/alt-text */ }
				{ captionElement && getImageCaption( {
					...captionElementAttributes,
					children: captionElement,
				} ) }
			</figure>
		);
	};

	const attrs = {
		className: classnames( {
			'gform-image': true,
			'gform-image--bg': asBg,
			...spacerClasses( spacing ),
		}, customClasses ),
		ref,
		...customAttributes,
	};

	const { customClasses: imageCustomClasses, ...restImageAttributes } = imageAttributes;
	const imageAttrs = {
		...restImageAttributes,
		className: classnames( {
			'gform-image__image': true,
		}, imageCustomClasses || [] ),
	};

	if ( asBg ) { // Background image.
		imageAttrs.style = {
			backgroundImage: `url('${ url }')`,
			backgroundSize: 'cover',
			backgroundRepeat: 'no-repeat',
			backgroundPosition: getBackgroundPosition( imagePosition ), // Use the function to ensure it's a valid position
			...( imageAttributes.style || {} ),
		};
		if ( aspectRatio ) {
			const padding = Math.round( ( 10000 / aspectRatio ) ) / 100; // Round to nearest 2 decimals.
			imageAttrs.style.paddingBottom = `${ padding }%`;
		}
		if ( width ) {
			attrs.style = {
				...( customAttributes.style || {} ),
				width: `${ width }px`,
			};
		}
		imageAttrs.role = 'img';
		imageAttrs[ 'aria-label' ] = altText;
	} else { // Normal image.
		const imageStyles = {};
		imageAttrs.src = url;
		imageAttrs.alt = altText;
		if ( width ) {
			imageStyles.width = `${ width }px`;
		}
		if ( height ) {
			imageStyles.height = `${ height }px`;
		}
		if ( 'lazy' === lazyload ) {
			imageAttrs.lazyload = 'lazy';
		}
		imageAttrs.style = {
			...( imageAttributes.style || {} ),
			...imageStyles,
		};
	}

	return asBg
		? getBackgroundImage( {
			attributes: attrs,
			imageAttributes: imageAttrs,
		} )
		: ( getImageWithCaption( {
			attributes: attrs,
			imageAttributes: imageAttrs,
			caption,
			captionAttributes,
		} ) );
} );

Image.propTypes = {
	altText: PropTypes.string,
	asBg: PropTypes.bool,
	aspectRatio: PropTypes.number,
	caption: PropTypes.node,
	captionAttributes: PropTypes.object,
	customAttributes: PropTypes.object,
	customClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
	height: PropTypes.number,
	imageAttributes: PropTypes.object,
	imagePosition: PropTypes.string,
	lazyload: PropTypes.bool,
	spacing: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.number,
		PropTypes.array,
		PropTypes.object,
	] ),
	url: PropTypes.string,
	width: PropTypes.number,
};

Image.displayName = 'Image';

export default Image;