main/classes/SimpleVideo.js

import customEvent from '../../utils/customEvent';
import InView from '../../utils/classes/InView';

/**
 * SimpleVideo Module for playing video without ads, analytics, playback control
 *
 * @class SimpleVideo
 * @prop {object} settings - configurations for SimpleVideo
 * @prop {boolean} started - true if playback has started, false otherwise
 * @prop {boolean} isInView - true if player is in view
 * @prop {object} playler - video player object created via third party library (video.js)
 * @prop {element} $poster - DOM reference of poster image element
 * @prop {element} $container - DOM reference of container element where video is inserted
 * @prop {string} posterSelector - css selector for poster image DOM element
 * @prop {string} identifer - css class identifier a SimpleVideo element
 * @prop {string} animateInCss - css class for animation of showing element
 * @prop {string} animateOutCss - css class for animation of hiding element
 * @prop {string} prefix - prefix of DOM element id of a SimpleVideo
 * @prop {object} libraries - library URLs required for enabling video and the respective load state
 * @prop {number} videoCount - number of SimpleVideo instances on the page
 * @prop {boolean} ready - whether or not video object is ready to be instantiated
 * @prop {number} numLibLoaded - number of scripts loaded
 * @prop {string} API_READY - name of event fired when libraries are loaded
 */
class SimpleVideo {
	settings = null;

	started = false;

	isInView = false;

	player = false;

	$poster = false;

	$container = false;

	identifier = 'c-simpleVideo';

	posterSelector = '.vjs-poster';

	animateInCss = 'animate-fadeIn';

	animateOutCss = 'animate-fadeOut';

	template = '';

	watcher = false;

	inViewListener = false;

	static videoCount = 0;

	static ready = false;

	static numLibsToLoad = false;

	static prefix = 'simpleVideo';

	static libraries = {
		'/assets/dist/vendor/videojs/video.min.js': false,
	};

	static API_READY = 'simpleVideoAPIReady';

	/* global videojs */

	/**
	 * Constructor
	 */
	constructor( settings ) {
		this.settings = settings;
		SimpleVideo.videoCount += 1;
	}

	/**
	 * Insert video template into the specified container element
	 * Load video scripts and initalize the video player
	 *
	 * @method init
	 */
	init() {
		this.$container = document.querySelector( this.settings.selector );
		if ( this.$container ) {
			// Generate video template based on provided settings.
			const width = this.settings.width || '100%';
			const height = this.settings.height || '100%';
			const autoPlay = this.settings.autoPlay || true;
			this.template = `
				<video-js id="${SimpleVideo.getVideoId()}" width="${width}" height="${height}" poster="${this.settings.poster}" autoplay="${autoPlay}" controls="false" playsinline>
					<source src="${this.settings.source}" type="${this.settings.type}">
				</video-js>
			`;

			// detect visibility of video container to play / pause video in place
			this.watcher = InView.getWatcher( 'simpleVideoWatcher' );
			if ( ! this.watcher ) {
				this.watcher = new InView({ threshold: 1 }, 'simpleVideoWatcher' );
				this.watcher.init();
			}
			this.inViewListener = this.handleVisibilityChange.bind( this );
			this.$container.dataset.alwaysObserve = 'true';
			this.$container.addEventListener( customEvent.IN_VIEW, this.inViewListener );
			this.watcher.startWatching( this.$container );

			// load video scripts if not yet done so, otherwise initialize the player right away
			if ( ! SimpleVideo.ready ) {
				const libs = Object.keys( SimpleVideo.libraries );
				SimpleVideo.numLibsToLoad = libs.length;
				libs.forEach( ( url ) => {
					if ( ! SimpleVideo.libraries[url]) {
						SimpleVideo.embedScript( url );
						SimpleVideo.libraries[url] = true;
					}
				});

				window.addEventListener( SimpleVideo.API_READY, () => {
					if ( this.isInView ) {
						this.initPlayer();
					}
				});
			}
		}
	}

