elements_Select_index.js

import { consoleInfo, getNode, objectToAttributes, trigger, uniqueId, isObject, isEmptyObject, spacerClasses } from '@gravityforms/utils';
import { helpTextTemplate } from '../HelpText/index';
import { labelTemplate } from '../Label/index';

/**
 * @function selectTemplate
 * @description A select component to use wherever a simple select is needed.
 *
 * @since 2.2.0
 *
 * @param {object}              options                              The options for the select component.
 * @param {object}              options.customAttributes             Custom attributes for the select.
 * @param {Array}               options.customClasses                An array of additional classes for the select.
 * @param {boolean}             options.disabled                     If select is disabled.
 * @param {string}              options.helpTextAttributes           Custom attribute for the help text.
 * @param {string}              options.helpTextPosition             The position of the help text. Above or below.
 * @param {string}              options.id                           Optional id. Auto generated if not passed.
 * @param {string}              options.initialValue                 Initial value for the select.
 * @param {string}              options.labelAttributes              Any custom attributes for the label.
 * @param {string}              options.name                         The name attribute for the select.
 * @param {Array}               options.options                      An array of options, each option as an object in the following structure:
 *                                                                   {
 *                                                                   customOptionAttrs: {}, // Key-value pairs of custom attributes.
 *                                                                   customOptionClasses: [], // Array of strings of custom classes.
 *                                                                   label: '', // Label as a string.
 *                                                                   value: '', // Value as a string.
 *                                                                   }
 * @param {string}              options.size                         The select size. Small (size-s), Regular (size-r), or extra large (size-xl).
 * @param {string|number|Array} options.spacing                      The spacing for the component, string or array.
 * @param {string}              options.theme                        The theme of the select.
 * @param                       options.customAttributes.optionGroup
 * @param                       options.customAttributes.groups
 * @param {object}              options.wrapperAttributes            Any custom attributes for the wrapper element.
 * @param {Array}               options.wrapperClasses               Any custom classes for the wrapper element.
 * @param {object}              options.wrapperTagName               Tag to use for the textarea wrapper. Defaults to 'div'.
 * @param {string}              options.ariaLabel                    The aria-label text for the select element.
 *
 * @return {string} The html for the select component.
 * @example
 * import { selectTemplate } from '@gravityforms/components/html/admin/elements/Select';
 *
 * function Example() {
 *      const selectTemplateHTML = selectTemplate( options );
 *      document.body.insertAdjacentHTML( 'beforeend', selectTemplateHTML );
 * }
 *
 */
export const selectTemplate = ( {
	customAttributes = {},
	customClasses = [],
	disabled = false,
	helpTextAttributes = {},
	helpTextPosition = 'below',
	id = uniqueId( 'gform-admin-select' ),
	initialValue = '',
	labelAttributes = {},
	name = '',
	options = [],
	size = 'size-r',
	spacing = '',
	theme = 'cosmos',
	wrapperAttributes = {},
	wrapperClasses = [],
	wrapperTagName = 'div',
	ariaLabel = '',
} ) => {
	const inputId = id || uniqueId( 'gform-select' );
	const helpTextId = `${ inputId }-help-text`;

	const wrapperAttrs = objectToAttributes( {
		...wrapperAttributes,
		class: [
			'gform-input-wrapper',
			`gform-input-wrapper--theme-${ theme ? theme : 'cosmos' }`,
			'gform-input-wrapper--select',
			`gform-input-wrapper--${ size ? size : 'size-r' }`,
			disabled && 'gform-input-wrapper--disabled',
			...Object.keys( spacerClasses( spacing ) ),
			...wrapperClasses,
		],
	} );

	const selectObj = {
		...customAttributes,
		class: [
			'gform-select',
			...customClasses,
		],
		id: inputId,
		name,
		value: initialValue,
	};

	if ( disabled ) {
		selectObj.disabled = true;
	}
	if ( helpTextAttributes.content ) {
		selectObj[ 'aria-describedby' ] = helpTextId;
	}

	const selectAttrs = objectToAttributes( selectObj );

	if ( ariaLabel ) {
		selectObj[ 'aria-label' ] = ariaLabel;
	}

	const labelObj = {
		...labelAttributes,
		htmlFor: inputId,
	};

	const helpTextObj = {
		...helpTextAttributes,
		id: helpTextId,
	};

	const getSubOptions = ( choices = [] ) => {
		if ( isObject( choices ) && ! isEmptyObject( choices ) ) {
			return Object.entries( choices ).map( ( [ key ] ) => choices[ key ] );
		}
		return choices;
	};

	const getOption = ( {
		customOptionAttributes = {},
		customOptionClasses = [],
		label = '',
		value = '',
	} ) => {
		const optionObj = {
			...customOptionAttributes,
			class: [
				'gform-select__option',
				...customOptionClasses,
			],
			id: inputId,
			name,
			value,
		};
		return `
			<option ${ objectToAttributes( optionObj ) }>
				${ label }
			</option>
		`;
	};

	const getOptions = options.map( ( data ) => {
		const subOptions = getSubOptions( data.choices );
		return subOptions.length ? `
			<optgroup label="${ data.label }">
				${ subOptions.map( ( subData ) => getOption( subData ) ) }
			</optgroup>
		` : getOption( data );
	} );
	const helpText = helpTextTemplate( helpTextObj );
	return `
		<${ wrapperTagName } ${ wrapperAttrs }>
			${ labelTemplate( labelObj ) }
			${ helpTextPosition === 'above' ? helpText : '' }
			<div class="gform-select__wrapper">
				<select ${ selectAttrs }>
					${ getOptions }
				</select>
			</div>
			${ helpTextPosition === 'below' ? helpText : '' }
		</${ wrapperTagName }>
	`;
};

