hakimel/reveal.js

View on GitHub
js/controllers/keyboard.js

Summary

Maintainability
F
5 days
Test Coverage
import { enterFullscreen } from '../utils/util.js'

/**
 * Handles all reveal.js keyboard interactions.
 */
export default class Keyboard {

    constructor( Reveal ) {

        this.Reveal = Reveal;

        // A key:value map of keyboard keys and descriptions of
        // the actions they trigger
        this.shortcuts = {};

        // Holds custom key code mappings
        this.bindings = {};

        this.onDocumentKeyDown = this.onDocumentKeyDown.bind( this );

    }

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

        if( config.navigationMode === 'linear' ) {
            this.shortcuts['→  ,  ↓  ,  SPACE  ,  N  ,  L  ,  J'] = 'Next slide';
            this.shortcuts['←  ,  ↑  ,  P  ,  H  ,  K']           = 'Previous slide';
        }
        else {
            this.shortcuts['N  ,  SPACE']   = 'Next slide';
            this.shortcuts['P  ,  Shift SPACE']             = 'Previous slide';
            this.shortcuts['←  ,  H'] = 'Navigate left';
            this.shortcuts['→  ,  L'] = 'Navigate right';
            this.shortcuts['↑  ,  K'] = 'Navigate up';
            this.shortcuts['↓  ,  J'] = 'Navigate down';
        }

