modules_Loaders_Placeholder_index.js

import { classnames, React } from '@gravityforms/libraries';
import { ConditionalWrapper } from '@gravityforms/react-utils';
import { spacerClasses } from '@gravityforms/utils';
import Grid from '../../../elements/Grid';

const { forwardRef } = React;

// Helper Functions
const getWrapperProps = ( {
	animated = true,
	spacing = 2,
	wrapperAttributes = {},
	wrapperClasses = [],
} ) => ( {
	className: classnames(
		'gform-placeholder__wrapper',
		{ 'gform-placeholder__wrapper--animated': animated },
		spacerClasses( spacing ),
		wrapperClasses
	),
	...wrapperAttributes,
} );

const getPlaceholderProps = ( {
	borderRadius = '9999px',
	customClasses = [],
	customAttributes = {},
	height = '14px',
	gridCount = 1,
	gridItemSpacing = 0,
	gridItemWidth = 90,
	width = '100%',
} ) => ( {
	className: classnames(
		'gform-placeholder',
		{ 'gform-grid--item': gridCount > 1 },
		spacerClasses( gridItemSpacing ),
		customClasses
	),
	style: {
		borderRadius,
		height,
		width: gridCount > 1 ? `${ gridItemWidth / gridCount }%` : width,
	},
	...customAttributes,
} );

const getMergedGridProps = ( gridProps = {} ) => ( {
	alignItems: 'center',
	container: true,
	direction: 'row',
	elementType: 'div',
	justifyContent: 'space-between',
	...gridProps,
} );

// PlaceholderStripe Component
const PlaceholderStripe = ( {
	gridCount,
	gridProps,
	placeholderProps,
	wrapperProps,
} ) => (
	<div { ...wrapperProps }>
		<ConditionalWrapper
			condition={ gridCount > 1 }
			wrapper={ ( children ) => <Grid { ...gridProps }>{ children }</Grid> }
		>
			{ Array( gridCount )
				.fill( 0 )
				.map( ( _, index ) => (
					<span
						key={ `placeholder-${ index }` }
						{ ...placeholderProps }
					/>
				) ) }
		</ConditionalWrapper>
	</div>
);

/**
 * @module Placeholder
 * @description A component that renders single or grouped placeholder stripes with optional
 * opacity animations and grid layout. Useful for loading states or content placeholders.
 *
 * @since 4.5.0
 *
 * @param {object}                        props                     - Component props
 * @param {boolean}                       [props.animated]          - Whether to include animation effects
 * @param {string}                        [props.borderRadius]      - Border radius of placeholder stripes
 * @param {object}                        [props.customAttributes]  - Custom HTML attributes for placeholder elements
 * @param {string|string[]|object}        [props.customClasses]     - Additional CSS classes for placeholder elements
 * @param {number}                        [props.gridCount]         - Number of placeholder stripes in a single row
 * @param {number}                        [props.gridItemSpacing]   - Spacing between grid items when in a row using grid count
 * @param {number}                        [props.gridItemWidth]     - Width of each grid item as a percentage
 * @param {object}                        [props.gridProps]         - Props to pass to the Grid component
 * @param {object[]}                      [props.groupData]         - Array of group configurations for multiple placeholder sets
 * @param {string}                        [props.height]            - Height of placeholder stripes
 * @param {number}                        [props.rows]              - Number of rows. This overrides groupData if set.
 * @param {string|number|string[]|object} [props.spacing]           - Spacing between elements
 * @param {string}                        [props.width]             - Width of placeholder stripes
 * @param {object}                        [props.wrapperAttributes] - Custom HTML attributes for wrapper element
 * @param {string|string[]|object}        [props.wrapperClasses]    - Additional CSS classes for wrapper
 * @param {object|null}                   props.ref                 - Forwarded ref to the component
 *
 * @return {JSX.Element} A placeholder component with configurable stripes and layout
 *
 * @example
 * // Single placeholder
 * <Placeholder height="20px" width="50%" animated={true} />
 *
 * // Grid of placeholders
 * <Placeholder gridCount={3} spacing={4} />
 *
 * // Grouped placeholders
 * <Placeholder
 *   groupData={[
 *     { gridCount: 2, height: '20px' },
 *     { gridCount: 3, height: '15px', animated: false }
 *   ]}
 * />
 */
const Placeholder = forwardRef( ( {
	animated = true,
	borderRadius = '9999px',
	customAttributes = {},
	customClasses = [],
	gridCount = 1,
	gridItemSpacing = 0,
	gridItemWidth = 90,
	gridProps = {},
	groupData = [],
	height = '14px',
	rows = 1,
	spacing = 2,
	width = '100%',
	wrapperAttributes = {},
	wrapperClasses = [],
}, ref ) => {
	if ( ! groupData.length && rows === 1 ) {
		const wrapperProps = {
			...getWrapperProps( { animated, spacing, wrapperAttributes, wrapperClasses } ),
			ref,
		};

		return (
			<PlaceholderStripe
				gridCount={ gridCount }
				gridProps={ getMergedGridProps( gridProps ) }
				placeholderProps={ getPlaceholderProps( {
					borderRadius,
					customClasses,
					customAttributes,
					height,
					gridCount,
					gridItemSpacing,
					gridItemWidth,
					width,
				} ) }
				wrapperProps={ wrapperProps }
			/>
		);
	}

	// if we just want to use global prop data for each row, fill out the groupData array with empty objects
	if ( ! groupData.length ) {
		groupData = Array( rows ).fill( {} );
	}

	return (
		<div className="gform-placeholder__group-container">
			{ groupData.map( ( group, index ) => {
				const {
					animated: groupAnimated = animated,
					borderRadius: groupBorderRadius = borderRadius,
					customAttributes: groupCustomAttributes = customAttributes,
					customClasses: groupCustomClasses = customClasses,
					gridCount: groupGridCount = gridCount,
					gridItemSpacing: groupGridItemSpacing = gridItemSpacing,
					gridItemWidth: groupGridItemWidth = gridItemWidth,
					gridProps: groupGridProps = gridProps,
					height: groupHeight = height,
					spacing: groupSpacing = spacing,
					width: groupWidth = width,
					wrapperAttributes: groupWrapperAttributes = wrapperAttributes,
					wrapperClasses: groupWrapperClasses = wrapperClasses,
				} = group;

				return (
					<PlaceholderStripe
						key={ `placeholder-group-${ index }` }
						gridCount={ groupGridCount }
						gridProps={ getMergedGridProps( groupGridProps ) }
						placeholderProps={ getPlaceholderProps( {
							borderRadius: groupBorderRadius,
							customClasses: groupCustomClasses,
							customAttributes: groupCustomAttributes,
							height: groupHeight,
							gridCount: groupGridCount,
							gridItemSpacing: groupGridItemSpacing,
							gridItemWidth: groupGridItemWidth,
							width: groupWidth,
						} ) }
						wrapperProps={ getWrapperProps( {
							animated: groupAnimated,
							spacing: groupSpacing,
							wrapperAttributes: groupWrapperAttributes,
							wrapperClasses: groupWrapperClasses,
						} ) }
					/>
				);
			} ) }
		</div>
	);
} );

Placeholder.displayName = 'Loaders/Placeholder';

export default Placeholder;