main/Popups.js

import '../polyfills/Fetch';
import LoadContent from './LoadContent';
import LoadScript from './LoadScript';
import MicroModal from '../vendor/micromodal';
import Cookie from '../vendor/jscookie';

/**
 * Uses MicroModal.js library to handle popup functionality.
 * Fetches dynamic content and script on open, and resets on close.
 *
 * @module Popups
 * @prop {object} selectors - Set of selectors that can be used to grab popup features.
 * @prop {object} states - Set of flags that can be used to toggle various popup states.
 */
const Popups = {
	selectors: {
		dialog: '.l-popup__inner',
		content: '.l-popup__content',
		overlay: '.l-popup__overlay',
		skeleton: '.l-popup__skeletonTemplate',
		dummyTrigger: '.l-popup__dummyTrigger',
		onPageLoadPopup: '[data-popup-onpageload]',
		lockedPopup: '.l-popup--locked',
	},

	states: {
		init: 'l-popup--init',
		locked: 'l-popup--locked',
		noScroll: 'is-no-scroll',
		fading: {
			in: 'is-slid-and-faded-in',
			out: 'is-slid-and-faded-out',
		},
	},

	data: {
		disablePopupCookie: 'data-popup-disable-oncookie',
	},

	/**
	 * Initializes all MicroModal popups on page.
	 * Binds open and close handlers.
	 *
	 * @method init
	 */
	init() {
		// Listen to keydown events when there's an on load popup
		// This must be done before setting up MicroModal event listeners
		// in order to stop keydown event from propagating to MicroModal
		if ( document.querySelectorAll( this.selectors.lockedPopup ).length > 0 ) {
			this.lockPage();
		}

		// bind show / close events for MicroModals
		MicroModal.init({
			onShow: modal => Popups.show( modal ),
			onClose: modal => Popups.hide( modal ),
		});

		// Select popup that shows up on page load
		const $onPageLoadPopup = document.querySelectorAll( this.selectors.onPageLoadPopup );

		// Trigger popup automatically on page load.
		// This only applies to popup attributed [data-popup-onpageload]
		[].forEach.call( $onPageLoadPopup, ( $popup ) => {
			const id = $popup.getAttribute( 'id' );
			if ( ! this.isDisabled( $popup ) ) {
				document.querySelector( `${this.selectors.dummyTrigger}[data-micromodal-trigger=${id}]` ).click();
			} else {
				this.unlockPage();
			}
		});
	},

	/**
	 * Prevent popup from closing when user hits ESC key
	 *
	 * @method lockPage
	 */
	lockPage() {
		document.addEventListener( 'keydown', this.preventEscape );
	},

	/**
	 * Allow popup to be closed by user hiting ESC key
	 *
	 * @method unlockPage
	 */
	unlockPage() {
		document.removeEventListener( 'keydown', this.preventEscape );
	},

	/**
	 * Prevent event propagation to MicroModal when ESC key is hit
	 *
	 * @method preventEscape
	 * @param {event} evt - key down event
	 */
	preventEscape( evt ) {
		if ( 27 === evt.keyCode ) {
			evt.stopImmediatePropagation();
		}
	},

	/**
	 * Animate in popup when it's triggered.
	 * Show skeleton UI while dynamically fetching content if necessary.
	 * Dynamically fetch associated script if necessary.
	 *
	 * @method show
	 * @param {element} modal - MicroModal popup element.
	 */
	show( modal ) {
		const $dialog = modal.querySelector( Popups.selectors.dialog );

		// fade in popup
		$dialog.classList.remove( Popups.states.fading.out );
		$dialog.classList.add( Popups.states.fading.in );

		// stop body from scrolling
		document.querySelector( 'body' ).classList.add( this.states.noScroll );

		if ( 'true' !== $dialog.dataset.isInit ) {
			const $content = $dialog.querySelector( Popups.selectors.content );
			const $skeleton = modal.querySelector( Popups.selectors.skeleton );

			// show skeleton ui if available
			if ( $skeleton ) {
				$content.innerHTML = $skeleton.innerHTML;
			}

			// load popup content if specified
			if ( 'loadContent' in $content.dataset ) {
				const promise = LoadContent.load( $content );
				promise.then( () => {
					// load popup script if specified
					if ( 'loadScript' in $content.dataset && 'true' !== $content.dataset.scriptLoaded ) {
						LoadScript.load( $content );
					}

					Popups.focusStates( $content );
					Popups.setFocus( $content );
					Popups.preventShift( modal );
				});
			}

			$dialog.dataset.isInit = true;
			modal.classList.add( Popups.states.init );
		}
	},

	/**
	 * Animate out popup when close button or overlay is pressed.
	 * Clear popup content if it's dynamic.
	 *
	 * @method hide
	 * @param {element} modal - MicroModal popup element.
	 */
	hide( modal ) {
		const $dialog = modal.querySelector( Popups.selectors.dialog );
		const $content = $dialog.querySelector( Popups.selectors.content );

		// fade out popup
		$dialog.classList.remove( Popups.states.fading.in );
		$dialog.classList.add( Popups.states.fading.out );

		// stop body from scrolling
		document.querySelector( 'body' ).classList.remove( this.states.noScroll );

		// Unlock page when modal locked hte page
		if ( modal.classList.contains( this.states.locked ) ) {
			this.unlockPage();
		}

		setTimeout( () => {
			// remove any dynamically loaded content
			if ( 'loadContent' in $content.dataset ) {
				$content.innerHTML = '';
			}

			$dialog.dataset.isInit = false;
		}, 500 );
	},

	/**
	 * Scroll input elements into view on focus.
	 *
	 * @method focusStates
	 * @param {element} $content - Popup content wrapper element.
	 */
	focusStates( $content ) {
		const $focusableEls = $content.querySelectorAll( 'input, textarea' );
		const config = {
			behavior: 'smooth',
			block: 'start',
			inline: 'nearest',
		};

		[].forEach.call( $focusableEls, ( $el ) => {
			$el.addEventListener( 'focus', ( e ) => {
				if ( 'scrollIntoView' in e.target ) {
					e.target.scrollIntoView( config );
				}
			});
		});
	},

	/**
	 * Focuses in the first input/textarea of the dialog.
	 *
	 * @method setFocus
	 * @param {element} $content - Popup content wrapper element.
	 */
	setFocus( $content ) {
		const $focusableEls = $content.querySelectorAll( 'input, textarea' );
		if ( 0 < $focusableEls.length ) {
			$focusableEls[0].focus();
		}
	},

	/**
	 * Prevent page layout from shifting when the keyboard is brought forward in Android
	 * by input focus.
	 *
	 * @method preventShift
	 * @param {element} modal - MicroModal popup element.
	 */
	preventShift( modal ) {
		const $overlay = modal.querySelector( Popups.selectors.overlay );
		$overlay.setAttribute( 'style', 'overflow: hidden;' );
		setTimeout( () => {
			$overlay.setAttribute( 'style', '' );
		}, 1000 );
	},

	/**
	 * Check if a popup modal has been disabled by a cookie
	 *
	 * @method isDisabled
	 * @param {element} modal - MicroModal popup element.
	 */
	isDisabled( modal ) {
		const cookieName = modal.getAttribute( this.data.disablePopupCookie );
		if ( cookieName && Cookie.get( cookieName ) ) {
			return true;
		}
		return false;
	},

};

export default Popups;