modules_ColorPicker_index.js

import { React, PropTypes, classnames, HexColorPicker, HexColorInput } from '@gravityforms/libraries';
import { spacerClasses, uniqueId } from '@gravityforms/utils';
import Label from '../../elements/Label';
import Button from '../../elements/Button';
import { hexToRGB } from '../../utils/colors';

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

/**
 * @module ColorPicker
 * @description A color picker with HEX and RBG support.
 *
 * @since 1.1.15
 *
 * @param {object}                     props                  Component props.
 * @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.rgbIdPrefix      Custom prefix for the individual input IDs.
 * @param {Function}                   props.onCancel         Custom callback to fire when the picker is closed.
 * @param {Function}                   props.onChange         Custom callback to fire when the picker is changed.
 * @param {Function}                   props.onSave           Custom callback to fire when the picker is saved/applied.
 * @param {string|number|Array|object} props.spacing          The spacing for the component, as a string, number, array, or object.
 * @param {object}                     props.triggerRef       Ref object representing the trigger used to display this picker.
 * @param {string}                     props.value            The initial value to use when the picker is instantiated.
 *
 * @return {object} The ColorPicker component.
 *
 * @example
 * import ColorPicker from '@gravityforms/components/react/admin/modules/ColorPicker';
 *
 * return ( <ColorPicker onSave={ () => { handleSave(); } } value="#000" /> );
 *
 */
const ColorPicker = ( {
	customAttributes = {},
	customClasses = [],
	i18n = {},
	rgbIdPrefix = '',
	onCancel = () => {},
	onChange = () => {},
	onSave = () => {},
	spacing = '',
	triggerRef = { current: null },
	value = '#315f92',
} ) => {
	const [ color, setColor ] = useState( value );
	const [ rgbColor, setRgbColor ] = useState( hexToRGB( value ) );
	const [ position, setPosition ] = useState( false );
	const [ coords, setCoords ] = useState( false );
	const rgbInputIdPrefix = rgbIdPrefix || uniqueId( 'rgb-input-' );

	const selfRef = useRef( null );

	useEffect( () => {
		setRgbColor( hexToRGB( color ) );
	}, [ color ] );

	// Update the position of the color picker when it is rendered/instantiated.
	useLayoutEffect( () => {
		const getPosition = () => {
			if ( ! selfRef.current || ! triggerRef.current ) {
				return 'above';
			}

			const picker = selfRef.current;
			const source = triggerRef.current;

			return ( source.getBoundingClientRect().top - 20 ) > picker.offsetHeight ? 'above' : 'below';
		};

		setPosition( getPosition() );
		setCoords( {
			left: ( triggerRef?.current?.offsetLeft || 0 ) + ( triggerRef?.current?.offsetWidth / 2 || 0 ),
			top: getPosition() === 'above'
				? ( triggerRef?.current?.offsetTop || 0 ) - 10
				: ( triggerRef?.current?.offsetBottom || 0 ) + ( triggerRef?.current?.offsetHeight ) + 10,
		} );
	}, [ triggerRef ] );

	useEffect( () => {
		const handleOutsideClick = ( e ) => {
			if ( ! selfRef.current ) {
				return;
			}

			if ( selfRef.current.contains( e.target ) ) {
				return;
			}

			if ( triggerRef.current.contains( e.target ) ) {
				return;
			}

			handleClose();
		};

		document.addEventListener( 'click', handleOutsideClick );

		return () => document.removeEventListener( 'click', handleOutsideClick );
	} );

	/**
	 * @function handleOnChange
	 * @description Handler for the hex color picker change event.
	 *
	 * @since 1.1.15
	 *
	 * @param {string} newValue The new color value.
	 *
	 * @return {void}
	 */
	const handleOnChange = ( newValue ) => {
		setColor( newValue );
		onChange( newValue );
	};

	/**
	 * @function handleClose
	 * @description Handle the hex color picker close event.
	 *
	 * @since 1.1.15
	 *
	 * @return {void}
	 */
	const handleClose = () => {
		onCancel();
	};

	/**
	 * @function renderRGBInput
	 * @description Render the RGB input field.
	 *
	 * @since 1.1.15
	 *
	 * @param {string} colorPart The color value.
	 * @param {number} index     The index of the color.
	 *
	 * @return {JSX.Element}
	 */
	const renderRGBInput = ( colorPart, index ) => {
		const labelProps = {
			htmlFor: `${ rgbInputIdPrefix }-${ index }`,
		};

		const inputProps = {
			readOnly: true,
			value: colorPart,
			type: 'text',
			id: `${ rgbInputIdPrefix }-${ index }`,
			className: classnames( {
				'gform-input': true,
			} ),
		};

		const wrapperProps = {
			className: classnames( {
				'gform-input--picker-input': true,
				'gform-input--picker-input--rgb': true,
			} ),
			key: index,
		};

		return (
			<div { ...wrapperProps }>
				<Label { ...labelProps } label={ index } />
				<input { ...inputProps } />
			</div>
		);
	};

	const componentProps = {
		className: classnames( {
			'gform-input--picker': true,
			[ `gform-input--picker--pos-${ position }` ]: true,
			...spacerClasses( spacing ),
		}, customClasses ),
		style: {
			top: coords.top,
			left: coords.left,
		},
		ref: selfRef,
		...customAttributes,
	};

	const hexInputWrapperProps = {
		className: classnames( {
			'gform-input--picker-input': true,
		} ),
	};

	const hexInputProps = {
		color,
		onChange: handleOnChange,
		className: classnames( {
			'gform-input': true,
		} ),
		id: `${ rgbInputIdPrefix }-hex`,
		type: 'text',
	};

	const hexLabelProps = {
		label: i18n?.hex || '',
		htmlFor: `${ rgbInputIdPrefix }-hex`,
	};

	const saveButtonProps = {
		type: 'primary-new',
		label: i18n?.apply || '',
		onClick: () => onSave( color ),
		size: 'size-xs',
	};

	return (
		<div { ...componentProps }>
			<div className="gform-input__picker-ui">
				<HexColorPicker color={ color } onChange={ handleOnChange } />
				<div className="gform-input__picker-inputs">
					<div { ...hexInputWrapperProps } >
						<Label { ...hexLabelProps } />
						<HexColorInput { ...hexInputProps } />
					</div>
					{ Object.keys( rgbColor ).map( ( index ) => renderRGBInput( rgbColor[ index ], index ) ) }
				</div>
			</div>
			<div className="gform-input__picker-controls">
				<Button { ...saveButtonProps } />
			</div>
		</div>
	);
};

ColorPicker.propTypes = {
	customAttributes: PropTypes.object,
	customClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
	i18n: PropTypes.object,
	onCancel: PropTypes.func,
	onChange: PropTypes.func,
	onSave: PropTypes.func,
	spacing: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.number,
		PropTypes.array,
		PropTypes.object,
	] ),
	triggerRef: PropTypes.object,
	value: PropTypes.string,
};

export default ColorPicker;