modules_Swatch_index.js

import { React, PropTypes, classnames } from '@gravityforms/libraries';
import { spacerClasses } from '@gravityforms/utils';
import Icon from '../../elements/Icon';
import Label from '../../elements/Label';
import ColorPicker from '../ColorPicker';
import { invertColor } from '../../utils/colors';

const { useState, useEffect, useRef, forwardRef } = React;

/**
 * @module Swatch
 * @description Allows users to select from a pallete of swatches, or add their own using a color picker
 *
 * @since 1.1.15
 *
 * @param {object}                     props                  Component props.
 * @param {boolean}                    props.allowNew         Whether to display an icon to add new swatches to the palette.
 * @param {object}                     props.customAttributes Custom attributes for the component.
 * @param {string|Array|object}        props.customClasses    Custom classes for the component.
 * @param {object}                     props.i18n             Translated strings for the UI.
 * @param {string}                     props.id               The ID for the component.
 * @param {object}                     props.labelAttributes  Custom attributes to apply to the label for each swatch.
 * @param {string}                     props.name             The name of the component.
 * @param {Array}                      props.palette          An array of hex color values to display as default palette options.
 * @param {Array}                      props.paletteCustom    An array of hex color values to display as custom/editable palette options.
 * @param {string|number|Array|object} props.spacing          The spacing for the component, as a string, number, array, or object.
 * @param {string}                     props.value            The initial hex value to select.
 * @param {object|null}                ref                    Ref to the component.
 *
 * @return {JSX.Element} The Swatch component.
 *
 * @example
 * import Swatch from '@gravityforms/components/react/admin/modules/Swatch';
 *
 * return (
 *     <Swatch customClasses={ [ 'example-class' ] } palette={ [ '#000', '#111' ] } />
 * );
 *
 */
