dom_animate.js

/**
 * @module animate
 * @description Animate dom elements using the web animation api. https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API
 * This set of animations is just a few opinionated animations which currently include things like fading or translateY.
 * You can either run them on a group, or individually. Configuration can be passed directly, or applied to data attributes on the target element.
 * Data attributes to be placed on the element if not using the options object are: `data-animation-delay`, `data-animation-duration`, `data-animation-easing`, `data-animation-types`, `data-translate-distance-from`, `data-translate-distance-to`.
 *
 * @since 1.5.0
 *
 */

/**
 * @function getAnimationKeyframes
 * @description Get the keyframes for the animation. Follows this format https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API/Keyframe_Formats
 *
 * @since 1.5.0
 *
 * @param {HTMLElement} target               The target element to animate.
 * @param {object}      options              The options for the animation that are needed to form the Keyframe object.
 * @param {string}      options.distanceFrom The distance to travel for translateY if supplied as type.
 * @param {string}      options.distanceTo   The distance to travel for translateY if supplied as type.
 * @param {string}      options.opacityFrom  The opacity value to animate from for fadeIn or fadeOut types.
 * @param {string}      options.opacityTo    The opacity value to animate to for fadeIn or fadeOut types.
 * @param {string}      options.types        The types of animations to run. Supports `fadeIn`, `fadeOut`, and `translateY`. You can mix them as space seperated values.
 *
 * @return {Array} The Keyframe objects.
 */
const getAnimationKeyframes = ( target, options ) => {
	const from = {};
	const to = {};
	const {
		distanceFrom = target.dataset?.translateDistanceFrom || '20px',
		distanceTo = target.dataset?.translateDistanceTo || '0px',
		opacityFrom = target.dataset?.translateOpacityFrom,
		opacityTo = target.dataset?.translateOpacityTo,
		types = target.dataset?.animationTypes || '',
	} = options;

	types.split( ' ' ).forEach( ( type ) => {
		if ( type === 'fadeIn' ) {
			from.opacity = opacityFrom || 0;
			to.opacity = opacityTo || 1;
		}
		if ( type === 'fadeOut' ) {
			from.opacity = opacityFrom || 1;
			to.opacity = opacityTo || 0;
		}
		if ( type === 'translateY' ) {
			from.transform = `translateY(${ distanceFrom })`;
			to.transform = `translateY(${ distanceTo })`;
		}
	} );

	return [ from, to ];
};

/**
 * @function end
 * @description End the animation.
 *
 * @since 1.5.0
 *
 * @param {HTMLElement} target             The target element to animate.
 * @param {object}      options            The options for the animation that are needed to determine what type of properties are applied to the element at the end of the animation.
 * @param {string}      options.distanceTo The distance to end the translateY animation at if supplied as type.
 * @param {string}      options.opacityTo  The opacity value to animate to for fadeIn or fadeOut types.
 * @param {string}      options.types      The types of animations to handle. Supports `fadeIn`, `fadeOut`, and `translateY`.
 */
const end = ( target, options ) => {
	const {
		distanceTo = target.dataset?.translateDistanceTo || '0px',
		opacityTo = target.dataset?.translateOpacityTo,
		types = target.dataset?.animationTypes || '',
	} = options;

	types.split( ' ' ).forEach( ( type ) => {
		if ( type === 'fadeIn' ) {
			target.style.opacity = opacityTo || '1';
			target.setAttribute( 'aria-hidden', 'false' );
		}
		if ( type === 'fadeOut' ) {
			target.style.opacity = opacityTo || '0';
			target.setAttribute( 'aria-hidden', 'true' );
		}
		if ( type === 'translateY' ) {
			target.style.transform = `translateY(${ distanceTo })`;
		}
	} );
};

