main/classes/EmbedPlayer.js

/* global gnca_settings */

import StickyVideo from './StickyVideo';
import InView from '../../utils/classes/InView';
import saveData from '../../utils/saveData';
import customEvent from '../../utils/customEvent';
import isVisible from '../../utils/dom/isVisible';
import Messenger from '../Messenger';

/**
 * Video iframe embed functionality.
 * This handles anything related to video embedding:
 * Click to play, muted autoplay, sticky video.
 * There is a separate module for the video player itself within the iframe.
 *
 * @class EmbedPlayer
 * @prop {object} states - Set of classes that serve as flags for player states.
 * @prop {object} classes - Set of css classes that can be used to select player features.
 * @prop {element} $player - DOM reference to player node
 * @prop {object} startListeners - Static array of listener for player click events
 */
class EmbedPlayer {
	states = {
		faded: 'is-faded-out',
		animated: 'is-animated',
		expanded: 'is-full-width',
	};

	classes = {
		placeholder: 'c-video__placeholder',
		embed: 'c-video__embed',
		parentExpanded: 'c-posts__inner--expanded',
		main: 'c-video',
		post: 'c-post',
	};

	$player = null;

	$container = null;

	containerActiveCss = '';

	static messageType = 'gnca_video';

	static startListeners = [];

	static instances = [];

	/**
	 * @constructs
	 * @param {Element} $player - DOM reference to a video player embed
	 */
	constructor( $player ) {
		this.$player = $player;
	}

	/**
	 * Add click handler to video player embed or initiate muted autoplay if indicated.
	 *
	 * @member init
	 */
	// hook up each instance of this widget
	init() {
		if ( ! this.$player.dataset.displayinlineInit ) {
			// Determine if the embed player has an image placeholder.
			const $placeholder = this.$player.querySelector( `.${this.classes.placeholder}` );
			const hasPlaceholder = $placeholder && $placeholder.childNodes.length > 0;

			if ( hasPlaceholder && isVisible( this.$player )
				&& this.isAutoplayVideo()
				&& EmbedPlayer.autoplayEnabled()
				&& false === saveData() ) {
				if ( 'true' === this.$player.dataset.autoplay ) {
					// autoplay right away
					this.start( true );
				} else {
					// autoplay when in view
					this.startInView();
				}
			} else if ( hasPlaceholder ) {
				const length = EmbedPlayer.startListeners.push( this.start.bind( this, false ) );
				this.$player.addEventListener( 'click', EmbedPlayer.startListeners[length - 1]);

				// We need to store this as a data attribute because there are some cases where
				// another embed player instance is added to the same $player element, in which
				// case we need to remove the old click handler to avoid weird issues like that
				// found in GT-2152
				this.$player.dataset.displayinlineListenerID = length - 1;
			} else {
				// No placeholder available for this embed player,
				// immediately populate the video area with the video iframe.
				this.start( this.isAutoplayVideo() );
			}

			// register embed player by player id.
			EmbedPlayer.instances[this.$player.dataset.displayinlinePlayerId] = this;
		}
	}

