elements_Button_index.js

import Loader from '../Loader';
import {
	consoleError,
	consoleInfo,
	deepMerge,
	getNode,
	objectToAttributes,
	spacerClasses,
	trigger,
	uniqueId,
} from '@gravityforms/utils';

/**
 * @function buttonTemplate
 * @description The template function used to generate our button html.
 *
 * @since 1.0.5
 *
 * @param {object}                     options               The options object for the class.
 * @param {string}                     options.activeText    If interactive, what is the active text when the button is active?
 * @param {string}                     options.activeType    What is the active type? Supports loading currently.
 * @param {string}                     options.attributes    Arbitrary attributes for the button.
 * @param {Array}                      options.customClasses Custom classes for the button.
 * @param {string}                     options.html          Arbitrary html to append inside the button.
 * @param {string}                     options.icon          Icon name if wishing to use an icon button.
 * @param {string}                     options.iconPosition  Icon position if using one. leading or trailing.
 * @param {string}                     options.id            Id for the button.
 * @param {boolean}                    options.interactive   Is the button interactive? If true the button has two states it transitions, set by activeType.
 * @param {string}                     options.label         The  label for the button. If interactive, the text displayed when inactive.
 * @param {boolean}                    options.round         Does the button have round corners?
 * @param {string}                     options.size          Size of the button.
 * @param {string|number|Array|object} options.spacing       The spacing for the component, string, number, object or array.
 * @param {string}                     options.type          The button type.
 *
 * @return {string} The button html.
 * @example
 * import { buttonTemplate } from '@gravityforms/components/html/admin/elements/Button';
 *
 * function Example() {
 *      const buttonHTML = buttonTemplate( options );
 *      document.body.insertAdjacentHTML( 'beforeend', buttonHTML );
 * }
 *
 */
export const buttonTemplate = ( {
	activeText = '',
	activeType = '',
	attributes = '',
	customClasses = [],
	html = '',
	icon = '',
	iconPosition = 'leading',
	id = uniqueId( 'button' ),
	interactive = false,
	label = '',
	round = false,
	size = 'size-r',
	spacing = '',
	type = 'primary',
} ) => {
	const componentAttrs = objectToAttributes( {
		id,
		class: [
			'gform-button',
			`gform-button--${ size }`,
			`gform-button--${ type }`,
			round ? 'gform-button--round' : '',
			interactive ? `gform-button--interactive` : '',
			activeType ? `gform-button--active-type-${ activeType }` : '',
			icon && iconPosition === 'leading' ? 'gform-button--icon-leading' : '',
			icon && iconPosition === 'trailing' ? 'gform-button--icon-trailing' : '',
			...Object.keys( spacerClasses( spacing ) ),
			...customClasses,
		],
	} );

	const iconHtml = icon ? `<i class="gform-button__icon gform-button__icon--inactive gform-icon gform-icon--${ icon }" data-js="button-icon"></i>` : '';

	return `
		<button ${ componentAttrs } ${ attributes }>
			${ icon && iconPosition === 'leading' ? iconHtml : '' }
			${ label ? `<span class="gform-button__text gform-button__text--inactive" data-js="button-inactive-text">${ label }</span>` : '' }
			${ interactive ? `<span class="gform-button__text gform-button__text--active" data-js="button-active-text">${ activeText }</span>` : '' }
			${ icon && iconPosition === 'trailing' ? iconHtml : '' }
			${ html }
		</button>
	`;
};