/**
 * @function run
 * @description Animate a single element.
 *
 * @since 1.5.0
 *
 * @param {HTMLElement}   target                 The target element to animate.
 * @param {object}        options                The options for the animation that are needed to determine what type of properties are applied to the element at the end of the animation.
 * @param {Function}      options.onAnimateInit  Function to run when initializing animation.
 * @param {Function}      options.onAnimateStart Function to run at the beginning of the animation.
 * @param {Function}      options.onAnimateEnd   Function to run at the end of the animation.
 * @param {number}        options.delay          Delay for the animation in milliseconds.
 * @param {string|number} options.distance       The distance to travel in pixels for translateY if supplied as type.
 * @param {number}        options.duration       Duration for the animation in milliseconds.
 * @param {string}        options.easing         Easing for the animation. Supports `ease`, `ease-in`, `ease-out`, `ease-in-out`, `linear`, `step-start`, `step-end`, `steps()`, `cubic-bezier()`.
 * @param {string}        options.types          The types of animations to run. Supports `fadeIn`, `fadeOut`, and `translateY`. You can mix them as space seperated values.
 *
 * @return {void}
 * @example
 *   import { animate } from '@gravityforms/utils';
 *
 *   const animateExample = () => {
 *     const logo = getNode( '.gform-splash__header .gform-logo', document, true );
 *
 *     animate.run( logo, {
 *       types: 'fadeIn translateY',
 *       distanceFrom: '-50px',
 *       distanceTo: '0px',
 *       duration: 1000,
 *       easing: 'ease-in-out',
 *       delay: 500,
 *       onInit: () => {
 *         console.log( 'Animation initializing!' );
 *       },
 *       onAnimateStart: () => {
 *         console.log( 'Animation starting!' );
 *       },
 *       onAnimateEnd: () => {
 *         console.log( 'Animation complete!' );
 *       },
 *     } );
 *   };
 *
 */
const run = ( target = null, options = {} ) => {
	if ( ! target ) {
		return;
	}
	const {
		onAnimateInit = () => {},
		onAnimateStart = () => {},
		onAnimateEnd = () => {},
		delay = target.dataset?.animationDelay || 0,
		duration = target.dataset?.animationDuration || 400,
		easing = target.dataset?.animationEasing || 'linear',
	} = options;
	const keyframes = getAnimationKeyframes( target, options );

	onAnimateInit();

	setTimeout( () => {
		onAnimateStart();
		requestAnimationFrame( () => {
			const animated = target.animate( keyframes, {
				duration: Number( duration ),
				easing,
			} );

			animated.onfinish = () => {
				end( target, options );
				onAnimateEnd();
			};
		} );
	}, delay );
};

/**
 * @function runGroup
 * @description Animate a group of elements.
 *
 * @since 1.5.0
 *
 * @param {Array} entries The elements to animate. An array containing a target entry and an options entry that follows the structure of the run function options object.
 *
 * @return {void}
 * @example
 *   import { animate } from '@gravityforms/utils';
 *
 *   const animateExample = () => {
 *     const logo = getNode( '.gform-splash__header .gform-logo', document, true );
 *     const heading = getNode( '.gform-splash__header h1', document, true );
 *     const text = getNode( '.gform-splash__header p', document, true );
 *     const button = getNode( '.gform-splash__header .gform-button', document, true );
 *     const reviews = getNode( '.gform-splash__header .gform-reviews', document, true );
 *
 *     const defaults = {
 *       onAnimateEnd: () => {
 *         console.log( `hello from: ${ performance.now() }` );
 *       },
 *       distance: -20,
 *       duration: 600,
 *       easing: 'cubic-bezier(0.455, 0.030, 0.515, 0.955)',
 *       types: 'fadeIn translateY',
 *     };
 *     animate.runGroup( [
 *       { target: logo, options: { ...defaults } },
 *       { target: heading, options: { callback: defaults.callback } }, // this one uses data attributes or internal defaults instead since no options where passed here.
 *       { target: text, options: { ...defaults, delay: 200 } },
 *       { target: button, options: { ...defaults, delay: 300 } },
 *       { target: reviews, options: { ...defaults, delay: 400 } },
 *     ] );
 *   };
 *
 */
const runGroup = ( entries = [] ) => {
	entries.forEach( ( { target, options } ) => {
		run( target, options );
	} );
};

export { run, runGroup };