        this.shortcuts['Alt + ←/&#8593/→/↓']        = 'Navigate without fragments';
        this.shortcuts['Shift + ←/&#8593/→/↓']      = 'Jump to first/last slide';
        this.shortcuts['B  ,  .']                       = 'Pause';
        this.shortcuts['F']                             = 'Fullscreen';
        this.shortcuts['G']                             = 'Jump to slide';
        this.shortcuts['ESC, O']                        = 'Slide overview';

    }

    /**
     * Starts listening for keyboard events.
     */
    bind() {

        document.addEventListener( 'keydown', this.onDocumentKeyDown, false );

    }

    /**
     * Stops listening for keyboard events.
     */
    unbind() {

        document.removeEventListener( 'keydown', this.onDocumentKeyDown, false );

    }

    /**
     * Add a custom key binding with optional description to
     * be added to the help screen.
     */
    addKeyBinding( binding, callback ) {

        if( typeof binding === 'object' && binding.keyCode ) {
            this.bindings[binding.keyCode] = {
                callback: callback,
                key: binding.key,
                description: binding.description
            };
        }
        else {
            this.bindings[binding] = {
                callback: callback,
                key: null,
                description: null
            };
        }

    }

    /**
     * Removes the specified custom key binding.
     */
    removeKeyBinding( keyCode ) {

        delete this.bindings[keyCode];

    }

    /**
     * Programmatically triggers a keyboard event
     *
     * @param {int} keyCode
     */
    triggerKey( keyCode ) {

        this.onDocumentKeyDown( { keyCode } );

    }

    /**
     * Registers a new shortcut to include in the help overlay
     *
     * @param {String} key
     * @param {String} value
     */
    registerKeyboardShortcut( key, value ) {

        this.shortcuts[key] = value;

    }

    getShortcuts() {

        return this.shortcuts;

    }

    getBindings() {

        return this.bindings;

    }

    /**
     * Handler for the document level 'keydown' event.
     *
     * @param {object} event
     */
    onDocumentKeyDown( event ) {

        let config = this.Reveal.getConfig();

        // If there's a condition specified and it returns false,
        // ignore this event
        if( typeof config.keyboardCondition === 'function' && config.keyboardCondition(event) === false ) {
            return true;
        }

        // If keyboardCondition is set, only capture keyboard events
        // for embedded decks when they are focused
        if( config.keyboardCondition === 'focused' && !this.Reveal.isFocused() ) {
            return true;
        }

        // Shorthand
        let keyCode = event.keyCode;

        // Remember if auto-sliding was paused so we can toggle it
        let autoSlideWasPaused = !this.Reveal.isAutoSliding();

        this.Reveal.onUserInput( event );

        // Is there a focused element that could be using the keyboard?
        let activeElementIsCE = document.activeElement && document.activeElement.isContentEditable === true;
        let activeElementIsInput = document.activeElement && document.activeElement.tagName && /input|textarea/i.test( document.activeElement.tagName );
        let activeElementIsNotes = document.activeElement && document.activeElement.className && /speaker-notes/i.test( document.activeElement.className);

        // Whitelist certain modifiers for slide navigation shortcuts
        let keyCodeUsesModifier = [32, 37, 38, 39, 40, 63, 78, 80, 191].indexOf( event.keyCode ) !== -1;

        // Prevent all other events when a modifier is pressed
        let unusedModifier =     !( keyCodeUsesModifier && event.shiftKey || event.altKey ) &&
                                ( event.shiftKey || event.altKey || event.ctrlKey || event.metaKey );

        // Disregard the event if there's a focused element or a
        // keyboard modifier key is present
        if( activeElementIsCE || activeElementIsInput || activeElementIsNotes || unusedModifier ) return;

        // While paused only allow resume keyboard events; 'b', 'v', '.'
        let resumeKeyCodes = [66,86,190,191,112];
        let key;

        // Custom key bindings for togglePause should be able to resume
        if( typeof config.keyboard === 'object' ) {
            for( key in config.keyboard ) {
                if( config.keyboard[key] === 'togglePause' ) {
                    resumeKeyCodes.push( parseInt( key, 10 ) );
                }
            }
        }

        if( this.Reveal.isPaused() && resumeKeyCodes.indexOf( keyCode ) === -1 ) {
            return false;
        }

        // Use linear navigation if we're configured to OR if
        // the presentation is one-dimensional
        let useLinearMode = config.navigationMode === 'linear' || !this.Reveal.hasHorizontalSlides() || !this.Reveal.hasVerticalSlides();

        let triggered = false;

        // 1. User defined key bindings
        if( typeof config.keyboard === 'object' ) {

            for( key in config.keyboard ) {

                // Check if this binding matches the pressed key
                if( parseInt( key, 10 ) === keyCode ) {

                    let value = config.keyboard[ key ];

                    // Callback function
                    if( typeof value === 'function' ) {
                        value.apply( null, [ event ] );
                    }
                    // String shortcuts to reveal.js API
                    else if( typeof value === 'string' && typeof this.Reveal[ value ] === 'function' ) {
                        this.Reveal[ value ].call();
                    }

                    triggered = true;

                }

            }

        }

        // 2. Registered custom key bindings
        if( triggered === false ) {

            for( key in this.bindings ) {

                // Check if this binding matches the pressed key
                if( parseInt( key, 10 ) === keyCode ) {

                    let action = this.bindings[ key ].callback;

                    // Callback function
                    if( typeof action === 'function' ) {
                        action.apply( null, [ event ] );
                    }
                    // String shortcuts to reveal.js API
                    else if( typeof action === 'string' && typeof this.Reveal[ action ] === 'function' ) {
                        this.Reveal[ action ].call();
                    }

                    triggered = true;
                }
            }
        }

        // 3. System defined key bindings
        if( triggered === false ) {

            // Assume true and try to prove false
            triggered = true;

            // P, PAGE UP
            if( keyCode === 80 || keyCode === 33 ) {
                this.Reveal.prev({skipFragments: event.altKey});
            }
            // N, PAGE DOWN
            else if( keyCode === 78 || keyCode === 34 ) {
                this.Reveal.next({skipFragments: event.altKey});
            }
            // H, LEFT
            else if( keyCode === 72 || keyCode === 37 ) {
                if( event.shiftKey ) {
                    this.Reveal.slide( 0 );
                }
                else if( !this.Reveal.overview.isActive() && useLinearMode ) {
                    if( config.rtl ) {
                        this.Reveal.next({skipFragments: event.altKey});
                    }
                    else {
                        this.Reveal.prev({skipFragments: event.altKey});
                    }
                }
                else {
                    this.Reveal.left({skipFragments: event.altKey});
                }
            }
            // L, RIGHT
            else if( keyCode === 76 || keyCode === 39 ) {
                if( event.shiftKey ) {
                    this.Reveal.slide( this.Reveal.getHorizontalSlides().length - 1 );
                }
                else if( !this.Reveal.overview.isActive() && useLinearMode ) {
                    if( config.rtl ) {
                        this.Reveal.prev({skipFragments: event.altKey});
                    }
                    else {
                        this.Reveal.next({skipFragments: event.altKey});
                    }
                }
                else {
                    this.Reveal.right({skipFragments: event.altKey});
                }
            }
            // K, UP
            else if( keyCode === 75 || keyCode === 38 ) {
                if( event.shiftKey ) {
                    this.Reveal.slide( undefined, 0 );
                }
                else if( !this.Reveal.overview.isActive() && useLinearMode ) {
                    this.Reveal.prev({skipFragments: event.altKey});
                }
                else {
                    this.Reveal.up({skipFragments: event.altKey});
                }
            }
            // J, DOWN
            else if( keyCode === 74 || keyCode === 40 ) {
                if( event.shiftKey ) {
                    this.Reveal.slide( undefined, Number.MAX_VALUE );
                }
                else if( !this.Reveal.overview.isActive() && useLinearMode ) {
                    this.Reveal.next({skipFragments: event.altKey});
                }
                else {
                    this.Reveal.down({skipFragments: event.altKey});
                }
            }
            // HOME
            else if( keyCode === 36 ) {
                this.Reveal.slide( 0 );
            }
            // END
            else if( keyCode === 35 ) {
                this.Reveal.slide( this.Reveal.getHorizontalSlides().length - 1 );
            }
            // SPACE
            else if( keyCode === 32 ) {
                if( this.Reveal.overview.isActive() ) {
                    this.Reveal.overview.deactivate();
                }
                if( event.shiftKey ) {
                    this.Reveal.prev({skipFragments: event.altKey});
                }
                else {
                    this.Reveal.next({skipFragments: event.altKey});
                }
            }
            // TWO-SPOT, SEMICOLON, B, V, PERIOD, LOGITECH PRESENTER TOOLS "BLACK SCREEN" BUTTON
            else if( [58, 59, 66, 86, 190].includes( keyCode ) || ( keyCode === 191 && !event.shiftKey ) ) {
                this.Reveal.togglePause();
            }
            // F
            else if( keyCode === 70 ) {
                enterFullscreen( config.embedded ? this.Reveal.getViewportElement() : document.documentElement );
            }
            // A
            else if( keyCode === 65 ) {
                if( config.autoSlideStoppable ) {
                    this.Reveal.toggleAutoSlide( autoSlideWasPaused );
                }
            }
            // G
            else if( keyCode === 71 ) {
                if( config.jumpToSlide ) {
                    this.Reveal.toggleJumpToSlide();
                }
            }
            // ?
            else if( ( keyCode === 63 || keyCode === 191 ) && event.shiftKey ) {
                this.Reveal.toggleHelp();
            }
            // F1
            else if( keyCode === 112 ) {
                this.Reveal.toggleHelp();
            }
            else {
                triggered = false;
            }

        }

        // If the input resulted in a triggered action we should prevent
        // the browsers default behavior
        if( triggered ) {
            event.preventDefault && event.preventDefault();
        }
        // ESC or O key
        else if( keyCode === 27 || keyCode === 79 ) {
            if( this.Reveal.closeOverlay() === false ) {
                this.Reveal.overview.toggle();
            }

            event.preventDefault && event.preventDefault();
        }

        // If auto-sliding is enabled we need to cue up
        // another timeout
        this.Reveal.cueAutoSlide();

    }

}