aboutsummaryrefslogblamecommitdiff
path: root/static/presentations/2021-11-13/garage/js/controllers/controls.js
blob: 556bcf083d1d242beacef7d9810081a792702d41 (plain) (tree)


































































































































































































































































                                                                                                                                                                                  
import { queryAll } from '../utils/util.js'
import { isAndroid } from '../utils/device.js'

/**
 * Manages our presentation controls. This includes both
 * the built-in control arrows as well as event monitoring
 * of any elements within the presentation with either of the
 * following helper classes:
 * - .navigate-up
 * - .navigate-right
 * - .navigate-down
 * - .navigate-left
 * - .navigate-next
 * - .navigate-prev
 */
export default class Controls {

	constructor( Reveal ) {

		this.Reveal = Reveal;

		this.onNavigateLeftClicked = this.onNavigateLeftClicked.bind( this );
		this.onNavigateRightClicked = this.onNavigateRightClicked.bind( this );
		this.onNavigateUpClicked = this.onNavigateUpClicked.bind( this );
		this.onNavigateDownClicked = this.onNavigateDownClicked.bind( this );
		this.onNavigatePrevClicked = this.onNavigatePrevClicked.bind( this );
		this.onNavigateNextClicked = this.onNavigateNextClicked.bind( this );

	}

	render() {

		const rtl = this.Reveal.getConfig().rtl;
		const revealElement = this.Reveal.getRevealElement();

		this.element = document.createElement( 'aside' );
		this.element.className = 'controls';
		this.element.innerHTML =
			`<button class="navigate-left" aria-label="${ rtl ? 'next slide' : 'previous slide' }"><div class="controls-arrow"></div></button>
			<button class="navigate-right" aria-label="${ rtl ? 'previous slide' : 'next slide' }"><div class="controls-arrow"></div></button>
			<button class="navigate-up" aria-label="above slide"><div class="controls-arrow"></div></button>
			<button class="navigate-down" aria-label="below slide"><div class="controls-arrow"></div></button>`;

		this.Reveal.getRevealElement().appendChild( this.element );

		// There can be multiple instances of controls throughout the page
		this.controlsLeft = queryAll( revealElement, '.navigate-left' );
		this.controlsRight = queryAll( revealElement, '.navigate-right' );
		this.controlsUp = queryAll( revealElement, '.navigate-up' );
		this.controlsDown = queryAll( revealElement, '.navigate-down' );
		this.controlsPrev = queryAll( revealElement, '.navigate-prev' );
		this.controlsNext = queryAll( revealElement, '.navigate-next' );

		// The left, right and down arrows in the standard reveal.js controls
		this.controlsRightArrow = this.element.querySelector( '.navigate-right' );
		this.controlsLeftArrow = this.element.querySelector( '.navigate-left' );
		this.controlsDownArrow = this.element.querySelector( '.navigate-down' );

	}

	/**
	 * Called when the reveal.js config is updated.
	 */
	configure( config, oldConfig ) {

		this.element.style.display = config.controls ? 'block' : 'none';

		this.element.setAttribute( 'data-controls-layout', config.controlsLayout );
		this.element.setAttribute( 'data-controls-back-arrows', config.controlsBackArrows );

	}

	bind() {

		// Listen to both touch and click events, in case the device
		// supports both
		let pointerEvents = [ 'touchstart', 'click' ];

		// Only support touch for Android, fixes double navigations in
		// stock browser
		if( isAndroid ) {
			pointerEvents = [ 'touchstart' ];
		}

		pointerEvents.forEach( eventName => {
			this.controlsLeft.forEach( el => el.addEventListener( eventName, this.onNavigateLeftClicked, false ) );
			this.controlsRight.forEach( el => el.addEventListener( eventName, this.onNavigateRightClicked, false ) );
			this.controlsUp.forEach( el => el.addEventListener( eventName, this.onNavigateUpClicked, false ) );
			this.controlsDown.forEach( el => el.addEventListener( eventName, this.onNavigateDownClicked, false ) );
			this.controlsPrev.forEach( el => el.addEventListener( eventName, this.onNavigatePrevClicked, false ) );
			this.controlsNext.forEach( el => el.addEventListener( eventName, this.onNavigateNextClicked, false ) );
		} );

	}

	unbind() {

		[ 'touchstart', 'click' ].forEach( eventName => {
			this.controlsLeft.forEach( el => el.removeEventListener( eventName, this.onNavigateLeftClicked, false ) );
			this.controlsRight.forEach( el => el.removeEventListener( eventName, this.onNavigateRightClicked, false ) );
			this.controlsUp.forEach( el => el.removeEventListener( eventName, this.onNavigateUpClicked, false ) );
			this.controlsDown.forEach( el => el.removeEventListener( eventName, this.onNavigateDownClicked, false ) );
			this.controlsPrev.forEach( el => el.removeEventListener( eventName, this.onNavigatePrevClicked, false ) );
			this.controlsNext.forEach( el => el.removeEventListener( eventName, this.onNavigateNextClicked, false ) );
		} );

	}

