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 + ←/↑/→/↓'] = 'Navigate without fragments'; this.shortcuts['Shift + ←/↑/→/↓'] = '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(); } }