main/classes/VideoCarousel.js

import EmbedPlayer from './EmbedPlayer';
import StickyVideo from './StickyVideo';
import Carousel from './Carousel';

/**
 * Video carousel functionality.
 * Initializes `GlideJS` carousel based on settings provided in constructor.
 * Set's up all listeners and event handlers.
 *
 * @class VideoCarousel
 * @prop {object} states - Set of classes that serve as flags for carousel states.
 * @prop {object} selectors - Set of selectors that can be used to select carousel features.
 * @prop {string} isStandalone - Setting identifies whether or not this is a standalone carousel.
 * @prop {string} currentVideoId - ID of the current active video iframe.
 * @prop {element} $elem - The video carousel element itself.
 * @prop {element} $player - The current active video player object.
 * @prop {element} $label - Featured video label element if one exists (standalone carousels only)
 * @prop {element} $title - Featured video title element if one exists (standalone carousels only)
 * @prop {element} $description - Featured video description element if one exists
 * (standalone carousels only)
 * @prop {element} $link - Featured video link element if one exists (standalone carousels only)
 */

class VideoCarousel {
	static states = {
		playing: 'c-posts__item--playing',
	};

	static selectors = {
		carousel: '.l-videoCarousel',
		video: '.l-videoCarousel__featured .c-video',
		embed: '.l-videoCarousel__featured .c-video__embed',
		thumbnail: '.l-videoCarousel__link:not(.l-videoCarousel__link--page)',
		slide: '.c-posts__item',
		activeSlide: '.glide__slide--active',
		label: '.l-videoCarousel__info',
		title: '.l-videoCarousel__fullTitle',
		description: '.l-videoCarousel__description',
		link: '.l-videoCarousel__details',
		edit: '.c-editButton__link',
	};

	static instances = {};

	isStandalone = false;

	currentVideoId = null;

	carousel = null;

	$elem = null;

	$player = null;

	$label = null;

	$title = null;

	$description = null;

	$link = null;

	/**
	 * @constructs
	 * @param {object} $elem - The video carousel element object.
	 * @param {object} settings - Carousel settings to be merged with defaults
	 */
	constructor( $elem, settings ) {
		this.$elem = $elem;
		this.isStandalone = settings.isStandalone;

		// create carousel object
		const carouselOptions = Object.assign({}, settings.carousel, { layoutClass: 'l-videoCarousel' });
		this.carousel = new Carousel( $elem, carouselOptions );
	}

	/**
	 * Initialize video carousel, set up class members, and bind event listeners.
	 *
	 * @method init
	 */
	init() {
		// initialize carousel
		this.carousel.init();

		this.$player = this.$elem.querySelector( VideoCarousel.selectors.video );
		this.$slides = this.$elem.querySelectorAll( VideoCarousel.selectors.slide );

		// Bail if there are no videos in video section
		if ( ! this.$player ) {
			return;
		}

		this.currentVideoId = this.$player.dataset.displayinlinePlayerId;
		this.$elem.dataset.videoCarouselInit = 'true';

		// initialize red label, title, description, and link elements if it's a standalone carousel
		if ( this.isStandalone ) {
			this.$label = this.$elem.querySelector( VideoCarousel.selectors.label );
			this.$title = this.$elem.querySelector( VideoCarousel.selectors.title );
			this.$description = this.$elem.querySelector( VideoCarousel.selectors.description );
			this.$link = this.$elem.querySelector( VideoCarousel.selectors.link );

			// add setting to restrict sticky videos to mobile.
			// standalone player need not be sticky on desktop.
			if ( this.enableAtWidth ) {
				this.$player.dataset.displayinlineStickyOnMobile = this.enableAtWidth;
			}
		}

		// Only bind videos if slides trigger inline playback.
		if ( this.$elem.dataset.videoCarouselPlayInline ) {
			this.bindVideos();
		}

		const id = this.$elem.getAttribute( 'id' );
		if ( id ) {
			VideoCarousel.instances[id] = this;
		}

		window.addEventListener( 'message', VideoCarousel.receiveMessage );
	}

	/**
	 * Bind video slide click listeners. Clicking on a video slide should load video into player.
	 *
	 * @method bindVideos
	 */
	bindVideos() {
		const $videos = this.$elem.querySelectorAll( VideoCarousel.selectors.thumbnail );

		[].forEach.call( $videos, ( $video ) => {
			$video.addEventListener( 'click', event => this.handleVideoClick( event, $video ) );
		});
	}

	/**
	 * Video slide click handler. Loads selected video into player if it is not there already.
	 * Updates now-playing flag on carousel to highlight selected video.
	 *
	 * @method handleVideoClick
	 * @param {object} event - Video slide click event instance.
	 * @param {object} $video - Video slide element object.
	 */
	handleVideoClick( event, $video ) {
		event.preventDefault();

		if ( ! $video.classList.contains( VideoCarousel.states.playing ) ) {
			const { videoPlayerId } = $video.dataset;
			const { videoCarouselUrl } = $video.dataset;
			const { videoCarouselPlaylist } = $video.dataset;

			this.swapVideo( videoPlayerId, videoCarouselUrl, videoCarouselPlaylist, $video );
			VideoCarousel.updateNowPlaying( $video.dataset.videoId, this.$elem );
			this.currentVideoId = videoPlayerId;
		}
	}

