import {
consoleInfo,
getNode,
objectToAttributes,
spacerClasses,
trigger,
uniqueId,
viewport,
} from '@gravityforms/utils';
/**
* @function loaderTemplate
* @description Generates the markup for a loader in the admin.
*
* @since 1.1.16
*
* @param {object} options The options for the component template.
* @param {string} options.background The background color for the loader.
* @param {object} options.customAttributes Any custom attributes.
* @param {Array} options.customClasses An array of additional classes for the toggle.
* @param {boolean} options.displayText Whether to display the loader text.
* @param {string} options.foreground The foreground color for the loader.
* @param {string} options.id Id for the loader, auto generated if not passed.
* @param {string} options.mask The mask for the loader.
* @param {string} options.maskTheme The mask theme for the loader.
* @param {string} options.position The position for the loader.
* @param {string} options.size The size for the loader.
* @param {string|number|Array|object} options.spacing The spacing for the component, string, number, object or array.
* @param {string} options.text The text for the loader.
* @param {string} options.textColor The text color for the loader.
* @param {string} options.theme Theme for the toggle, primary or cosmos.
* @param {string} options.type Type for the loader, spinner or bar.
* @param {object} options.wrapperCustomAttributes Any custom attributes for the textarea wrapper.
* @param {Array} options.wrapperCustomClasses Any custom classes for the textarea wrapper.
* @param {string} options.wrapperTagName Tag to use for the textarea wrapper. Defaults to 'div',
*
* @return {string}
* @example
* import { loaderTemplate } from '@gravityforms/components/html/admin/elements/Loader';
*
* function Example() {
* const loaderTemplateHTML = loaderTemplateTemplate( options );
* document.body.insertAdjacentHTML( 'beforeend', loaderTemplateHTML );
* }
*
*/
export const loaderTemplate = ( {
background = '#ecedf8',
customAttributes = {},
customClasses = [],
displayText = true,
foreground = '#242748',
id = '',
mask = true,
maskTheme = 'light',
position = 'center',
size = 5,
spacing = '',
text = '',
textColor = '#000',
theme = 'primary',
type = 'simple',
wrapperCustomAttributes = {},
wrapperCustomClasses = [],
wrapperTagName = 'div',
} ) => {
const wrapperAttrs = objectToAttributes( {
...wrapperCustomAttributes,
id: `${ id }-mask`,
class: [
'gform-loader__mask',
`gform-loader__mask--theme-${ maskTheme }`,
`gform-loader__mask--theme-${ theme }`,
`gform-loader__mask--position-${ position }`,
...wrapperCustomClasses,
],
role: 'alert',
} );
const componentAttrs = objectToAttributes( {
...customAttributes,
id,
class: [
'gform-loader',
`gform-loader--${ type }`,
`gform-loader--theme-${ theme }`,
...Object.keys( spacerClasses( spacing ) ),
...customClasses,
],
} );
return `
${ mask ? `<${ wrapperTagName } ${ wrapperAttrs }>` : '' }
${ mask ? `<div id="${ id }-mask-positioner" class="gform-loader__mask-positioner">` : '' }
<span ${ componentAttrs }></span>
${ mask && text && displayText ? `<span id="${ id }-text" class="gform-loader__text">${ text }</span>` : '' }
${ mask && text && ! displayText ? `<span class="gform-visually-hidden">${ text }</span>` : '' }
${ mask ? `</div>` : '' }
${ mask ? `</${ wrapperTagName }>` : '' }
<style id="${ id }-style">
#${ id } {
${ type === 'simple' ? `
border-bottom-color: ${ foreground };
border-left-color: ${ foreground };
border-right-color: ${ background };
border-top-color: ${ background };
font-size: ${ size }px;
` : '' }
}
#${ id }-text {
${ textColor ? `color: ${ textColor };` : '' }
}
</style>
`;
};
/**
* @class Loader
* @description A loader component that can be used as a simple spinner or a full masked element with spinner and text as needed.
*
* @since 1.1.16
*
* @borrows loaderTemplate as loaderTemplate
*
* @param {object} options The options for the component.
* @param {string} options.background The background color for the loader.
* @param {object} options.customAttributes Any custom attributes for the component.
* @param {Array} options.customClasses An array of additional classes for the component.
* @param {boolean} options.displayNoneOnHide On hide should it use display none or just opacity?
* @param {boolean} options.displayText Whether to display the loader text.
* @param {string} options.foreground The foreground color for the loader.
* @param {string} options.id Id for the component, auto generated if not passed.
* @param {string} options.mask The mask for the loader.
* @param {string} options.maskTheme The mask theme for the loader.
* @param {string} options.position The position for the loader.
* @param {string} options.rendered Is the component already rendered in the dom, eg by php?
* @param {string} options.renderOnInit Render the component on init of the class?
* @param {string} options.showOnRender Show the component on render?
* @param {string} options.size The size for the loader.
* @param {string} options.spacing Spacing for the component.
* @param {string} options.target The target to render to. Any valid css selector string.
* @param {string} options.targetPosition The insert position for the component relative to the target.
* @param {string} options.text Text to show below the loader or for screen readers based on the displayText option if loader mask is enabled. Required for a11y if mask is enabled.
* @param {string} options.textColor The text color for the loader.
* @param {string} options.theme Theme for the component, primary or cosmos.
* @param {string} options.type Type for the loader, spinner or bar.
* @param {object} options.wrapperCustomAttributes Any custom attributes for the textarea wrapper.
* @param {Array} options.wrapperCustomClasses Any custom classes for the textarea wrapper.
* @param {string} options.wrapperTagName Tag to use for the textarea wrapper. Defaults to 'div',
*
* @return {Class} The class instance.
* @example
* import Loader from '@gravityforms/components/html/admin/elements/Loader';
*
* function Example() {
* const loaderInstance = new Loader( {
* id: 'example-loader',
* 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.
* loaderInstance.init();
* }
*
*/
export default class Loader {
constructor( options = {} ) {
this.options = {};
Object.assign(
this.options,
{
background: '#ecedf8', // background color for the loader
customAttributes: {},
customClasses: [],
displayNoneOnHide: true, // on hide should it use display none or just opacity?
displayText: true, // should we display the text or make it only available to screen readers if loader mask is enabled
foreground: '#242748', // the color of the loader
id: uniqueId( 'loader' ), // the id for the loader
mask: true, // should the loader mask an area? If false we assume the implementation will handle the details (layout, a11y, etc.).
maskTheme: 'light', // background mask theme, none, light, or dark
position: 'center', // vertical position, auto, top, center, bottom, or sticky if loader mask is enabled
rendered: false, // is the loader already rendered in the dom?
renderOnInit: true, // should we render this loader on init?
showOnRender: true, // visible on render?
size: 5, // size of the loader, decimal int values
target: '', // node to inject the loader to (uses insertAdjacentHtml)
targetPosition: 'afterbegin', // insert position for the loader markup
text: '', // text to show below the loader or for screen readers based on the displayText option if loader mask is enabled. Required for a11y if mask is enabled.
textColor: '#000', // text color
theme: 'cosmos', // theme for the loader, primary or cosmos
type: 'simple', // loader type
wrapperCustomAttributes: {},
wrapperCustomClasses: [],
wrapperTagName: 'div',
},
options
);
/**
* @event gform/loader/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/loader/pre_init', native: false, data: { instance: this } } );
this.elements = {};
if ( this.options.renderOnInit ) {
this.init();
}
}
/**
* @memberof Loader
* @description Sets the vertical position for the loader.
*
* @since 1.1.16
*
* @return {void}
*/
positionLoader() {
const { position, target } = this.options;
const targetNode = getNode( target, document, true );
const { maskPositioner } = this.elements;
const targetVisibleHeight = viewport.elVisibleHeight( targetNode );
const viewportHeight = viewport.height();
const { top } = targetNode.getBoundingClientRect();
let windowScroll = 0;
// Account for window scroll in our top positioning when:
// - position auto, target height greater than window height, and scrolled past top of target
// - position fixed, window height greater than target visible height, and haven't scrolled past top of target
if (
( position === 'auto' && ( targetNode.offsetHeight > viewportHeight ) && top < 0 ) ||
( position === 'sticky' && ( targetVisibleHeight < viewportHeight ) && top > 0 )
) {
windowScroll = Math.abs( targetNode.getBoundingClientRect().top );
}
maskPositioner.style.top = `${ windowScroll + ( ( targetVisibleHeight / 2 ) - ( maskPositioner.offsetHeight / 2 ) ) }px`;
}
/**
* @memberof Loader
* @description Removes the injected nodes from the dom.
*
* @since 1.1.16
*
* @return {void}
*/
removeLoader() {
const { loaderEl, style } = this.elements;
loaderEl.parentNode.removeChild( loaderEl );
style.parentNode.removeChild( style );
}
/**
* @memberof Loader
* @description Reveal the loader.
*
* @since 1.1.16
*
* @return {void}
*/
showLoader() {
const { mask, position } = this.options;
const { loaderEl } = this.elements;
loaderEl.style.display = '';
loaderEl.style.opacity = '';
if ( mask && ( position === 'auto' || position === 'sticky' ) ) {
this.positionLoader();
}
trigger( { event: 'gform/loader/post_show', native: false, data: { instance: this } } );
}
/**
* @memberof Loader
* @description Hide the loader.
*
* @fires gform/loader/post_hide
*
* @since 1.1.16
*
* @return {void}
*/
hideLoader() {
const { displayNoneOnHide } = this.options;
const { loaderEl } = this.elements;
if ( displayNoneOnHide ) {
loaderEl.style.display = 'none';
} else {
loaderEl.style.opacity = '0';
}
/**
* @event gform/loader/post_hide
* @type {object}
* @description Fired when the component has completed hiding in the dom.
*
* @since 1.1.16
*
* @property {object} instance The Component class instance.
*/
trigger( { event: 'gform/loader/post_hide', native: false, data: { instance: this } } );
}
/**
* @memberof Loader
* @description Handles initial visibility for the loader. If showOnRender is false, hides the loader.
*
* @since 1.1.16
*
* @return {void}
*/
setInitialUI() {
const { mask, position, showOnRender } = this.options;
if ( ! showOnRender ) {
this.hideLoader();
}
if ( showOnRender && mask && ( position === 'auto' || position === 'sticky' ) ) {
this.positionLoader();
}
}
/**
* @memberof Loader
* @description Store the elements on the elements object of this instance.
*
* @since 1.1.16
*
* @return {void}
*/
storeElements() {
const { id } = this.options;
this.elements = {
loader: getNode( `#${ id }`, document, true ),
mask: getNode( `#${ id }-mask`, document, true ),
maskPositioner: getNode( `#${ id }-mask-positioner`, document, true ),
style: getNode( `#${ id }-style`, document, true ),
};
this.elements.loaderEl = this.elements.mask ? this.elements.mask : this.elements.loader;
}
/**
* @memberof Loader
* @description Renders the component into the dom.
*
* @since 1.1.16
*
* @return {void}
*/
render() {
const { rendered, target, targetPosition } = this.options;
if ( ! rendered ) {
const renderTarget = getNode( target, document, true );
if ( this.options.mask ) {
renderTarget.style.position = 'relative';
}
renderTarget.insertAdjacentHTML(
targetPosition,
loaderTemplate( this.options )
);
}
this.elements.loader = getNode( `#${ this.options.id }`, document, true );
this.elements.wrapper = this.elements.loader.parentNode;
}
/**
* @memberof Loader
* @description Initialize the component.
*
* @fires gform/loader/post_render
*
* @since 1.1.16
*
* @return {void}
*/
init() {
this.render();
this.storeElements();
this.setInitialUI();
/**
* @event gform/loader/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/loader/post_render', native: false, data: { instance: this } } );
consoleInfo( `Gravity Forms Admin: Initialized loader component.` );
}
}