const Swatch = forwardRef( ( {
	allowNew = true,
	customAttributes = {},
	customClasses = [],
	i18n = {},
	id = '',
	labelAttributes = {
		size: 'text-sm',
		weight: 'medium',
	},
	name = '',
	palette = [],
	paletteCustom = [],
	spacing = '',
	value = '',
}, ref ) => {
	const [ showPicker, setShowPicker ] = useState( false );
	const [ selectedColor, setSelectedColor ] = useState( value );
	const [ color, setColor ] = useState( value );
	const [ colorBeingModified, setColorBeingModified ] = useState( palette.length + 1 );
	const [ customPaletteOptions, setCustomPaletteOptions ] = useState( paletteCustom );
	const [ currentRef, setCurrentRef ] = useState( null );

	const allInputRef = useRef( null );
	const addNewRef = useRef( document.querySelector( `[data-js-setting-name="${ name }"] .gform-input--swatch__option--new` ) );
	const swatchRefs = useRef( [] );

	useEffect( () => {
		const newPaletteString = JSON.stringify( customPaletteOptions );
		allInputRef.current.value = newPaletteString;

		if ( customPaletteOptions.length === 0 ) {
			setSelectedColor( value );
		}
	}, [ customPaletteOptions, value ] );

	useEffect( () => {
		if ( ! currentRef || ! currentRef.current ) {
			setCurrentRef( addNewRef );
		}
	}, [ setCurrentRef, currentRef ] );

	/**
	 * @function handlePickerCancel
	 * @description Handler for the cancel event on the swatch.
	 *
	 * @since 1.1.15
	 *
	 * @return {void}
	 */
	const handlePickerCancel = () => {
		setShowPicker( false );
	};

	/**
	 * @function handlePickerDelete
	 * @description Handler for the delete event on the swatch.
	 *
	 * @since 1.1.15
	 *
	 * @param {number} index Index of the swatch palette.
	 *
	 * @return {void}
	 */
	const handlePickerDelete = ( index ) => {
		setCustomPaletteOptions( ( prevPalette ) => prevPalette.filter( ( item, thisIndex ) => thisIndex !== index ) );
		handlePickerCancel();
	};

	/**
	 * @function handlePickerSave
	 * @description Handler for the save event on the swatch.
	 *
	 * @since 1.1.15
	 *
	 * @param {string} swatch The swatch value.
	 *
	 * @return {void}
	 */
	const handlePickerSave = ( swatch ) => {
		setColor( swatch );
		if ( ! customPaletteOptions.includes( swatch ) ) {
			setCustomPaletteOptions( ( prevPalette ) => {
				const newPalette = prevPalette;
				newPalette[ colorBeingModified ] = swatch;
				return newPalette;
			} );
		}
		setSelectedColor( swatch );
		setShowPicker( false );
	};

	/**
	 * @function handleColorChange
	 * @description Handler for the color change event.
	 *
	 * @since 1.1.15
	 *
	 * @param {object} event Event object.
	 *
	 * @return {void}
	 */
	const handleColorChange = ( event ) => {
		setSelectedColor( event.target.value );
	};

	/**
	 * @function renderSwatchOption
	 * @description Render the swatch option.
	 *
	 * @since 1.1.15
	 *
	 * @param {string}  swatch   The swatch value.
	 * @param {number}  index    The index of the swatch palette.
	 * @param {boolean} isCustom Whether the swatch is custom or not.
	 *
	 * @return {JSX.Element} The swatch option.
	 */
	const renderSwatchOption = ( swatch, index, isCustom = false ) => {
		const liProps = {
			className: classnames( {
				'gform-input--swatch__option': true,
			} ),
			key: index,
		};

		const labelProps = {
			htmlFor: `${ name }_${ swatch }_${ index }`,
			label: i18n?.swatch || '',
			isVisible: false,
			...labelAttributes,
		};

		const swatchInputProps = {
			onChange: handleColorChange,
			type: 'radio',
			name,
			value: swatch,
			id: `${ name }_${ swatch }_${ index }`,
			checked: swatch === selectedColor,
		};

		if ( isCustom ) {
			swatchInputProps.onClick = () => {
				setColor( swatch );
				setCurrentRef( { current: swatchRefs.current[ index ] } );
				setColorBeingModified( index );
				setShowPicker( true );
			};
		}

		const swatchSpanProps = {
			className: classnames( {
				'gform-input--swatch__option-preview': true,
			} ),
			style: {
				backgroundColor: swatch,
			},
			onClick: ( e ) => {
				if ( e.target.classList.contains( 'gform-input--swatch-delete' ) ) {
					handlePickerDelete( index );
					return;
				}
				document.getElementById( `${ name }_${ swatch }_${ index }` ).click();
			},
			ref: isCustom ? ( el ) => ( swatchRefs.current[ index ] = el ) : null,
		};

		const iconProps = {
			icon: 'check',
			customClasses: classnames( {
				'gform-input--swatch-selected': true,
			} ),
			customAttributes: {
				style: {
					color: invertColor( swatch ),
				},
			},
		};

		const deleteIconProps = {
			icon: 'delete',
			customClasses: classnames( {
				'gform-input--swatch-delete': true,
			} ),
		};

		return (
			<li { ...liProps }>
				<Label { ...labelProps } />
				<input { ...swatchInputProps } />
				<span { ...swatchSpanProps } >
					{ swatch === selectedColor && <Icon { ...iconProps } /> }
					{ isCustom && <Icon { ...deleteIconProps } /> }
				</span>
			</li>
		);
	};

	/**
	 * @function renderAddNewSwatchOption
	 * @description Render the add new swatch option.
	 *
	 * @since 1.1.15
	 *
	 * @return {JSX.Element} The add new swatch option.
	 */
	const renderAddNewSwatchOption = () => {
		const liProps = {
			className: classnames( {
				'gform-input--swatch__option': true,
				'gform-input--swatch__option--new': true,
			} ),
			key: 'add-new',
		};

		const swatchSpanProps = {
			className: classnames( {
				'gform-input--swatch__option-preview': true,
				'gform-input--swatch__option-preview--new': true,
			} ),
			onClick: () => {
				setCurrentRef( addNewRef );
				setColorBeingModified( customPaletteOptions.length + 1 );
				setShowPicker( true );
			},
			ref: addNewRef,
		};

		const iconProps = {
			icon: 'plus-regular',
		};

		return (
			<li { ...liProps }>
				<span { ...swatchSpanProps } >
					<Icon { ...iconProps } />
				</span>
			</li>
		);
	};

	const componentProps = {
		className: classnames( {
			'gform-input--swatch': true,
			...spacerClasses( spacing ),
		}, customClasses ),
		id,
		'data-js-setting-name': name,
		...customAttributes,
	};

	const swatchOptionsProps = {
		className: classnames( {
			'gform-input--swatch-options': true,
		} ),
	};

	const allSwatchesInputProps = {
		name: `${ name }-all-swatches`,
		defaultValue: JSON.stringify( customPaletteOptions ),
		id: `${ name }-all-swatches`,
		type: 'hidden',
		ref: allInputRef,
	};

	const pickerProps = {
		value: color || '#ffffff',
		onSave: handlePickerSave,
		onCancel: handlePickerCancel,
		triggerRef: currentRef,
		i18n: i18n?.colorPicker || {},
	};

	return (
		<div { ...componentProps } ref={ ref }>
			<div style={ { height: '0' } } />
			<ul { ...swatchOptionsProps }>
				{ palette.map( ( swatch, index ) => renderSwatchOption( swatch, index ) ) }
				{ customPaletteOptions.map( ( swatch, index ) => renderSwatchOption( swatch, index, true ) ) }
				{ allowNew && renderAddNewSwatchOption() }
			</ul>
			{ showPicker && <ColorPicker { ...pickerProps } /> }
			<input { ...allSwatchesInputProps } />
		</div>
	);
} );

Swatch.propTypes = {
	allowNew: PropTypes.bool,
	customAttributes: PropTypes.object,
	customClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
	i18n: PropTypes.object,
	id: PropTypes.string,
	labelAttributes: PropTypes.object,
	name: PropTypes.string,
	palette: PropTypes.array,
	paletteCustom: PropTypes.array,
	spacing: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.number,
		PropTypes.array,
		PropTypes.object,
	] ),
	value: PropTypes.string,
};

Swatch.displayName = 'Swatch';

export default Swatch;