modules_Dialog_index.js

import {
	bodyLock,
	consoleInfo,
	focusLoop,
	getClosest,
	getNodes,
	trigger,
	uniqueId,
} from '@gravityforms/utils';

/**
 * @function dialogTemplate
 * @description The template function that returns html for the dialog. Options below are passed from constructor and
 * described there.
 *
 * @since 1.0.6
 *
 * @param {object}  options                         The options for the dialog.
 * @param {string}  options.alertButtonText         The text for the alert button.
 * @param {string}  options.cancelButtonText        The text for the cancel button.
 * @param {string}  options.closeButtonClasses      The classes for the close button. Space seperated string.
 * @param {string}  options.closeButtonAriaLabel    The aria label for the close button.
 * @param {string}  options.closeButtonSize         The size for the close button.
 * @param {string}  options.closeButtonTitle        The title for the close button.
 * @param {string}  options.closeButtonType         The type for the close button.
 * @param {string}  options.confirmButtonAttributes The attributes for the confirm button. Space seperated string.
 * @param {string}  options.confirmButtonIcon       The icon for the confirm button.
 * @param {string}  options.confirmButtonText       The text for the confirm button.
 * @param {string}  options.content                 The content for the dialog. HTML allowed.
 * @param {string}  options.id                      The id for the dialog.
 * @param {boolean} options.maskBlur                Whether to blur the background when the dialog is open.
 * @param {string}  options.maskClasses             The classes for the mask. Space seperated string.
 * @param {boolean} options.maskTheme               Background mask theme, `none`, `light` or `dark`.
 * @param {string}  options.mode                    The mode for the dialog. `alert`, `confirm` or `dialog`.
 * @param {string}  options.position                The position for the dialog. `center`, `top` or `bottom`.
 * @param {string}  options.title                   The title for the dialog.
 * @param {string}  options.titleIcon               The icon for the title.
 * @param {string}  options.titleIconColor          The color for the title icon.
 * @param {string}  options.wrapperClasses          The classes for the wrapper. Space seperated string.
 * @param {number}  options.zIndex                  The z-index for the dialog.
 *
 * @return string
 * @example
 * import { dialogTemplate } from '@gravityforms/components/html/admin/modules/Dialog';
 *
 * function Example() {
 *      const dialogHTML = dialogTemplate( options );
 *      document.body.insertAdjacentHTML( 'beforeend', dialogHTML );
 * }
 *
 */
export const dialogTemplate = ( {
	alertButtonText = '',
	cancelButtonText = '',
	closeButtonAriaLabel = '',
	closeButtonClasses = '',
	closeButtonSize = 'md',
	closeButtonTitle = '',
	closeButtonType = 'circular',
	confirmButtonAttributes = '',
	confirmButtonIcon = '',
	confirmButtonText = '',
	content = '',
	id = '',
	maskBlur = true,
	maskClasses = '',
	maskTheme = 'none',
	mode = '',
	position = 'fixed',
	title = '',
	titleIcon = '',
	titleIconColor = '',
	wrapperClasses = '',
	zIndex = 10,
} ) =>
	`
	<div class="${ maskClasses } gform-dialog__mask--position-${ position } gform-dialog__mask--theme-${ maskTheme }${ maskBlur ? ` gform-dialog__mask--blur` : '' }" data-js="gform-dialog-mask" style="z-index: ${ zIndex };">
		<article 
			id="${ id }" 
			class="${ wrapperClasses }"
			data-js="${ id }"
		>
			<button 
				class="gform-dialog__close ${ closeButtonClasses } gform-button ${ ( closeButtonType === 'circular' ) ? 'gform-button--secondary' : '' } gform-button--${ closeButtonType } gform-button--size-${ closeButtonSize }"
				data-js="gform-dialog-close"
				style="z-index: ${ zIndex + 1 };"
				title="${ closeButtonTitle }"
				aria-label="${ closeButtonAriaLabel }"
			>
				<span class="gform-button__icon gform-common-icon gform-common-icon--x"></span>
			</button>
			${ title ? '<header class="gform-dialog__head" data-js="gform-dialog-header">' : '' }
			${ title ? `<h5 class="gform-dialog__title${ titleIcon ? ` gform-dialog__title--has-icon` : '' }">${ titleIcon ? `<span class="gform-dialog__title-icon gform-icon gform-icon--${ titleIcon }"${ titleIconColor ? ` style="color: ${ titleIconColor };"` : '' }></span>` : '' }${ title }</h5>` : '' }
			${ title ? '</header>' : '' }
			<div class="gform-dialog__content" data-js="gform-dialog-content">${ content }</div>
			${ ( mode === 'dialog' || mode === 'alert' ) ? '<footer class="gform-dialog__footer" data-js="gform-dialog-footer">' : '' }
			${ mode === 'dialog' ? `
				<button
					class="gform-dialog__cancel gform-button gform-button--white"
					data-js="gform-dialog-cancel"
				>
					${ cancelButtonText }
				</button>
				<button
					id="${ id }-dialog-confirm-button"
					class="gform-dialog__confirm gform-button gform-button--primary-new${ confirmButtonIcon ? ` gform-button--icon-leading` : '' }"
					data-js="gform-dialog-confirm"
					${ confirmButtonAttributes }
				>
					${ confirmButtonIcon ? `<span class="gform-button__icon gform-icon gform-icon--${ confirmButtonIcon }"></span>` : '' }${ confirmButtonText }
				</button>
			` : '' }
			${ mode === 'alert' ? `
				<button
					class="gform-dialog__alert gform-button gform-button--primary-new"
					data-js="gform-dialog-alert"
				>
					${ alertButtonText }
				</button>
			` : '' }
			${ ( mode === 'dialog' || mode === 'alert' ) ? '</footer>' : '' }
		</article>
	</div>
	`;