	/**
	 * Swap out current carousel video with a new video and restart player.
	 *
	 * @method swapVideo
	 * @param {object} playerId - ID of current active video iframe.
	 * @param {url} embedUrl - New video player (and playlist) url.
	 * @param {string} playlist - Comma separated list of successive video ids in playlist.
	 * @param {object} $video - Video slide element object.
	 */
	swapVideo( playerId, embedUrl, playlist, $video ) {
		const $stickyVideoElem = document.querySelector( `#${this.currentVideoId}__sticky` );
		if ( $stickyVideoElem ) {
			StickyVideo.detachCurrentVideo();
			$stickyVideoElem.parentNode.removeChild( $stickyVideoElem );
		} else {
			const $videoElem = this.$player.querySelector( VideoCarousel.selectors.embed );
			if ( $videoElem ) {
				$videoElem.parentNode.removeChild( $videoElem );
			}
		}

		this.$player.dataset.displayinlineInit = '';
		this.$player.dataset.displayinline = embedUrl;
		this.$player.dataset.displayinlinePlayerId = playerId;
		this.$player.dataset.displayinlinePlaylist = playlist;
		this.$player.dataset.displayinlineVideoId = $video.dataset.videoId;

		// update red label, title, description, and link if this is a standalone carousel
		if ( this.isStandalone ) {
			VideoCarousel.updateDetails( this, $video );
		}

		// Swap edit video link it present (e.g. for logged in users)
		const $editButton = this.$player.querySelector( VideoCarousel.selectors.edit );
		if ( $editButton ) {
			const oldLink = $editButton.getAttribute( 'href' );
			const newLink = oldLink.replace( /post=[\d]+/, `post=${$video.dataset.videoId}` );
			$editButton.setAttribute( 'href', newLink );
		}

		const $embedPlayer = new EmbedPlayer( this.$player );
		$embedPlayer.start();
	}

	/**
	 * Update video details
	 *
	 * @static
	 * @method updateDetails
	 * @param {object} carousel - VideoCarousel
	 * @param {object} $video - $video element
	 */
	static updateDetails( carousel, $video ) {
		/* eslint-disable no-param-reassign */
		carousel.$label.innerText = $video.dataset.videoCarouselLabel;
		carousel.$title.innerText = $video.getAttribute( 'title' );
		carousel.$description.innerText = $video.dataset.videoCarouselContent;
		carousel.$link.setAttribute( 'href', $video.getAttribute( 'href' ) );
		/* eslint-enable no-param-reassign */
	}

	/**
	 * Checks to see if play event has fired for current active carousel video.
	 * If so, grabs current playlist index and passes along to update now playing flag.
	 * This method has to be static as we can't bind `postMessage` events to object instances.
	 *
	 * @static
	 * @method receiveMessage
	 * @param {object} data - postMessage data object.
	 */
	static receiveMessage( data ) {
		if ( data.status && data.iframeId && 'undefined' !== typeof data.playlistIndex ) {
			if ( 'playlistItem' === data.status ) {
				// videoPostId will not be defined for live stream
				// live stream would only be selectable from the carousel, not from video playlist

				// Bail if the video is played within the expanded iframe, rather than generic main player.
				if ( data.iframeId.indexOf( data.videoPostId ) > 0 ) {
					return;
				}

				const $video = document.querySelector( `[data-video-id="${data.videoPostId}"]` );
				if ( $video ) {
					const carouselId = VideoCarousel.getId( $video.dataset.videoPlayerId );
					const carousel = VideoCarousel.instances[carouselId];
					if ( carousel.isStandalone ) {
						VideoCarousel.updateDetails( carousel, $video );
					}

					VideoCarousel.updateNowPlaying( $video.dataset.videoId, carousel.$elem );
				}
			}
		}
	}

	/**
	 * Updates the now playing flag based on the current active `videoId`.
	 *
	 * @static
	 * @method updateNowPlayling
	 * @param {string} iframeId - ID of current active video iframe.
	 * @param {object} $carousel - Video carousel element object.
	 */
	static updateNowPlaying( videoId, $carousel ) {
		const $oldPlaying = $carousel.querySelectorAll( `.${VideoCarousel.states.playing}` );

		[].forEach.call( $oldPlaying, ( $slide ) => {
			$slide.classList.remove( VideoCarousel.states.playing );
		});

		const $playing = $carousel.querySelector( `[data-video-id="${videoId}"]` );
		$playing.parentNode.classList.add( VideoCarousel.states.playing );

		Carousel.goToSlide( $playing, $carousel );
	}

	/**
	 * Get video carousel id
	 *
	 * @static
	 * @method getId
	 * @param {sting} playerId
	 * @return {string} The corresponding ID for that video carousel.
	 */
	static getId( playerId ) {
		return `${playerId}-carousel`;
	}
}

export default VideoCarousel;

// @TODO: liveblog video carousel