	/**
	 * Fade in video player and remove click handler.
	 *
	 * @member start
	 * @param {Boolean} mutedAutoplay - Determines if video should start automatically without sound
	 */
	start( mutedAutoplay = false, event = null ) {
		if ( event ) {
			const cssString = event.target && event.target.classList.length > 0 ? event.target.classList[0] : '';

			// If user clicked Edit Video button (logged-in only)
			// we want to trigger the default handler. Otherwise no.
			if ( cssString.indexOf( 'c-editButton' ) < 0 ) {
				event.preventDefault();
			}

			// If user clicked on an element that isn't part of the embed player, ignore
			// e.g. sticky close button
			if ( cssString.indexOf( this.classes.main ) < 0
				&& cssString.indexOf( this.classes.post ) < 0 ) {
				return;
			}
		}

		if ( null !== this.$player.dataset.displayinlineListenerID ) {
			this.$player.removeEventListener( 'click', EmbedPlayer.startListeners[this.$player.dataset.displayinlineListenerID]);
		}
		this.$player.dataset.displayinlineInit = 'true'; // eslint-disable-line no-param-reassign

		// Append new video element to the DOM and return it
		const $video = this.fadeInVideo( mutedAutoplay );

		// If the video sits within a media container,
		// reset video on video detach so the embed player can be re-initialized on click
		if ( this.$player.dataset.displayinlineContainer ) {
			this.$container = document.querySelector( `[data-container-id="${this.$player.dataset.displayinlineContainer}"]` );
			if ( this.$container ) {
				this.containerActiveCss = this.$container.classList.length > 0 ? this.$container.classList[0] : '';
				this.containerActiveCss = `${this.containerActiveCss}--active`;
				this.$container.classList.add( this.containerActiveCss );
			}

			const detachHandler = this.reset.bind( this, $video );
			$video.addEventListener( customEvent.STICKY_DETACHED, detachHandler );
		}

		if ( 'true' === this.$player.dataset.displayinlineExpand ) {
			this.expandVideoPlayer();
		}
	}

	/**
	 * Add in view listener to trigger playback when player becomes in view.
	 *
	 * @member startInView
	 */
	startInView() {
		// set up in view watcher for video player
		const watcherName = 'embedPlayerWatcher';
		let watcher = InView.getWatcher( watcherName );
		if ( ! watcher ) {
			watcher = new InView({
				threshold: 0.5,
			}, watcherName );
			watcher.init();
		}

		this.$player.addEventListener( customEvent.IN_VIEW, ( event ) => {
			if ( event.detail
				&& watcherName === event.detail.caller.name ) {
				if ( ! this.$player.dataset.displayinlineInit ) {
					if ( event.detail.isInView ) {
						this.start( true );
					}
				} else {
					const $iframe = this.$player.querySelector( 'iframe' );
					const iframeId = $iframe.getAttribute( 'id' );
					if ( event.detail.isInView ) {
						Messenger.sendMessage( iframeId, EmbedPlayer.messageType, 'resume' );
					} else {
						Messenger.sendMessage( iframeId, EmbedPlayer.messageType, 'pause' );
					}
				}
			}
		});

		watcher.startWatching( this.$player );
	}

	/**
	 * Reset embed player to initial state and remove existing $video element
	 *
	 * @member reset
	 * @param {HTMLElement} $video - $video element to be removed
	 */
	reset( $video ) {
		if ( null !== this.$player.dataset.displayinlineListenerID ) {
			this.$player.addEventListener( 'click', EmbedPlayer.startListeners[this.$player.dataset.displayinlineListenerID]);
		}

		this.$player.dataset.displayinlineInit = null; // eslint-disable-line no-param-reassign
		this.$player.removeChild( $video );

		const $placeholder = this.$player.querySelector( `.${this.classes.placeholder}` );
		$placeholder.classList.remove( this.states.faded );
		$placeholder.classList.remove( this.states.animated );

		if ( this.$container ) {
			this.$container.classList.remove( this.containerActiveCss );
		}
	}

	/**
	 * Fade out placeholder and fade in video player.
	 *
	 * @member fadeInVideo
	 * @param {Boolean} mutedAutoplay - Determines if video should start automatically without sound
	 */
	fadeInVideo( mutedAutoplay = false ) {
		const $placeholder = this.$player.querySelector( `.${this.classes.placeholder}` );
		$placeholder.classList.add( this.states.faded );
		$placeholder.classList.add( this.states.animated );

		const $video = this.getVideo( mutedAutoplay );
		this.$player.appendChild( $video );

		return $video;
	}

	/**
	 * Expand player to full width.
	 * Used when triggering a video within a story stream.
	 *
	 * @member expandVideoPlayer
	 */
	expandVideoPlayer() {
		this.$player.classList.add( this.states.expanded );
		this.$player.parentNode.classList.add( this.classes.parentExpanded );
	}

