Commit 3d621237 authored by Hakim El Hattab's avatar Hakim El Hattab
Browse files

new keyboard module

parent ddbe06eb
This diff is collapsed.
import { extend, toArray, enterFullscreen } from '../utils/util.js'
/**
*
*/
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 );
}
refreshSortcuts() {
// Define our contextual list of keyboard shortcuts
if( this.Reveal.getConfig().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'] = 'Previous slide';
this.shortcuts['← , H'] = 'Navigate left';
this.shortcuts['→ , L'] = 'Navigate right';
this.shortcuts['↑ , K'] = 'Navigate up';
this.shortcuts['↓ , J'] = 'Navigate down';
}
this.shortcuts['Home , Shift ←'] = 'First slide';
this.shortcuts['End , Shift →'] = 'Last slide';
this.shortcuts['B , .'] = 'Pause';
this.shortcuts['F'] = 'Fullscreen';
this.shortcuts['ESC, O'] = 'Slide overview';
}
bind() {
document.addEventListener( 'keydown', this.onDocumentKeyDown, false );
document.addEventListener( 'keypress', this.onDocumentKeyPress, false );
}
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;
}
/**
* 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;
}
// 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.contentEditable !== 'inherit';
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 specific modified + keycode combinations
let prevSlideShortcut = event.shiftKey && event.keyCode === 32;
let firstSlideShortcut = event.shiftKey && keyCode === 37;
let lastSlideShortcut = event.shiftKey && keyCode === 39;
// Prevent all other events when a modifier is pressed
let unusedModifier = !prevSlideShortcut && !firstSlideShortcut && !lastSlideShortcut &&
( 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();
}
// N, PAGE DOWN
else if( keyCode === 78 || keyCode === 34 ) {
this.Review.next();
}
// H, LEFT
else if( keyCode === 72 || keyCode === 37 ) {
if( firstSlideShortcut ) {
this.Reveal.slide( 0 );
}
else if( !this.Reveal.overview.isActive() && useLinearMode ) {
this.Reveal.prev();
}
else {
this.Reveal.left();
}
}
// L, RIGHT
else if( keyCode === 76 || keyCode === 39 ) {
if( lastSlideShortcut ) {
this.Reveal.slide( Number.MAX_VALUE );
}
else if( !this.Reveal.overview.isActive() && useLinearMode ) {
this.Review.next();
}
else {
this.Reveal.right();
}
}
// K, UP
else if( keyCode === 75 || keyCode === 38 ) {
if( !this.Reveal.overview.isActive() && useLinearMode ) {
this.Reveal.prev();
}
else {
this.Reveal.up();
}
}
// J, DOWN
else if( keyCode === 74 || keyCode === 40 ) {
if( !this.Reveal.overview.isActive() && useLinearMode ) {
this.Review.next();
}
else {
this.Reveal.down();
}
}
// HOME
else if( keyCode === 36 ) {
this.Reveal.slide( 0 );
}
// END
else if( keyCode === 35 ) {
this.Reveal.slide( Number.MAX_VALUE );
}
// SPACE
else if( keyCode === 32 ) {
if( this.Reveal.overview.isActive() ) {
this.Reveal.overview.deactivate();
}
if( event.shiftKey ) {
this.Reveal.prev();
}
else {
this.Review.next();
}
}
// 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();
}
// A
else if( keyCode === 65 ) {
if ( config.autoSlideStoppable ) {
this.Reveal.toggleAutoSlide( autoSlideWasPaused );
}
}
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();
}
}
\ No newline at end of file
......@@ -2,6 +2,7 @@ import SlideContent from './controllers/slidecontent.js'
import AutoAnimate from './controllers/autoanimate.js'
import Fragments from './controllers/fragments.js'
import Overview from './controllers/overview.js'
import Keyboard from './controllers/keyboard.js'
import Plugins from './controllers/plugins.js'
import Playback from './components/playback.js'
import defaultConfig from './config.js'
......@@ -85,8 +86,12 @@ export default function( revealElement, options ) {
// Controls navigation between slide fragments
fragments = new Fragments( Reveal ),
// Controls the birds-eye overview of slides
overview = new Overview( Reveal ),
// Controls all keyboard interactions
keyboard = new Keyboard( Reveal ),
// List of asynchronously loaded reveal.js dependencies
asyncDependencies = [],
......@@ -121,14 +126,7 @@ export default function( revealElement, options ) {
startCount: 0,
captured: false,
threshold: 40
},
// A key:value map of shortcut keyboard keys and descriptions of
// the actions they trigger, generated in #configure()
keyboardShortcuts = {},
// Holds custom key code mappings
registeredKeyBindings = {};
};
/**
* Starts up the presentation if the client is capable.
......@@ -949,25 +947,7 @@ export default function( revealElement, options ) {
dom.wrapper.removeAttribute( 'data-navigation-mode' );
}
// Define our contextual list of keyboard shortcuts
if( config.navigationMode === 'linear' ) {
keyboardShortcuts['→ , ↓ , SPACE , N , L , J'] = 'Next slide';
keyboardShortcuts['← , ↑ , P , H , K'] = 'Previous slide';
}
else {
keyboardShortcuts['N , SPACE'] = 'Next slide';
keyboardShortcuts['P'] = 'Previous slide';
keyboardShortcuts['← , H'] = 'Navigate left';
keyboardShortcuts['→ , L'] = 'Navigate right';
keyboardShortcuts['↑ , K'] = 'Navigate up';
keyboardShortcuts['↓ , J'] = 'Navigate down';
}
keyboardShortcuts['Home , Shift ←'] = 'First slide';
keyboardShortcuts['End , Shift →'] = 'Last slide';
keyboardShortcuts['B , .'] = 'Pause';
keyboardShortcuts['F'] = 'Fullscreen';
keyboardShortcuts['ESC, O'] = 'Slide overview';
keyboard.refreshSortcuts();
sync();
......@@ -1005,8 +985,7 @@ export default function( revealElement, options ) {
}
if( config.keyboard ) {
document.addEventListener( 'keydown', onDocumentKeyDown, false );
document.addEventListener( 'keypress', onDocumentKeyPress, false );
keyboard.bind();
}
if( config.progress && dom.progress ) {
......@@ -1047,8 +1026,8 @@ export default function( revealElement, options ) {
eventsAreBound = false;
document.removeEventListener( 'keydown', onDocumentKeyDown, false );
document.removeEventListener( 'keypress', onDocumentKeyPress, false );
keyboard.unbind();
window.removeEventListener( 'hashchange', onWindowHashChange, false );
window.removeEventListener( 'resize', onWindowResize, false );
......@@ -1081,38 +1060,6 @@ export default function( revealElement, options ) {
}
/**
* Add a custom key binding with optional description to
* be added to the help screen.
*/
function addKeyBinding( binding, callback ) {
if( typeof binding === 'object' && binding.keyCode ) {
registeredKeyBindings[binding.keyCode] = {
callback: callback,
key: binding.key,
description: binding.description
};
}
else {
registeredKeyBindings[binding] = {
callback: callback,
key: null,
description: null
};
}
}
/**
* Removes the specified custom key binding.
*/
function removeKeyBinding( keyCode ) {
delete registeredKeyBindings[keyCode];
}
/**
* Applies CSS transforms to the slides container. The container
* is transformed from two separate sources: layout and the overview
......@@ -1327,14 +1274,14 @@ export default function( revealElement, options ) {
let html = '<p class="title">Keyboard Shortcuts</p><br/>';
html += '<table><th>KEY</th><th>ACTION</th>';
for( let key in keyboardShortcuts ) {
html += `<tr><td>${key}</td><td>${keyboardShortcuts[ key ]}</td></tr>`;
for( let key in keyboard.shortcuts ) {
html += `<tr><td>${key}</td><td>${keyboard.shortcuts[ key ]}</td></tr>`;
}
// Add custom key bindings that have associated descriptions
for( let binding in registeredKeyBindings ) {
if( registeredKeyBindings[binding].key && registeredKeyBindings[binding].description ) {
html += `<tr><td>${registeredKeyBindings[binding].key}</td><td>${registeredKeyBindings[binding].description}</td></tr>`;
for( let binding in keyboard.registeredKeyBindings ) {
if( keyboard.registeredKeyBindings[binding].key && keyboard.registeredKeyBindings[binding].description ) {
html += `<tr><td>${keyboard.registeredKeyBindings[binding].key}</td><td>${keyboard.registeredKeyBindings[binding].description}</td></tr>`;
}
}
......@@ -1366,8 +1313,11 @@ export default function( revealElement, options ) {
if( dom.overlay ) {
dom.overlay.parentNode.removeChild( dom.overlay );
dom.overlay = null;
return true;
}
return false;
}
/**
......@@ -3482,252 +3432,6 @@ export default function( revealElement, options ) {
}
/**
* Handler for the document level 'keypress' event.
*
* @param {object} event
*/
function onDocumentKeyPress( event ) {
// Check if the pressed key is question mark
if( event.shiftKey && event.charCode === 63 ) {
toggleHelp();
}
}
/**
* Handler for the document level 'keydown' event.
*
* @param {object} event
*/
function onDocumentKeyDown( event ) {
// If there's a condition specified and it returns false,
// ignore this event
if( typeof config.keyboardCondition === 'function' && config.keyboardCondition(event) === false ) {
return true;
}
// Shorthand
let keyCode = event.keyCode;
// Remember if auto-sliding was paused so we can toggle it
let autoSlideWasPaused = autoSlidePaused;
onUserInput( event );
// Is there a focused element that could be using the keyboard?
let activeElementIsCE = document.activeElement && document.activeElement.contentEditable !== 'inherit';
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 specific modified + keycode combinations
let prevSlideShortcut = event.shiftKey && event.keyCode === 32;
let firstSlideShortcut = event.shiftKey && keyCode === 37;
let lastSlideShortcut = event.shiftKey && keyCode === 39;
// Prevent all other events when a modifier is pressed
let unusedModifier = !prevSlideShortcut && !firstSlideShortcut && !lastSlideShortcut &&
( 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( 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' || !hasHorizontalSlides() || !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 Reveal[ value ] === 'function' ) {
Reveal[ value ].call();
}