/**
 * @class Select
 * @description A select component to use wherever a simple select is needed.
 *
 * @since 2.2.0
 *
 * @param {object}                     options                    Component options.
 * @param {object}                     options.customAttributes   Custom attributes for the component.
 * @param {string|Array|object}        options.customClasses      Custom classes for the component.
 * @param {boolean}                    options.disabled           If select is disabled.
 * @param {string}                     options.helpTextAttributes Custom attribute for the help text.
 * @param {string}                     options.helpTextPosition   The position of the help text. Above or below.
 * @param {string}                     options.id                 Optional id. Auto generated if not passed.
 * @param {string}                     options.initialValue       Initial value for the select.
 * @param {string}                     options.labelAttributes    Any custom attributes for the label.
 * @param {string}                     options.name               The name attribute for the select.
 * @param {Array}                      options.options            An array of options, each option as an object in the following structure:
 *                                                                {
 *                                                                customOptionAttrs: {}, // Key-value pairs of custom attributes.
 *                                                                customOptionClasses: [], // Array of strings of custom classes.
 *                                                                label: '', // Label as a string.
 *                                                                value: '', // Value as a string.
 *                                                                }
 * @param {string}                     options.size               The select size. Regular (size-r) or extra large (size-xl).
 * @param {boolean}                    options.rendered           Is this select already rendered in the dom, eg by php?
 * @param {boolean}                    options.renderOnInit       Render the select on init of the class?
 * @param {string|number|Array|object} options.spacing            The spacing for the component, as a string, number, array, or object.
 * @param {string}                     options.theme              The theme of the select.
 * @param {string}                     options.target             The target to render to. Any valid css selector string.
 * @param {string}                     options.targetPosition     The insert position for the target.
 * @param {object}                     options.wrapperAttributes  Custom attributes for the wrapper element.
 * @param {string|Array|object}        options.wrapperClasses     Custom classes for the wrapper element.
 * @param {object}                     options.wrapperTagName     Tag to use for the textarea wrapper. Defaults to `div`.
 * @param {string}                     options.ariaLabel          The aria-label text for the select element.
 *
 * @return {Class} The select instance.
 * @example
 * import Select from '@gravityforms/components/html/admin/elements/Select';
 *
 * function Example() {
 *      const selectInstance = new Select( {
 *          name: 'select-name'
 *          options: { [
 *             {
 *                 label: 'Option 1',
 *                 value: 'option-1',
 *             },
 *             {
 *                 label: 'Option 2',
 *                 value: 'option-2',
 *             },
 *             {
 *                 label: 'Option 3',
 *                 value: 'option-3',
 *             },
 *         ] },
 *         target: '#select-target'
 *      } );
 * }
 *
 */
export default class Select {
	/**
	 * @description The class constructor.
	 *
	 * @param {object} options The options object. Check defaults for descriptions.
	 */
	constructor( options = {} ) {
		this.options = {};
		Object.assign(
			this.options,
			{
				customAttributes: {},
				customClasses: [],
				disabled: false,
				helpTextAttributes: {},
				helpTextPosition: 'below',
				id: '',
				initialValue: '',
				labelAttributes: {},
				name: '',
				options: [],
				rendered: false,
				renderOnInit: true,
				size: 'size-r',
				spacing: '',
				theme: 'cosmos',
				target: '',
				targetPosition: 'afterbegin',
				wrapperAttributes: {},
				wrapperClasses: [],
				wrapperTagName: 'div',
				ariaLabel: '',
			},
			options
		);

		trigger( { event: 'gform/select/pre_init', native: false, data: { instance: this } } );

		this.elements = {};

		if ( this.options.renderOnInit ) {
			this.init();
		}
	}

	/**
	 * @function render
	 * @description Renders the component into the dom.
	 *
	 * @since 1.0.0
	 *
	 * @return {void}
	 */
	render() {
		const { rendered, target, targetPosition } = this.options;

		if ( ! rendered ) {
			const renderTarget = getNode( target, document, true );

			renderTarget.insertAdjacentHTML(
				targetPosition,
				selectTemplate( this.options )
			);
		}

		this.elements.select = getNode( `#${ this.options.id }`, document, true );
	}

	/**
	 * @function init
	 * @description Initialize the component.
	 *
	 * @since 1.0.0
	 *
	 * @return {void}
	 */
	init() {
		this.render();

		trigger( { event: 'gform/select/post_render', native: false, data: { instance: this } } );

		consoleInfo( `Gravity Forms Admin: Initialized select component.` );
	}
}