	/**
	 * Create video player iframe tag from settings.
	 *
	 * @member getVideo
	 * @param {Boolean} mutedAutoplay - Determines if video should start automatically without sound
	 * @return {Element} $video - Video player iframe object
	 */
	getVideo( mutedAutoplay = false ) {
		let $src = this.$player.dataset.displayinline;

		if ( true === mutedAutoplay ) {
			$src = `${$src}&mute`;
		}

		if ( this.isAutoplayVideo() ) {
			$src = `${$src}&embedAutoPlay`;
		}

		let $video = document.createElement( 'iframe' );
		$video.setAttribute( 'id', `embedPlayer_${this.$player.dataset.displayinlineVideoId}` );
		$video.dataset.displayinlineContent = '';
		$video.classList.add( this.classes.embed );
		$video.setAttribute( 'title', 'Sticky Video' );
		$video.setAttribute( 'src', $src );
		$video.setAttribute( 'scrolling', 'no' );

		if ( this.stickyEnabled() ) {
			$video = this.setupStickyVideo( $video );
		}

		return $video;
	}

	/**
	 * Initializes sticky video functionality
	 *
	 * @member setupStickyVideo
	 * @param {Element} $video - Video player iframe object
	 * @return {Element} = Video player iframe object with sticky video markup added
	 */
	setupStickyVideo( $video ) {
		const iframeId = this.$player.dataset.displayinlinePlayerId;

		$video.setAttribute( 'id', iframeId );
		$video.setAttribute( 'allowfullscreen', true );

		const sticky = new StickyVideo( $video, this );
		return sticky.init();
	}

	/**
	 * Checks if this is a featured video
	 *
	 * @member isFeaturedVideo
	 * @param {Element} $player - DOM reference to a video player embed
	 * @return {Boolean} - True / False whether or not it's a featured video
	 */
	isFeaturedVideo() {
		return ( 'true' === this.$player.dataset.displayinlineFeatured );
	}

	/**
	 * Checks if this is an autoplayed video
	 *
	 * @member isAutoplayVideo
	 * @param {Element} $player - DOM reference to a video player embed
	 * @return {Boolean} - True / False whether or not it's a featured video
	 */
	isAutoplayVideo() {
		return ( 'true' === this.$player.dataset.autoplay
			|| 'inview' === this.$player.dataset.autoplay
			|| 'inviewonce' === this.$player.dataset.autoplay );
	}

	/**
	 * Checks to see if sticky video is enabled. First check if the sticky video setting is on.
	 * Then do a viewport width check if the video is set to be turned off on desktop.
	 *
	 * @member stickyEnabled()
	 * @return {Boolean} - True / False whether or not sticky video is enabled
	 */
	stickyEnabled() {
		if ( this.$player.dataset.displayinlinePlayerId && this.$player.dataset.displayinlineSticky ) {
			if ( this.$player.dataset.displayinlineStickyOnMobile ) {
				const mobileBreakpoint = parseInt( this.$player.dataset.displayinlineStickyOnMobile, 10 );
				return window.innerWidth < mobileBreakpoint;
			}

			return true;
		}

		return false;
	}

	/**
	 * Checks page settings to see if muted autoplay is enabled.
	 *
	 * @member autoplayEnabled
	 * @return {Boolean} - State of muted autoplay setting
	 */
	static autoplayEnabled() {
		/* eslint-disable camelcase */
		if ( 'undefined' !== typeof ( gnca_settings )
			&& gnca_settings.video_settings
			&& gnca_settings.video_settings.muted_autoplay ) {
			return Boolean( gnca_settings.video_settings.muted_autoplay );
		}
		/* eslint-enable camelcase */

		return false;
	}

	/**
	 * Return player instance by id
	 *
	 * @member getPlayer
	 * @return {string} id - id of the player
	 */
	static getPlayer( id ) {
		return EmbedPlayer.instances[id];
	}
}

export default EmbedPlayer;