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 } } );
}
}