/**
 * @class Dialog
 * @description A dialog component to house modals, dialogs or prompts.
 *
 * @since 1.0.6
 *
 * @borrows dialogTemplate as dialogTemplate
 *
 * @param {object}   options                         The options for the dialog.
 * @param {string}   options.alertButtonText         The text for the alert button.
 * @param {string}   options.cancelButtonText        The text for the cancel button.
 * @param {string}   options.closeButtonClasses      The classes for the close button. Space seperated string.
 * @param {string}   options.closeButtonTitle        The title for the close button.
 * @param {string}   options.confirmButtonAttributes The attributes for the confirm button. Space seperated string.
 * @param {string}   options.confirmButtonIcon       The icon for the confirm button.
 * @param {string}   options.confirmButtonText       The text for the confirm button.
 * @param {string}   options.content                 The content for the dialog. HTML allowed.
 * @param {string}   options.id                      The id for the dialog.
 * @param {boolean}  options.lockBody                Whether to lock the body behind the dialog
 * @param {boolean}  options.maskBlur                Whether to blur the background when the dialog is open.
 * @param {string}   options.maskClasses             The classes for the mask. Space seperated string.
 * @param {boolean}  options.maskTheme               Background mask theme, `none`, `light` or `dark`.
 * @param {string}   options.mode                    The mode for the dialog. `alert`, `confirm` or `dialog`.
 * @param {Function} options.onClose                 Function to fire when closed
 * @param {Function} options.onConfirm               Function to fire when confirm button is clicked in dialog mode
 * @param {Function} options.onOpen                  Function to fire when opened
 * @param {string}   options.position                The position for the dialog. `center`, `top` or `bottom`.
 * @param {boolean}  options.renderOnInit            Render on initialization?
 * @param {string}   options.target                  Append target for the dialog and its mask. Uses querySelectorAll
 * @param {string}   options.title                   The title for the dialog.
 * @param {string}   options.titleIcon               The icon for the title.
 * @param {string}   options.titleIconColor          The color for the title icon.
 * @param {string}   options.triggers                The optional selector[s] of the trigger that shows it
 * @param {string}   options.wrapperClasses          The classes for the wrapper. Space seperated string.
 * @param {number}   options.zIndex                  The z-index for the dialog.
 *
 * @return {Class} The class instance.
 * @example
 * import Dialog from '@gravityforms/components/html/admin/modules/Dialog';
 *
 * function Example() {
 *      const dialogInstance = new Dialog( {
 *          id: 'example-dialog',
 *          renderOnInit: false,
 *          target: '#example-target',
 *          targetPosition: 'beforeend',
 *      } );
 *
 *      // Some time later we can render it. This is only done if we set renderOnInit to false.
 *      // If true it will render on initialization.
 *      dialogInstance.init();
 * }
 *
 */
