article/ImageViewer.js

import animateMove from '../utils/animation/animateMove';
import customEvent from '../utils/customEvent';

/**
 * The ImageViewer module allows article images to render in full screen mode.
 *
 * @module ImageViewer
 * @prop {Object} selectors - DOM element selectors
 * @prop {Object} states - different css states
 * @prop {String} template - template used for populating the image viewer
 * @prop {HTMLElement} $viewer - the current image viewer
 * @prop {HTMLElement} $currentImage - the current image selected to be shown in the image viewer
 * @prop {HTMLElement} $body - the body element
 * @prop {HTMLElement} $html - the root HTML Element
 * @prop {function} keyboardListener - keyboard key up listener
 * @prop {integer} maxWidth - maximum width for an image in the image viewer
 */

const ImageViewer = {
	selectors: {
		figure: '.c-figure--expandable:not([data-in-gallery])',
		expand: '.c-figure__expand',
		image: '.c-figure__image',
		viewer: '.c-imageViewer',
		fullImage: '.c-imageViewer__image',
	},

	states: {
		noScroll: 'is-no-scroll',
		noSmoothScroll: 'is-no-smooth-scroll',
		animateOut: 'c-imageViewer--animateOut',
		animateIn: 'c-imageViewer--animateIn',
		hidden: 'c-imageViewer--hidden',
		portrait: 'c-imageViewer__image--portrait',
		loading: 'c-imageViewer--loading',
	},

	template: `
	<div class="c-imageViewer__inner">
		<div class="c-imageViewer__loader c-loader c-loader--small"></div>
		<img src="" class="c-imageViewer__image" />
	</div>
	`,

	$body: false,

	$viewer: false,

	$currentImage: false,

	keyboardListener: false,

	maxWidth: 2048,

	/* global gn_analytics */

	/**
	 * Select expandable images and set up click listeners to trigger the image viewer.
	 *
	 * @method init
	 */
	init() {
		const $figures = document.querySelectorAll( this.selectors.figure );
		[].forEach.call( $figures, ( $figure ) => {
			const $expand = $figure.querySelector( this.selectors.expand );
			const $image = $figure.querySelector( this.selectors.image );
			const clickListener = this.initializeFullScreen.bind( this, $image );
			$expand.addEventListener( 'click', clickListener );
		});

		this.$body = document.querySelector( 'body' );
		this.$html = document.querySelector( 'html' );
	},

	/**
	 * Initialize the image viewer for viewing the selected image in full screen mode.
	 *
	 * @param {HTMLElement} $image - the selected image to be shown in full screen mode
	 * @param {Event} evt - click event
	 *
	 * @method initializeFullScreen
	 */
	initializeFullScreen( $image, evt ) {
		evt.preventDefault();

		// Bail if no image source is found.
		let imageSrc = $image.dataset.src || $image.src;
		if ( ! imageSrc ) {
			return;
		}

		// Set maximum width
		imageSrc = imageSrc.replace( /\?.+/, '' );
		imageSrc = `${imageSrc}?w=${this.maxWidth}`;

		// Set current image.
		this.$currentImage = $image;

		// Create image viewer UI, add click listener.
		this.$viewer = document.createElement( 'a' );
		this.$viewer.href = '';
		this.$viewer.title = 'Close image';
		this.$viewer.dataset.trackaction = 'image | close';
		this.$viewer.dataset.trackRegion = 'Content';
		this.$viewer.classList.add( this.selectors.viewer.replace( '.', '' ) );
		this.$viewer.classList.add( this.states.animateIn );
		this.$viewer.classList.add( this.states.loading );
		this.$viewer.innerHTML = this.template;
		this.$viewer.addEventListener( 'click', clickEvt => this.close( clickEvt ) );

		// Key listeners.
		this.keyboardListener = this.interactWithKeyboard.bind( this );
		document.addEventListener( 'keyup', this.keyboardListener );

		const $fullImage = this.$viewer.querySelector( this.selectors.fullImage );
		$fullImage.addEventListener( 'load', () => this.imageLoad() );
		$fullImage.src = imageSrc;
		$fullImage.alt = $image.alt;

		if ( parseInt( $image.width, 10 ) < parseInt( $image.height, 10 ) ) {
			$fullImage.classList.add( this.states.portrait );
		}

		// Add the image viewer to the page.
		this.$body.appendChild( this.$viewer );
		this.$body.classList.add( this.states.noScroll );
		this.$html.classList.add( this.states.noSmoothScroll );

		// Track
		this.track( evt.currentTarget );
	},

	/**
	 * Flag an image as being loaded
	 *
	 * @param {HTMLElement} $image - the image that has been loaded
	 *
	 * @method imageLoad
	 */
	imageLoad() {
		if ( this.$viewer ) {
			this.$viewer.classList.remove( this.states.loading );
		}
	},

	/**
	 * Close the image viewer with ESC key.
	 *
	 * @param {Event} evt - key up event
	 *
	 * @method interactWithKeyboard
	 */
	interactWithKeyboard( evt ) {
		switch ( evt.keyCode ) {
		case 27: // ESC
			this.$viewer.click();
			break;
		default:
			break;
		}
	},

	/**
	 * Close the image viewer with an animation.
	 *
	 * @param {Event} evt - mouse click event.
	 *
	 * @method close
	 */
	close( evt ) {
		evt.preventDefault();
		if ( this.$viewer ) {
			document.removeEventListener( 'keyup', this.keyboardListener );

			// Reset to original states.
			this.$viewer.classList.remove( this.states.animateIn );
			this.$body.classList.remove( this.states.noScroll );
			this.$html.classList.remove( this.states.noSmoothScroll );
			this.$viewer.classList.add( this.states.hidden );

			// Transition out the gallery, destory the gallery when the transition is completed.
			const $fullImage = this.$viewer.querySelector( this.selectors.fullImage );
			$fullImage.addEventListener( customEvent.TRANSITION_COMPLETED, () => {
				this.destory();
			});
			animateMove( $fullImage, this.$currentImage, 0.35 );

			// Analytics tracking.
			this.track( evt.currentTarget );
		}
	},

	/**
	 * Completely remove the image viewer and the reference to it.
	 *
	 * @method destroy
	 */
	destory() {
		if ( this.$viewer ) {
			this.$viewer.remove();
			this.$viewer = false;
		}
	},

	/**
	 * Analytics tracking.
	 *
	 * @param {HTMLElement} $elem - element triggering click tracking
	 *
	 * @method track
	 */
	track( $elem ) {
		/* eslint-disable camelcase */
		if ( 'undefined' !== typeof ( gn_analytics ) ) {
			gn_analytics.Analytics.track(['adobe'], {
				eventType: 'click',
				action: $elem.dataset.trackaction,
				target: $elem,
			});
		}
		/* eslint-enable camelcase */
	},
};

export default ImageViewer;