386 lines
9.9 KiB
JavaScript
386 lines
9.9 KiB
JavaScript
|
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, 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];
|
||
|
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( [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 === 191 && event.shiftKey ) {
|
||
|
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();
|
||
|
|
||
|
}
|
||
|
|
||
|
}
|