article/ReadProgress.js

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

/**
 * Displays 'Back to Top' button with circle progress bar
 *
 * @module ReadProgress
 * @prop {String} buttonDivSelector - Class reference for Back to Top button container
 * @prop {String} buttonSelector - Class reference for Back to Top button
 * @prop {String} circleSelector - Class reference for circle element
 * @prop {String} hiddenClass - Hiding class
 * @prop {String} mainSelector - Class reference for the main element
 * @prop {String} summarySelector - Class reference for the summary section
 *       (used on Results template)
 * @prop {String} interactiveSelector - Class reference for the interactive section
 *       (used on Results template)
 * @prop {String} articleSelector - Class reference for the article wrapper
 * @prop {String} mainResultsClass - Results template class
 * @prop {Object} circle - DOM reference to the circle element
 * @prop {Integer} topLocation - Measurable content start location from top of the document
 * @prop {Integer} bottomLocation - Measurable content end location from top of the document
 * @prop {Integer} measurableHeight - Measurable content height
 * @prop {Object} $buttonDiv - DOM reference to Back to Top button
 */
const ReadProgress = {
	buttonDivSelector: '.c-readProgress',
	buttonSelector: '.c-readProgress__button',
	circleSelector: '.c-readProgress__circle',
	stickyOffsetClass: 'c-readProgress--stickyOffset',
	hiddenClass: 'is-hidden',
	mainSelector: '.l-main',
	summarySelector: '.l-main__summary',
	interactiveSelector: '.l-main__interactive',
	articleSelector: '.l-main__article',
	mainResultsClass: 'l-main--results',
	circle: false,
	topLocation: 0,
	bottomLocation: 0,
	measurableHeight: 0,
	$buttonDiv: null,

	/**
	 * Initializes 'Back To Top' button on article pages
	 *
	 * @method init
	 */
	init() {
		this.$buttonDiv = document.querySelector( this.buttonDivSelector );

		// Make sure there is a button container on the page
		if ( ! this.$buttonDiv ) {
			return;
		}

		this.circle = document.querySelector( this.circleSelector );

		// Bind click event to the button
		const $button = document.querySelector( this.buttonSelector );
		if ( $button ) {
			// markup has is-hidden applied to avoid big gap rendering on page load.
			$button.classList.remove( this.hiddenClass );
			$button.addEventListener( 'click', () => {
				this.handleReadProgressClick();
			});
		}

		window.addEventListener( customEvent.SCROLLED_TO_TOP, () => this.updatePercentage() );
		window.addEventListener( customEvent.SCROLLED, () => this.updatePercentage() );

		// Adds an offset to the ReadProgress button when sticky video is visible
		window.addEventListener( customEvent.STICKY_ON, () => {
			this.$buttonDiv.classList.add( this.stickyOffsetClass );
		});

		// Removes an offset from ReadProgress button when sticky video is not visible
		window.addEventListener( customEvent.STICKY_OFF, () => {
			this.$buttonDiv.classList.remove( this.stickyOffsetClass );
		});

		// We include only the 'article' element into the article reading length on most templates
		// Measure the article height, and it's position from the top of the document
		const article = document.querySelector( this.articleSelector );
		this.topLocation = article.offsetTop;
		this.bottomLocation = this.topLocation + article.offsetHeight;
		this.measurableHeight = article.offsetHeight;

		const $main = document.querySelector( this.mainSelector );
		// On results template we include the 'summary', 'interactive' and 'article' elements
		// into the article reading length due to different markup
		if ( $main.classList.contains( this.mainResultsClass ) ) {
			const summarySection = document.querySelector( this.summarySelector );
			this.topLocation = summarySection.offsetTop;
			const summaryHeight = summarySection.offsetHeight;

			const interactiveSection = document.querySelector( this.interactiveSelector );
			let interactiveHeight = 0;
			if ( interactiveSection ) {
				interactiveHeight = interactiveSection.offsetHeight;
			}

			this.bottomLocation = this.topLocation + summaryHeight + interactiveHeight + article.offsetHeight; // eslint-disable-line max-len
			this.measurableHeight = summaryHeight + interactiveHeight + article.offsetHeight;
		}
	},

	/**
	 * Updates the percentage bar in the circle
	 *
	 * @method updatePercentage
	 */
	updatePercentage() {
		// Find current scroll position from the top of the document
		// to the bottom of the visible screen
		const currentPosition = window.pageYOffset + window.innerHeight;

		let readPercentage = 0;
		if ( currentPosition > this.bottomLocation ) {
			readPercentage = 100;
		} else if ( currentPosition < this.topLocation ) {
			readPercentage = 0;
		} else {
			const positionInArticle = currentPosition - this.topLocation;
			readPercentage = Math.round( ( positionInArticle / this.measurableHeight ) * 100 );
		}

		// Convert percentage into stroke-dashoffset value
		// Note: use the reverse percentage value
		// Ex. if you are 25% into the article, use the remaing 75% to calculate stroke-dashoffset
		const dashOffset = ( 195 / 100 ) * ( 100 - readPercentage );
		this.circle.style.strokeDashoffset = dashOffset;
	},

	/**
	 * Handles click on 'Back To Top' button
	 *
	 * @method handleReadProgressClick
	 */
	handleReadProgressClick() {
		window.scroll({ top: 0, behavior: 'smooth' });
	},
};

export default ReadProgress;