/**
 * @class Button
 * @description A button component that returns an instance method and event api.
 *
 * @since 1.0.5
 *
 * @borrows buttonTemplate as buttonTemplate
 *
 * @param {object}                     options                                 The options object for the class.
 * @param {string}                     options.activeText                      If interactive, what is the active text when the button is active?
 * @param {string}                     options.activeType                      What is the active type? Supports loading currently.
 * @param {string}                     options.attributes                      Arbitrary attributes for the button.
 * @param {Array}                      options.customClasses                   Custom classes for the button.
 * @param {boolean}                    options.disableWhileActive              If interactive, disable the button while active?
 * @param {string}                     options.html                            Arbitrary html to append inside the button.
 * @param {string}                     options.icon                            Icon name if wishing to use an icon button.
 * @param {string}                     options.iconPosition                    Icon position if using one. leading or trailing.
 * @param {string}                     options.id                              Id for the button.
 * @param {boolean}                    options.interactive                     Is the button interactive? If true the button has two states it transitions, set by activeType.
 * @param {boolean}                    options.interactiveOnClick              If interactive, does clicking the button swap the active state and fire the callbacks?
 * @param {string}                     options.label                           The  label for the button. If interactive, the text displayed when inactive.
 * @param {object}                     options.loaderOptions                   All valid options for the loader component if using an interactive loader button.
 * @param {string}                     options.loaderOptions.additionalClasses Additional classes for the loader element.
 * @param {string}                     options.loaderOptions.background        Background color for the loader.
 * @param {string}                     options.loaderOptions.foreground        The color of the loader.
 * @param {boolean}                    options.loaderOptions.mask              Should the loader mask an area?
 * @param {boolean}                    options.loaderOptions.showOnRender      Visible on render?
 * @param {number}                     options.loaderOptions.size              Size of the loader, decimal int values.
 * @param {boolean}                    options.lockSize                        If interactive, lock the width of the button when transitioning states?
 * @param {Function}                   options.onActive                        If interactive, a callback to fire when button goes into its active state.
 * @param {Function}                   options.onInactive                      If interactive, a callback to fire when button goes into its inactive state.
 * @param {boolean}                    options.rendered                        Is this button already rendered in the dom, eg by php?
 * @param {boolean}                    options.renderOnInit                    Render this button on init?
 * @param {boolean}                    options.round                           Does the button have round corners?
 * @param {string}                     options.size                            Size of the button.
 * @param {string|number|Array|object} options.spacing                         The spacing for the component, string, number, object or array.
 * @param {string}                     options.target                          The target to render to. Any valid css selector string.
 * @param {string}                     options.type                            The button type.
 *
 * @return {this}
 * @example
 * import Button from '@gravityforms/components/html/admin/elements/Button';
 *
 * function Example() {
 *      const buttonInstance = new Button( {
 *          id: 'example-button',
 *          renderOnInit: false,
 *          target: '#example-target',
 *          targetPosition: 'beforeend',
 *          theme: 'cosmos',
 *      } );
 *
 *      // Some time later we can render it. This is only done if we set renderOnInit to false.
 *      // If true it will render on initialization.
 *      buttonInstance.init();
 * }
 *
 */