	/**
	 * Updates the state of all control/navigation arrows.
	 */
	update() {

		let routes = this.Reveal.availableRoutes();

		// Remove the 'enabled' class from all directions
		[...this.controlsLeft, ...this.controlsRight, ...this.controlsUp, ...this.controlsDown, ...this.controlsPrev, ...this.controlsNext].forEach( node => {
			node.classList.remove( 'enabled', 'fragmented' );

			// Set 'disabled' attribute on all directions
			node.setAttribute( 'disabled', 'disabled' );
		} );

		// Add the 'enabled' class to the available routes; remove 'disabled' attribute to enable buttons
		if( routes.left ) this.controlsLeft.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
		if( routes.right ) this.controlsRight.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
		if( routes.up ) this.controlsUp.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
		if( routes.down ) this.controlsDown.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );

		// Prev/next buttons
		if( routes.left || routes.up ) this.controlsPrev.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );
		if( routes.right || routes.down ) this.controlsNext.forEach( el => { el.classList.add( 'enabled' ); el.removeAttribute( 'disabled' ); } );

		// Highlight fragment directions
		let currentSlide = this.Reveal.getCurrentSlide();
		if( currentSlide ) {

			let fragmentsRoutes = this.Reveal.fragments.availableRoutes();

			// Always apply fragment decorator to prev/next buttons
			if( fragmentsRoutes.prev ) this.controlsPrev.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
			if( fragmentsRoutes.next ) this.controlsNext.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );

			// Apply fragment decorators to directional buttons based on
			// what slide axis they are in
			if( this.Reveal.isVerticalSlide( currentSlide ) ) {
				if( fragmentsRoutes.prev ) this.controlsUp.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
				if( fragmentsRoutes.next ) this.controlsDown.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
			}
			else {
				if( fragmentsRoutes.prev ) this.controlsLeft.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
				if( fragmentsRoutes.next ) this.controlsRight.forEach( el => { el.classList.add( 'fragmented', 'enabled' ); el.removeAttribute( 'disabled' ); } );
			}

		}

		if( this.Reveal.getConfig().controlsTutorial ) {

			let indices = this.Reveal.getIndices();

			// Highlight control arrows with an animation to ensure
			// that the viewer knows how to navigate
			if( !this.Reveal.hasNavigatedVertically() && routes.down ) {
				this.controlsDownArrow.classList.add( 'highlight' );
			}
			else {
				this.controlsDownArrow.classList.remove( 'highlight' );

				if( this.Reveal.getConfig().rtl ) {

					if( !this.Reveal.hasNavigatedHorizontally() && routes.left && indices.v === 0 ) {
						this.controlsLeftArrow.classList.add( 'highlight' );
					}
					else {
						this.controlsLeftArrow.classList.remove( 'highlight' );
					}

				} else {

					if( !this.Reveal.hasNavigatedHorizontally() && routes.right && indices.v === 0 ) {
						this.controlsRightArrow.classList.add( 'highlight' );
					}
					else {
						this.controlsRightArrow.classList.remove( 'highlight' );
					}
				}
			}
		}
	}

	/**
	 * Event handlers for navigation control buttons.
	 */
	onNavigateLeftClicked( event ) {

		event.preventDefault();
		this.Reveal.onUserInput();

		if( this.Reveal.getConfig().navigationMode === 'linear' ) {
			this.Reveal.prev();
		}
		else {
			this.Reveal.left();
		}

	}

	onNavigateRightClicked( event ) {

		event.preventDefault();
		this.Reveal.onUserInput();

		if( this.Reveal.getConfig().navigationMode === 'linear' ) {
			this.Reveal.next();
		}
		else {
			this.Reveal.right();
		}

	}

	onNavigateUpClicked( event ) {

		event.preventDefault();
		this.Reveal.onUserInput();

		this.Reveal.up();

	}

	onNavigateDownClicked( event ) {

		event.preventDefault();
		this.Reveal.onUserInput();

		this.Reveal.down();

	}

	onNavigatePrevClicked( event ) {

		event.preventDefault();
		this.Reveal.onUserInput();

		this.Reveal.prev();

	}

	onNavigateNextClicked( event ) {

		event.preventDefault();
		this.Reveal.onUserInput();

		this.Reveal.next();

	}


}