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); this.onDocumentKeyPress = this.onDocumentKeyPress.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); document.addEventListener("keypress", this.onDocumentKeyPress, false); } /** * Stops listening for keyboard events. */ unbind() { document.removeEventListener("keydown", this.onDocumentKeyDown, false); document.removeEventListener("keypress", this.onDocumentKeyPress, 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 'keypress' event. * * @param {object} event */ onDocumentKeyPress(event) { // Check if the pressed key is question mark if (event.shiftKey && event.charCode === 63) { this.Reveal.toggleHelp(); } } /** * 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 isNavigationKey = [32, 37, 38, 39, 40, 78, 80].indexOf(event.keyCode) !== -1; // Prevent all other events when a modifier is pressed let unusedModifier = !((isNavigationKey && 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]; 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) { 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) { 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 ( keyCode === 58 || keyCode === 59 || keyCode === 66 || keyCode === 86 || keyCode === 190 || keyCode === 191 ) { 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 { 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(); } }