	/**
	 * Initialize video player and set up event listener for animating in
	 * the player when ready.
	 *
	 * @method initPlayer
	 */
	initPlayer() {
		// Bail if player already initialized
		if ( this.player ) {
			return;
		}

		// Insert video template into $container node
		this.$container.innerHTML = this.template;

		// Instantiate video player
		this.player = videojs( SimpleVideo.getVideoId() );
		this.player.muted( true );
		this.player.addClass( this.identifier );
		this.$poster = this.$container.querySelector( this.posterSelector );
		this.player.one( 'playing', () => {
			this.$poster.classList.add( this.animateOutCss );
			this.started = true;
		});

		this.play();
	}

	/**
	 * Begin video playback
	 *
	 * @method play
	 * @param {string} url - optional. Video stream URL.
	 */
	play( url ) {
		if ( url ) {
			this.player.src( url );
		}

		this.show();
		this.player.play();
	}

	/**
	 * Stop video playback
	 *
	 * @method stop
	 */
	stop() {
		if ( ! this.player ) {
			return;
		}

		this.player.dispose();
		this.player = false;
		this.$poster = false;
	}

	/**
	 * Remove video
	 *
	 * @method remove
	 */
	remove() {
		this.$container.removeEventListener( customEvent.IN_VIEW, this.inViewListener );
		this.watcher.stopWatching( this.$container );
		this.stop();
		this.started = false;
	}

	/**
	 * Animate in the video player
	 *
	 * @method show
	 */
	show() {
		this.player.addClass( this.animateInCss );
	}

	/**
	 * Animate out the video player
	 *
	 * @method hide
	 */
	hide() {
		this.player.removeClass( this.animateInCss );
		if ( this.$poster ) {
			this.$poster.classList.remove( this.animateOutCss );
		}
	}

	/**
	 * Handle InView events
	 * stop playback when container element is invisible
	 * start playback when container element is visible
	 *
	 * @method handleVisibilityChange
	 * @param {object} event
	 */
	handleVisibilityChange( event ) {
		this.isInView = event.detail.isInView;

		// bail if JS not ready
		if ( ! SimpleVideo.ready ) {
			return;
		}

		if ( event.detail.isInView && ! this.player ) {
			this.initPlayer();
		} else if ( ! event.detail.isInView && this.player ) {
			this.stop();
		}
	}

	/**
	 * Embed JS script and listen for when the script is loaded
	 *
	 * @method embedScript
	 * @param {string} url - URL of js script
	 */
	static embedScript( url ) {
		/* global gnca_settings */
		let scriptUrl = url;

		/* eslint-disable camelcase */
		if ( 0 !== scriptUrl.indexOf( 'http' ) && gnca_settings ) {
			scriptUrl = `${gnca_settings.js_base_path}${url}`;
		}
		/* eslint-enable */

		const script = document.createElement( 'script' );
		script.async = false;
		script.type = 'text/javascript';
		script.src = scriptUrl;
		script.onload = () => {
			SimpleVideo.onScriptLoaded();
		};

		const node = document.getElementsByTagName( 'script' )[0];
		node.parentNode.insertBefore( script, node );
	}

	/**
	 * Handler for when a script is loaded
	 * Trigger API_READY event when all libraries have been loaded
	 *
	 * @method onScriptLoaded
	 */
	static onScriptLoaded() {
		SimpleVideo.numLibsToLoad -= 1;
		if ( 0 === SimpleVideo.numLibsToLoad ) {
			SimpleVideo.ready = true;
			customEvent.fire( window, SimpleVideo.API_READY );
		}
	}

	/**
	 * Generate a unique id to identify a SimpleVideo element
	 *
	 * @method getVideoId
	 */
	static getVideoId() {
		return `${SimpleVideo.prefix}-${SimpleVideo.videoCount}`;
	}
}

export default SimpleVideo;