hakimel/reveal.js

View on GitHub
js/controllers/controls.js

Summary

Maintainability
F
6 days
Test Coverage
import { queryAll, enterFullscreen } 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
 * - .enter-fullscreen
 */
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 );
        this.onEnterFullscreen = this.onEnterFullscreen.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' );
        this.controlsFullscreen = queryAll( revealElement, '.enter-fullscreen' );

        // 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 ) );
            this.controlsFullscreen.forEach( el => el.addEventListener( eventName, this.onEnterFullscreen, 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 ) );
            this.controlsFullscreen.forEach( el => el.removeEventListener( eventName, this.onEnterFullscreen, 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' );
                    }
                }
            }
        }
    }

    destroy() {

        this.unbind();
        this.element.remove();

    }

    /**
     * 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();

    }

    onEnterFullscreen( event ) {

        const config = this.Reveal.getConfig();
        const viewport = this.Reveal.getViewportElement();

        enterFullscreen( config.embedded ? viewport : viewport.parentElement );

    }

}