UNPKG

@gravityforms/components

Version:

UI components for use in Gravity Forms development. Both React and vanilla js flavors.

603 lines (566 loc) 20.3 kB
import { consoleError, consoleInfo, delay, focusLoop, getClosest, getNodes, matchesOrContainedInSelectors, resize, simpleBar, trigger, uniqueId, viewport, } from '@gravityforms/utils'; /** * @function flyoutTemplate * @description The template function that returns html for the flyout. Options below are passed from constructor and * described there. * * @since 1.0.5 * * @param {object} options The options for the flyout. * @param {string} options.closeButtonClasses The classes for the close button. * @param {string} options.closeButtonTitle The title for the close button. * @param {string} options.content The content for the flyout. * @param {string} options.description The description for the flyout. Found under the title in the header. * @param {number} options.desktopWidth The flyout desktop width in percentage. * @param {string} options.direction Direction to fly in from, left or right. * @param {boolean} options.expandable Whether the flyout shows an extra trigger to allow expanding to a larger width. * @param {string} options.expandableTitle The title for the expandable trigger. * @param {string} options.id The id for the flyout. * @param {number} options.maxWidth The flyout max width in pixels. * @param {number} options.mobileBreakpoint The breakpoint for mobile in pixels. * @param {number} options.mobileWidth The flyout mobile width in percentage. * @param {string} options.position The position of the flyout, fixed or absolute positioning * @param {boolean} options.showDivider Whether to show the divider between the header and content. * @param {boolean} options.simplebar Whether to use simplebar for the content. * @param {string} options.title The title for the flyout. * @param {string} options.wrapperClasses The classes for the wrapper. * @param {number} options.zIndex The z-index for the flyout. * * @return {string} * @example * import { flyoutTemplate } from '@gravityforms/components/html/admin/modules/Flyout'; * * function Example() { * const flyoutTemplateHTML = flyoutTemplateTemplate( options ); * document.body.insertAdjacentHTML( 'beforeend', flyoutTemplateHTML ); * } * */ export const flyoutTemplate = ( { id = '', closeButtonClasses = '', closeButtonTitle = '', content = '', description = '', desktopWidth = 0, direction = '', expandable = false, expandableTitle = '', maxWidth = 0, mobileBreakpoint = 0, mobileWidth = 0, position = '', showDivider = true, simplebar = false, title = '', wrapperClasses = '', zIndex = 10, } ) => ` <article id="${ id }" class="${ wrapperClasses } gform-flyout--${ direction } gform-flyout--${ position } ${ showDivider ? 'gform-flyout--divider' : 'gform-flyout--no-divider' }${ description ? '' : ' gform-flyout--no-description' }" style="z-index: ${ zIndex };" data-js="${ id }" > ${ expandable ? ` <button class="gform-flyout__expand" style="z-index: ${ zIndex + 2 };" data-js="gform-flyout-expand" title="${ expandableTitle }" > <span class="gform-flyout__expand-icon gform-icon gform-icon--chevron"></span> </button> <div class="gform-flyout__expand-rail" style="z-index: ${ zIndex + 1 };"></div> ` : `` } <header class="gform-flyout__head"> <div class="gform-flyout__head-left"> ${ title ? `<h5 class="gform-flyout__title">${ title }</h5>` : '' } ${ description ? `<div class="gform-flyout__desc"><p>${ description }</p></div>` : '' } </div> <div class="gform-flyout__head-right"> <button class="${ closeButtonClasses } gform-button gform-button--secondary gform-button--circular gform-button--size-xs" data-js="gform-flyout-close" title="${ closeButtonTitle }" > <i class="gform-button__icon gform-icon gform-icon--delete"></i> </button> </div> </header> <div class="gform-flyout__body"${ simplebar ? ' data-simplebar' : '' }><div class="gform-flyout__body-inner" data-js="flyout-content">${ content }</div></div> </article> <style> #${ id } { max-width: ${ maxWidth ? `${ maxWidth }px` : 'none' }; width: ${ mobileWidth }%; } #${ id }.gform-flyout--expanded { width: ${ expandable ? `calc( ${ mobileWidth }% - 50px)` : `${ mobileWidth }%` }; } @media only screen and (min-width: ${ mobileBreakpoint }px) { #${ id } { width: ${ desktopWidth }%; } } </style> `; /** * @class Flyout * @description Embeds an html flyout component to house off canvas content that overlays a container pain from either the * right or the left. * * @since 1.0.5 * * @borrows flyoutTemplate as flyoutTemplate * * @param {object} options The options for the flyout. * @param {number} options.animationDelay Total runtime of close animation. must be synced with css. * @param {string} options.closeButtonClasses Classes for the close button. * @param {string} options.closeButtonTitle Text for the close button title. * @param {boolean} options.closeOnOutsideClick Close the flyout on a click outside of it? * @param {Array} options.closeOnOutsideClickExceptions Array of selectors to ignore when checking for outside clicks. * @param {string} options.content The html content. * @param {boolean} options.expandable Whether the flyout shows an extra trigger to allow expanding to a larger width (set below). * @param {string} options.expandableTitle Title/a11y text for the expandable button. * @param {number} options.expandableWidth Width to expand to if expandable is true. * @param {string} options.description The optional description for the flyout. * @param {number} options.desktopWidth Desktop width in percent. * @param {string} options.direction Direction to fly in from, left or right. * @param {string} options.id Id for the flyout. * @param {string} options.insertPosition Insert position relative to target. * @param {boolean} options.lockBody Whether to lock body scroll when open. * @param {number} options.maxWidth Max width in pixels. * @param {number} options.mobileBreakpoint Mobile breakpoint. * @param {number} options.mobileWidth Mobile width in percent. * @param {Function} options.onClose Function to fire when closed. * @param {Function} options.onOpen Function to fire when opened. * @param {string} options.position Fixed or absolute positioning. * @param {boolean} options.renderOnInit Render on initialization? * @param {boolean} options.showDivider Show the divider below optional title? * @param {boolean} options.simplebar Enable the simple bar ui for the body content scroll? * @param {string} options.target The selector to append the flyout to. * @param {string} options.title The optional title for the flyout. * @param {string} options.triggers The selector[s] of the trigger that shows it. * @param {string} options.wrapperClasses Additional classes for the wrapper. * @param {number} options.zIndex Z-index for the flyout. * * @return {Class} The class instance. * @example * import Flyout from '@gravityforms/components/html/admin/modules/Flyout'; * * function Example() { * const flyoutInstance = new Flyout( { * id: 'example-flyout', * 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. * flyoutInstance.init(); * } * */ export default class Flyout { constructor( options = {} ) { this.options = {}; Object.assign( this.options, { animationDelay: 170, // total runtime of close animation. must be synced with css closeButtonClasses: 'gform-flyout__close', // classes for the close button closeButtonTitle: '', // text for the close button title closeOnOutsideClick: true, // close the flyout on a click outside of it? closeOnOutsideClickExceptions: [], // array of selectors to ignore when clicking outside the flyout content: '', // the html content expandable: false, // whether the flyout shows an extra trigger to allow expanding to a larger width (set below) expandableTitle: '', // title/a11y text for the expandable button expandableWidth: 100, // width to expand to if expandable is true description: '', // the optional description for the flyout desktopWidth: 60, // desktop width in percent direction: 'right', // direction to fly in from, left or right id: uniqueId( 'flyout' ), // id for the flyout insertPosition: 'beforeend', // insert position relative to target lockBody: false, // whether to lock body scroll when open maxWidth: 850, // max width in pixels mobileBreakpoint: 768, // mobile breakpoint mobileWidth: 100, // mobile width in percent onClose: () => {}, // function to fire when closed onOpen: () => {}, // function to fire when opened position: 'fixed', // fixed or absolute positioning renderOnInit: true, // render on initialization? showDivider: true, // show the divider below optional title? simplebar: false, // enable the simple bar ui for the body content scroll? target: 'body', // the selector to append the flyout to title: '', // the optional title for the flyout triggers: '[data-js="gform-trigger-flyout"]', // the selector[s] of the trigger that shows it wrapperClasses: 'gform-flyout', // additional classes for the wrapper zIndex: 10, // z-index for the flyout }, options ); /** * @event gform/flyout/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/flyout/pre_init', native: false, data: { instance: this } } ); this.elements = {}; this.state = { expanded: false, open: false, unExpandedWidth: 0, }; if ( this.options.renderOnInit ) { this.init(); } } /** * @memberof Flyout * @description Opens the flyout and fires the onOpen function that can be passed in. * * @since 1.0.5 * * @return {void} */ showFlyout() { const { flyout } = this.elements; this.options.onOpen(); simpleBar.reInitChildren( flyout ); flyout.classList.add( 'gform-flyout--anim-in-ready' ); /** * @event gform/flyout/open * @type {object} * @description Fired when the flyout opens. * * @since 3.3.7 * * @property {object} instance The Component class instance. */ trigger( { event: 'gform/flyout/open', native: false, data: { instance: this } } ); window.setTimeout( () => { flyout.classList.add( 'gform-flyout--anim-in-active' ); }, 25 ); } /** * @memberof Flyout * @description Closes the flyout 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/flyout/close-all' is * fired on the document. * * @since 1.0.5 * * @return {void} */ closeFlyout = () => { const { flyout } = this.elements; const { animationDelay, onClose } = this.options; if ( ! flyout.classList.contains( 'gform-flyout--anim-in-active' ) ) { return; } flyout.classList.remove( 'gform-flyout--anim-in-active' ); window.setTimeout( () => { flyout.classList.remove( 'gform-flyout--anim-in-ready' ); }, animationDelay ); this.state.open = false; this.shrinkFlyout(); onClose(); /** * @event gform/flyout/close * @type {object} * @description Fired when the flyout closes. * * @since 3.3.7 * * @property {object} instance The Component class instance. */ trigger( { event: 'gform/flyout/close', native: false, data: { instance: this } } ); }; /** * @param e * @memberof Flyout * @description Closes all instances except the one that gets passed the activeId when the event 'gform/flyout/close' is * fired on the document. * * @since 1.0.5 * * @return {void} */ maybeCloseFlyout = ( e ) => { if ( e.detail?.activeId === this.options.id ) { return; } this.elements.flyout.classList.remove( 'anim-in-ready' ); this.elements.flyout.classList.remove( 'anim-in-active' ); this.elements.flyout.classList.remove( 'anim-out-ready' ); this.elements.flyout.classList.remove( 'anim-out-active' ); this.state.open = false; this.shrinkFlyout(); }; /** * @memberof Flyout * @description Hides the expander button if the flyout fills its available space. * * @since 1.0.5 * * @return {void} */ updateFlyoutWidth() { const { animationDelay, expandable } = this.options; if ( ! expandable || this.state.expanded ) { return; } const { flyout, expandableTrigger } = this.elements; // if resizeParent is set it means we are not position fixed, and hence use parent width, otherwise use viewport const containerWidth = this.elements.resizeParent ? this.elements.resizeParent.clientWidth : viewport.width(); const flyoutWidth = flyout.clientWidth; // include the width of the rail (buffer, 50px) outside of the flyout to determine if we need to show/hide if ( containerWidth <= flyoutWidth + 50 ) { flyout.classList.add( 'gform-flyout--hide-expander' ); window.setTimeout( () => { // set display none so focus is ignored on keyboard nav after animations run expandableTrigger.style.display = 'none'; }, animationDelay ); } else { expandableTrigger.style.display = ''; window.setTimeout( () => { flyout.classList.remove( 'gform-flyout--hide-expander' ); }, 20 ); } } /** * @memberof Flyout * @description Handles accessibility focus looping on the flyout using the focusLoop util. * * @since 1.0.5 * * @param {KeyboardEvent} e The event object. * * @return {void} */ handleKeyEvents = ( e ) => focusLoop( e, this.elements.activeTrigger, this.elements.flyout, this.closeFlyout ); /** * @memberof Flyout * @description Handles opening/closing the flyout on a trigger click. * * @since 1.0.5 * * @param {PointerEvent} e The event object. * * @return {void} */ handleTriggerClick = ( e ) => { this.elements.activeTrigger = e.target; if ( this.state.open ) { this.closeFlyout(); this.elements.activeTrigger.focus(); this.state.open = false; } else { this.showFlyout(); this.elements.closeButton.focus(); this.state.open = true; } }; /** * @memberof Flyout * @description Expands the flyout to the defined expandableWidth while removing the max-width setting. * * @since 1.0.5 * * @return {void} */ expandFlyout() { const { expandableWidth, expandable } = this.options; if ( ! expandable || this.state.expanded ) { return; } const { flyout } = this.elements; this.state.unExpandedWidth = flyout.clientWidth; flyout.style.width = `${ this.state.unExpandedWidth }px`; flyout.style.transition = 'none'; delay( () => { flyout.style.maxWidth = 'none'; }, 20 ).delay( () => { flyout.style.transition = ''; }, 20 ).delay( () => { flyout.style.width = `calc(${ expandableWidth }% - 50px)`; flyout.classList.add( 'gform-flyout--expanded' ); this.state.expanded = true; }, 20 ); } /** * @memberof Flyout * @description Returns the flyout to its natural width. The complexity comes from handling a mix of max-width * (which doesn't animate) and % widths for responsive. * * @since 1.0.5 * * @return {void} */ shrinkFlyout() { const { animationDelay, expandable } = this.options; if ( ! expandable || ! this.state.expanded ) { return; } const { flyout } = this.elements; flyout.style.width = `${ this.state.unExpandedWidth }px`; flyout.classList.remove( 'gform-flyout--expanded' ); window.setTimeout( () => { flyout.style.width = ''; flyout.style.maxWidth = ''; }, animationDelay ); this.state.expanded = false; } /** * @memberof Flyout * @description Handles expanding the flyout on a trigger click if the option is enabled. * * @since 1.0.5 * * @return {void} */ handleExpandable = () => { if ( this.state.expanded ) { this.shrinkFlyout(); } else { this.expandFlyout(); } }; /** * @memberof Flyout * @description Handle window resize events for the flyout * * @since 1.0.5 * * @return {void} */ handleResize = () => { this.updateFlyoutWidth(); }; /** * @memberof Flyout * @description Stores useful HTMLElements on the instance in the elements namespace after render. * * @since 1.0.5 * * @return {void} */ storeElements() { const flyout = getNodes( this.options.id )[ 0 ]; this.elements = { activeTrigger: null, content: getNodes( 'flyout-content', false, flyout )[ 0 ], closeButton: getNodes( 'gform-flyout-close', false, flyout )[ 0 ], expandableTrigger: this.options.expandable ? getNodes( 'gform-flyout-expand', false, flyout )[ 0 ] : null, flyout, resizeParent: this.options.position === 'fixed' ? null : flyout.parentNode, triggers: getNodes( this.options.triggers, true, document, true ), }; } /** * @memberof Flyout * @description Renders the component into the dom. * * @since 1.0.5 * * @return {void} */ render() { const target = document.querySelectorAll( this.options.target )[ 0 ]; if ( ! target ) { consoleError( `Flyout could not render as ${ this.options.target } could not be found.` ); return; } target.insertAdjacentHTML( this.options.insertPosition, flyoutTemplate( this.options ) ); consoleInfo( `Gravity Forms Admin: Initialized flyout component on ${ this.options.target }.` ); } /** * @memberof Flyout * @description Bind events to the component. * * @since 1.0.5 * * @return {void} */ bindEvents() { this.elements.flyout.addEventListener( 'keydown', this.handleKeyEvents ); this.elements.closeButton.addEventListener( 'click', this.closeFlyout ); getNodes( this.options.triggers, true, document, true ) .forEach( ( t ) => t.addEventListener( 'click', this.handleTriggerClick ) ); resize( this.handleResize ); document.addEventListener( 'gform/flyout/close', this.maybeCloseFlyout ); document.addEventListener( 'gform/flyout/close-all', this.closeFlyout ); if ( this.options.expandable ) { this.elements.expandableTrigger.addEventListener( 'click', this.handleExpandable ); } if ( this.options.closeOnOutsideClick ) { document.addEventListener( 'click', function( event ) { if ( this.elements.flyout.contains( event.target ) || ! this.state.open || getClosest( event.target, '#TB_window' ) || matchesOrContainedInSelectors( event.target, this.options.closeOnOutsideClickExceptions ) ) { return; } this.closeFlyout(); }.bind( this ) ); } } /** * @memberof Flyout * @description Initialize the component. * * @fires gform/flyout/post_render * * @since 1.0.5 * * @return {void} */ init() { this.render(); this.storeElements(); this.bindEvents(); this.updateFlyoutWidth(); /** * @event gform/flyout/post_render * @type {object} * @description Fired when the component has completed rendering and all class init functions have completed. * * @since 1.0.5 * * @property {object} instance The Component class instance. */ trigger( { event: 'gform/flyout/post_render', native: false, data: { instance: this } } ); } }