article/BackToTop.js

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

/**
 * Displays 'Back to Top' button when user scrolls up quickly
 *
 * @module BackToTop
 * @prop {Integer} oldScrollValue - initial vertical position on the page
 * @prop {Integer} tabletBreakpoint - Tablet-portrait breakpoint, same as in CSS
 * @prop {String} tagMetaSelector - DOM reference to tag meta header
 * @prop {String} buttonDivSelector - Class reference for Back to Top button container
 * @prop {String} buttonDivModifier - CSS modifier accounting for larger tag meta header offset
 * @prop {String} buttonSelector - Class reference for Back to Top button
 * @prop {String} fadeInClass - Fade in animation class
 * @prop {String} fadeOutClass - Fade out animation class
 * @prop {String} resetClass - 3d transform animation class
 * @prop {Object} $buttonDiv - DOM reference to Back to Top button
 */
const BackToTop = {
	oldScrollValue: 0,
	tabletBreakpoint: 768, // Same as css breakpoint tablet-portrait
	tagMetaSelector: '#custom-navbar',
	buttonDivSelector: '.c-backToTop',
	buttonDivModifier: 'c-backToTop--tagMeta',
	buttonSelector: '.c-backToTop__button',
	fadeInClass: 'is-slid-and-faded-in',
	fadeOutClass: 'is-slid-and-faded-out',
	resetClass: 'is-reset',
	hiddenClass: 'is-hidden',
	$buttonDiv: null,

	/**
	 * Initializes 'Back To Top' button on article pages
	 *
	 * @method init
	 */
	init() {
		// Make sure there is a button container on the page
		if ( ! document.querySelector( this.buttonDivSelector ) ) {
			return;
		}

		this.$buttonDiv = document.querySelector( this.buttonDivSelector );

		// Don't run on screens larger than tablet-portrait
		if ( window.innerWidth >= this.tabletBreakpoint ) {
			return;
		}

		// Detect if this article has a tag meta header
		if ( document.querySelector( this.tagMetaSelector ) ) {
			this.$buttonDiv.classList.add( this.buttonDivModifier );
		}

		// 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.handleBackToTopClick();
			});
		}

		window.addEventListener( customEvent.SCROLLED_TO_TOP, () => this.handleScrollToTop() );
		window.addEventListener( customEvent.SCROLLED, evt => this.handleUpwardScroll( evt ) );
		window.addEventListener( customEvent.SCROLL_DIRECTION_CHANGED,
			evt => this.handleDownwardScroll( evt ) );
	},

	/**
	 * Handles upward page scroll.
	 *
	 * @method handleScroll
	 */
	handleUpwardScroll( event ) {
		const { direction } = event.detail;

		// When user scrolls up by 100px or more, show button if it's currently not visible
		if ( 'up' === direction
			&& ! BackToTop.$buttonDiv.classList.contains( BackToTop.resetClass ) ) {
			BackToTop.showButton();
		}
	},

	/**
	 * Handles downward page scroll.
	 *
	 * @method handleScroll
	 */
	handleDownwardScroll( event ) {
		const { direction } = event.detail;

		// When user scrolls down, hide button if it's currently visible
		if ( 'down' === direction ) {
			BackToTop.hideButton();
		}
	},

	/**
	 * Handles page scroll back to the top.
	 *
	 * @method handleScroll
	 */
	handleScrollToTop() {
		// When page scrolls all the way to the top, hide button
		BackToTop.hideButton();
		ScrollingNav.resetTransitions();
	},

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

	/**
	 * Brings 'Back To Top' button out of view
	 *
	 * @method hideButton
	 */
	hideButton() {
		this.$buttonDiv.classList.remove( this.resetClass );
		this.$buttonDiv.classList.remove( this.fadeInClass );
		this.$buttonDiv.classList.add( this.fadeOutClass );
	},

	/**
	 * Brings 'Back To Top' button into view
	 *
	 * @method showButton
	 */
	showButton() {
		this.$buttonDiv.classList.add( this.resetClass );
		this.$buttonDiv.classList.add( this.fadeInClass );
		this.$buttonDiv.classList.remove( this.fadeOutClass );
	},
};

export default BackToTop;