export default class Button {
	constructor( options = {} ) {
		this.options = deepMerge(
			{
				activeText: '', // if interactive, what is the active text when the button is active?
				activeType: '', // what is the active type? Supports loading currently.
				attributes: '', // arbitrary attributes for the button.
				customClasses: [], // custom classes for the button.
				disableWhileActive: true, // if interactive, disable the button while active?
				html: '', // arbitrary html to append inside the button.
				icon: '', // icon name if wishing to use an icon button.
				iconPosition: 'leading', // icon position if using one. leading or trailing.
				id: uniqueId( 'button' ), // id for the button.
				interactive: false, // is the button interactive? If true the button has two states it transitions, set by activeType.
				interactiveOnClick: true, // If interactive, does clicking the button swap the active state and fire the callbacks?
				label: '', // the  label for the button. If interactive, teh text displayed when inactive.
				loaderOptions: { // all valid options for the loader component if using an interactive loader button.
					additionalClasses: 'gform-button__loader', // additional classes for the loader element.
					background: 'transparent', // background color for the loader.
					foreground: '#3e7da6', // the color of the loader.
					mask: false, // should the loader mask an area?
					showOnRender: false, // visible on render?
					size: 1, // size of the loader, decimal int values.
				},
				lockSize: true, // if interactive, lock the width of the button when transitioning states?
				onActive: () => {}, // if interactive, a callback to fire when button goes into its active state.
				onInactive: () => {}, // if interactive, a callback to fire when button goes into its inactive state.
				rendered: false, // is this button already rendered in the dom, eg by php?
				renderOnInit: true, // render this button on init?
				round: false, // does the button have round corners?
				size: 'size-r', // size of the button.
				target: '', // the target to render to. Any valid css selector string.
				type: 'primary', // the button type.
			},
			options,
		);

		if ( ! this.options.target && ! this.options.rendered ) {
			consoleError( 'You must supply a target to the button component.' );
			return;
		}

		/**
		 * @event gform/button/pre_init
		 * @type {object}
		 * @description Fired before the component has started any internal init functions. A great chance to augment the options.
		 *
		 * @since 1.1.16
		 *
		 * @property {object} instance The Component class instance.
		 */
		trigger( { event: 'gform/button/pre_init', native: false, data: { instance: this } } );

		this.elements = {};
		this.instances = {};
		this.state = {
			active: false,
		};

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

	/**
	 * @memberof Button
	 * @description If interactive, handles activating this button and swapping the ui state, plus
	 * executing any callbacks and firing useful events.
	 *
	 * @since 1.0.5
	 *
	 * @fires gform/button/activated
	 *
	 * @return {void}
	 */
	activateButton() {
		const { activeType, disableWhileActive, lockSize, onActive } = this.options;
		const { button } = this.elements;
		/**
		 * @event gform/button/activated
		 * @type {object}
		 * @description Fired when an interactive button is activated on document.
		 *
		 * @property {object} instance The Button class instance.
		 */
		trigger( { event: 'gform/button/activated', native: false, data: { instance: this } } );
		if ( lockSize ) {
			const rect = button.getBoundingClientRect();
			button.style.width = `${ rect.width }px`;
		}
		if ( disableWhileActive ) {
			button.disabled = true;
		}
		this.elements.button.classList.add( 'gform-button--activated' );
		if ( activeType === 'loader' ) {
			this.instances.loader.showLoader();
		}
		this.state.active = true;
		onActive( this );
	}

	/**
	 * @memberof Button
	 * @description If interactive, handles deactivating this button and swapping the ui state, plus
	 * executing any callbacks and firing useful events.
	 *
	 * @since 1.0.5
	 *
	 * @fires gform/button/deactivated
	 *
	 * @return {void}
	 */
	deactivateButton() {
		const { activeType, disableWhileActive, lockSize, onInactive } = this.options;
		const { button } = this.elements;
		/**
		 * @event gform/button/deactivated
		 * @type {object}
		 * @description Fired when an interactive button is deactivated on document.
		 *
		 * @property {object} instance The Button class instance.
		 */
		trigger( { event: 'gform/button/deactivated', native: false, data: { instance: this } } );
		this.elements.button.classList.remove( 'gform-button--activated' );
		if ( activeType === 'loader' ) {
			this.instances.loader.hideLoader();
		}
		if ( disableWhileActive ) {
			button.disabled = false;
		}
		if ( lockSize ) {
			button.style.width = '';
		}
		this.state.active = false;
		onInactive( this );
	}

	/**
	 * @memberof Button
	 * @description If interactive and button is not active, activate it.
	 *
	 * @since 1.1.16
	 *
	 * @return {void}
	 */
	handleButtonClick() {
		if ( this.state.active ) {
			return;
		}
		this.activateButton();
	}

	/**
	 * @memberof Button
	 * @description Stores useful HTMLElements on the instance in the elements namespace after render
	 *
	 * @since 1.1.16
	 *
	 * @return {void}
	 */
	storeElements() {
		const { button } = this.elements;
		const { activeText, icon, label } = this.options;
		if ( activeText ) {
			this.elements.activeText = getNode( 'button-active-text', button );
		}
		if ( icon ) {
			this.elements.icon = getNode( 'button-icon', button );
		}
		if ( label ) {
			this.elements.inactiveText = getNode( 'button-inactive-text', button );
		}
	}

	/**
	 * @memberof Button
	 * @description If interactive render the elements that are present on active state.
	 *
	 * @since 1.1.16
	 *
	 * @return {void}
	 */
	renderInteractive() {
		const { activeType, interactive, loaderOptions } = this.options;
		const { button } = this.elements;
		if ( ! interactive ) {
			return;
		}
		if ( activeType === 'loader' ) {
			loaderOptions.target = `#${ button.id }`;
			this.instances.loader = new Loader( loaderOptions );
		}
	}

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

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

			renderTarget.insertAdjacentHTML(
				'beforeend',
				buttonTemplate( this.options )
			);
		}

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

		this.renderInteractive();

		consoleInfo( `Gravity Forms Admin: Initialized button component on ${ target }.` );
	}

	/**
	 * @memberof Button
	 * @description Bind event handles for the button instance.
	 *
	 * @since 1.1.16
	 *
	 * @return {void}
	 */
	bindEvents() {
		const { interactive, interactiveOnClick } = this.options;
		if ( interactive && interactiveOnClick ) {
			this.elements.button.addEventListener( 'click', this.handleButtonClick.bind( this ) );
		}
	}

	/**
	 * @memberof Button
	 * @description Initialize the component.
	 *
	 * @fires gform/button/post_render
	 *
	 * @since 1.1.16
	 *
	 * @return {void}
	 */
	init() {
		this.render();
		this.storeElements();
		this.bindEvents();

		/**
		 * @event gform/button/post_render
		 * @type {object}
		 * @description Fired when the component has completed rendering and all class init functions have completed.
		 *
		 * @since 1.1.16
		 *
		 * @property {object} instance The Component class instance.
		 */
		trigger( { event: 'gform/button/post_render', native: false, data: { instance: this } } );
	}
}