export default class Dialog {
	constructor( options = {} ) {
		this.options = {};
		Object.assign(
			this.options,
			{
				alertButtonText: '', // text for the ok button if in alert mode
				animationDelay: 250, // animationDelay for the dialog
				cancelButtonText: '', // text for the cancel button if in dialog mode
				closeButtonClasses: 'gform-dialog__close', // classes for the close button
				closeButtonTitle: '', // text for the close button title
				closeOnMaskClick: true, // does clicking the mask close the dialog?
				closeOnConfirmClick: true, // does clicking the confirm button close the dialog?
				confirmButtonAttributes: '', // arbitrary additional attributes for the confirm button.
				confirmButtonIcon: '', // if in dialog mode, the optional confirmation button icon before the button text
				confirmButtonText: '', // the text for teh confirmation button if in dialog mode
				id: uniqueId( 'dialog' ), // id for the dialog
				lockBody: false, // whether to lock the body behind the dialog
				maskBlur: true, // whether the background mask has a blur effect or not
				maskClasses: 'gform-dialog__mask', // classes for the mask
				maskTheme: 'light', // background mask theme, none, light or dark
				mode: '', // mode for the dialog: can be modal, alert or dialog
				onClose: () => {}, // function to fire when closed
				onConfirm: () => {}, // function to fire when confirm button is clicked in dialog mode
				onOpen: () => {}, // function to fire when opened
				position: 'fixed', // fixed or absolute position?
				renderOnInit: true, // render on initialization?
				target: 'body', // append target for the dialog and its mask. Uses querySelectorAll
				title: '', // the optional title for the dialog
				titleIcon: '', // the optional title icon for the dialog
				titleIconColor: '', // the optional title icon color for the dialog
				triggers: '', // the optional selector[s] of the trigger that shows it
				wrapperClasses: 'gform-dialog', // classes for the wrapper
				zIndex: 10, // z-index for the dialog
			},
			options
		);

		/**
		 * @event gform/dialog/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/dialog/pre_init', native: false, data: { instance: this } } );

		this.elements = {};
		this.state = {
			open: false,
		};

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

	/**
	 * @memberof Dialog
	 * @description Opens the dialog and fires the onOpen function that can be passed in.
	 *
	 * @since 1.0.6
	 *
	 * @return {void}
	 */
	showDialog() {
		const { mask } = this.elements;
		if ( this.options.lockBody ) {
			bodyLock.lock();
		}
		this.options.onOpen();
		mask.classList.add( 'gform-dialog--anim-in-ready' );
		window.setTimeout( () => {
			mask.classList.add( 'gform-dialog--anim-in-active' );
		}, 25 );
		this.elements.closeButton.focus();
		this.state.open = true;
	}

	/**
	 * @memberof Dialog
	 * @description Closes the dialog and fires the onClose function that can be passed in. Can be used by external
	 * developers by firing the method on the instance. Also closes all instances if the event 'gform/dialog/close-all' is
	 * fired on the document.
	 *
	 * @since 1.0.6
	 *
	 * @return {void}
	 */
	closeDialog = () => {
		const { mask } = this.elements;
		const { animationDelay, onClose } = this.options;
		if ( ! mask.classList.contains( 'gform-dialog--anim-in-active' ) ) {
			return;
		}

		mask.classList.remove( 'gform-dialog--anim-in-active' );

		window.setTimeout( () => {
			mask.classList.remove( 'gform-dialog--anim-in-ready' );
		}, animationDelay );

		this.state.open = false;
		if ( this.elements.activeTrigger ) {
			this.elements.activeTrigger.focus();
		}
		if ( this.options.lockBody ) {
			bodyLock.unlock();
		}
		onClose();
	};

	/**
	 * @param  e
	 * @memberof Dialog
	 * @description Closes all instances except the one that gets passed the activeId when the event 'gform/dialog/close' is
	 * fired on the document.
	 *
	 * @since 1.0.6
	 *
	 * @return {void}
	 */
	maybeCloseDialog = ( e ) => {
		if ( e.detail?.activeId === this.options.id ) {
			return;
		}

		this.closeDialog();
	};

	/**
	 * @memberof Dialog
	 * @description Handles accessibility focus looping on the dialog using the focusLoop util.
	 *
	 * @since 1.0.6
	 *
	 * @param {PointerEvent} e The event object.
	 *
	 * @return {void}
	 */
	handleKeyEvents = ( e ) =>
		focusLoop(
			e,
			this.elements.activeTrigger,
			this.elements.dialog,
			this.closeDialog
		);

	/**
	 * @memberof Dialog
	 * @description Handles opening/closing the dialog on a trigger click.
	 *
	 * @since 1.0.6
	 *
	 * @param {PointerEvent} e The event object.
	 *
	 * @return {void}
	 */
	handleTriggerClick = ( e ) => {
		this.elements.activeTrigger = e.target;
		if ( this.state.open ) {
			this.closeDialog();
		} else {
			this.showDialog();
		}
	};

	/**
	 * @memberof Dialog
	 * @description Handles closing the dialog on mask click.
	 *
	 * @since 1.0.6
	 *
	 * @param {PointerEvent} e The event object.
	 *
	 * @return {void}
	 */
	handleMaskClick = ( e ) => {
		if ( e.target.id === this.options.id || getClosest( e.target, `[data-js="${ this.options.id }"]` ) ) {
			return;
		}
		this.closeDialog();
	};

	/**
	 * @memberof Dialog
	 * @description Handles a confirm button click and closes if the option is true.
	 *
	 * @since 1.0.6
	 *
	 * @param {PointerEvent} e The event object.
	 *
	 * @return {void}
	 */
	handleConfirm = ( e ) => {
		const { onConfirm } = this.options;
		trigger( { event: 'gform/dialog/confirm', native: false, data: { instance: this, button: e.target } } );

		if ( this.options.closeOnConfirmClick ) {
			this.closeDialog();
		}

		onConfirm();
	};

	/**
	 * @memberof Dialog
	 * @description Stores useful HTMLElements on the instance in the elements namespace after render
	 *
	 * @since 1.0.6
	 *
	 * @return {void}
	 */
	storeElements() {
		const dialog = getNodes( this.options.id )[ 0 ];
		this.elements = {
			activeTrigger: null,
			alertButton: getNodes( 'gform-dialog-alert', false, dialog )[ 0 ],
			content: getNodes( 'gform-dialog-content', false, dialog )[ 0 ],
			cancelButton: getNodes( 'gform-dialog-cancel', false, dialog )[ 0 ],
			closeButton: getNodes( 'gform-dialog-close', false, dialog )[ 0 ],
			confirmButton: getNodes( 'gform-dialog-confirm', false, dialog )[ 0 ],
			dialog,
			footer: getNodes( 'gform-dialog-footer', false, dialog )[ 0 ],
			header: getNodes( 'gform-dialog-header', false, dialog )[ 0 ],
			mask: dialog.parentNode,
			triggers: this.options.triggers ? getNodes( this.options.triggers, true, document, true ) : [],
		};
	}

	/**
	 * @memberof Dialog
	 * @description Renders the component into the dom.
	 *
	 * @since 1.0.6
	 *
	 * @return {void}
	 */
	render() {
		const { target } = this.options;
		const renderTarget = getNodes( target, false, document, true )[ 0 ];

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

	/**
	 * @memberof Dialog
	 * @description Binds the events for this component.
	 *
	 * @since 1.0.6
	 *
	 * @return {void}
	 */
	bindEvents() {
		this.elements.dialog.addEventListener( 'keydown', this.handleKeyEvents );
		this.elements.closeButton.addEventListener( 'click', this.closeDialog );

		if ( this.options.triggers ) {
			getNodes( this.options.triggers, true, document, true )
				.forEach( ( t ) => t.addEventListener( 'click', this.handleTriggerClick ) );
		}

		if ( this.options.closeOnMaskClick ) {
			this.elements.mask.addEventListener( 'click', this.handleMaskClick );
		}

		if ( this.elements.alertButton ) {
			this.elements.alertButton.addEventListener( 'click', this.closeDialog );
		}

		if ( this.elements.cancelButton ) {
			this.elements.cancelButton.addEventListener( 'click', this.closeDialog );
		}

		if ( this.elements.confirmButton ) {
			this.elements.confirmButton.addEventListener( 'click', this.handleConfirm );
		}

		document.addEventListener( 'gform/dialog/close', this.maybeCloseFlyout );
		document.addEventListener( 'gform/dialog/close-all', this.closeFlyout );
	}

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

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

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