blob: f09cd938de8be69a925e8ef54db9e1fdb6068fd9 [file] [log] [blame]
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +02001import { enterFullscreen } from '../utils/util.js'
2
3/**
4 * Handles all reveal.js keyboard interactions.
5 */
6export default class Keyboard {
7
8 constructor( Reveal ) {
9
10 this.Reveal = Reveal;
11
12 // A key:value map of keyboard keys and descriptions of
13 // the actions they trigger
14 this.shortcuts = {};
15
16 // Holds custom key code mappings
17 this.bindings = {};
18
19 this.onDocumentKeyDown = this.onDocumentKeyDown.bind( this );
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +020020
21 }
22
23 /**
24 * Called when the reveal.js config is updated.
25 */
26 configure( config, oldConfig ) {
27
28 if( config.navigationMode === 'linear' ) {
29 this.shortcuts['→ , ↓ , SPACE , N , L , J'] = 'Next slide';
30 this.shortcuts['← , ↑ , P , H , K'] = 'Previous slide';
31 }
32 else {
33 this.shortcuts['N , SPACE'] = 'Next slide';
Christophe Dervieux8afae132021-12-06 15:16:42 +010034 this.shortcuts['P , Shift SPACE'] = 'Previous slide';
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +020035 this.shortcuts['← , H'] = 'Navigate left';
36 this.shortcuts['→ , L'] = 'Navigate right';
37 this.shortcuts['↑ , K'] = 'Navigate up';
38 this.shortcuts['↓ , J'] = 'Navigate down';
39 }
40
Christophe Dervieux8afae132021-12-06 15:16:42 +010041 this.shortcuts['Alt + ←/&#8593/→/↓'] = 'Navigate without fragments';
42 this.shortcuts['Shift + ←/&#8593/→/↓'] = 'Jump to first/last slide';
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +020043 this.shortcuts['B , .'] = 'Pause';
44 this.shortcuts['F'] = 'Fullscreen';
Marc Kupietz09b75752023-10-07 09:32:19 +020045 this.shortcuts['G'] = 'Jump to slide';
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +020046 this.shortcuts['ESC, O'] = 'Slide overview';
47
48 }
49
50 /**
51 * Starts listening for keyboard events.
52 */
53 bind() {
54
55 document.addEventListener( 'keydown', this.onDocumentKeyDown, false );
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +020056
57 }
58
59 /**
60 * Stops listening for keyboard events.
61 */
62 unbind() {
63
64 document.removeEventListener( 'keydown', this.onDocumentKeyDown, false );
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +020065
66 }
67
68 /**
69 * Add a custom key binding with optional description to
70 * be added to the help screen.
71 */
72 addKeyBinding( binding, callback ) {
73
74 if( typeof binding === 'object' && binding.keyCode ) {
75 this.bindings[binding.keyCode] = {
76 callback: callback,
77 key: binding.key,
78 description: binding.description
79 };
80 }
81 else {
82 this.bindings[binding] = {
83 callback: callback,
84 key: null,
85 description: null
86 };
87 }
88
89 }
90
91 /**
92 * Removes the specified custom key binding.
93 */
94 removeKeyBinding( keyCode ) {
95
96 delete this.bindings[keyCode];
97
98 }
99
100 /**
101 * Programmatically triggers a keyboard event
102 *
103 * @param {int} keyCode
104 */
105 triggerKey( keyCode ) {
106
107 this.onDocumentKeyDown( { keyCode } );
108
109 }
110
111 /**
112 * Registers a new shortcut to include in the help overlay
113 *
114 * @param {String} key
115 * @param {String} value
116 */
117 registerKeyboardShortcut( key, value ) {
118
119 this.shortcuts[key] = value;
120
121 }
122
123 getShortcuts() {
124
125 return this.shortcuts;
126
127 }
128
129 getBindings() {
130
131 return this.bindings;
132
133 }
134
135 /**
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200136 * Handler for the document level 'keydown' event.
137 *
138 * @param {object} event
139 */
140 onDocumentKeyDown( event ) {
141
142 let config = this.Reveal.getConfig();
143
144 // If there's a condition specified and it returns false,
145 // ignore this event
146 if( typeof config.keyboardCondition === 'function' && config.keyboardCondition(event) === false ) {
147 return true;
148 }
149
150 // If keyboardCondition is set, only capture keyboard events
151 // for embedded decks when they are focused
152 if( config.keyboardCondition === 'focused' && !this.Reveal.isFocused() ) {
153 return true;
154 }
155
156 // Shorthand
157 let keyCode = event.keyCode;
158
159 // Remember if auto-sliding was paused so we can toggle it
160 let autoSlideWasPaused = !this.Reveal.isAutoSliding();
161
162 this.Reveal.onUserInput( event );
163
164 // Is there a focused element that could be using the keyboard?
165 let activeElementIsCE = document.activeElement && document.activeElement.isContentEditable === true;
166 let activeElementIsInput = document.activeElement && document.activeElement.tagName && /input|textarea/i.test( document.activeElement.tagName );
167 let activeElementIsNotes = document.activeElement && document.activeElement.className && /speaker-notes/i.test( document.activeElement.className);
168
Christophe Dervieux8afae132021-12-06 15:16:42 +0100169 // Whitelist certain modifiers for slide navigation shortcuts
Marc Kupietz9c036a42024-05-14 13:17:25 +0200170 let keyCodeUsesModifier = [32, 37, 38, 39, 40, 63, 78, 80, 191].indexOf( event.keyCode ) !== -1;
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200171
172 // Prevent all other events when a modifier is pressed
Marc Kupietz9c036a42024-05-14 13:17:25 +0200173 let unusedModifier = !( keyCodeUsesModifier && event.shiftKey || event.altKey ) &&
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200174 ( event.shiftKey || event.altKey || event.ctrlKey || event.metaKey );
175
176 // Disregard the event if there's a focused element or a
177 // keyboard modifier key is present
178 if( activeElementIsCE || activeElementIsInput || activeElementIsNotes || unusedModifier ) return;
179
180 // While paused only allow resume keyboard events; 'b', 'v', '.'
Marc Kupietz9c036a42024-05-14 13:17:25 +0200181 let resumeKeyCodes = [66,86,190,191,112];
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200182 let key;
183
184 // Custom key bindings for togglePause should be able to resume
185 if( typeof config.keyboard === 'object' ) {
186 for( key in config.keyboard ) {
187 if( config.keyboard[key] === 'togglePause' ) {
188 resumeKeyCodes.push( parseInt( key, 10 ) );
189 }
190 }
191 }
192
Marc Kupietzcf6e9982026-08-15 15:37:40 +0200193 if( this.Reveal.isOverlayOpen() && !["Escape", "f", "c", "b", "."].includes(event.key) ) {
194 return false;
195 }
196
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200197 if( this.Reveal.isPaused() && resumeKeyCodes.indexOf( keyCode ) === -1 ) {
198 return false;
199 }
200
201 // Use linear navigation if we're configured to OR if
202 // the presentation is one-dimensional
203 let useLinearMode = config.navigationMode === 'linear' || !this.Reveal.hasHorizontalSlides() || !this.Reveal.hasVerticalSlides();
204
205 let triggered = false;
206
207 // 1. User defined key bindings
208 if( typeof config.keyboard === 'object' ) {
209
210 for( key in config.keyboard ) {
211
212 // Check if this binding matches the pressed key
213 if( parseInt( key, 10 ) === keyCode ) {
214
215 let value = config.keyboard[ key ];
216
217 // Callback function
218 if( typeof value === 'function' ) {
219 value.apply( null, [ event ] );
220 }
221 // String shortcuts to reveal.js API
222 else if( typeof value === 'string' && typeof this.Reveal[ value ] === 'function' ) {
223 this.Reveal[ value ].call();
224 }
225
226 triggered = true;
227
228 }
229
230 }
231
232 }
233
234 // 2. Registered custom key bindings
235 if( triggered === false ) {
236
237 for( key in this.bindings ) {
238
239 // Check if this binding matches the pressed key
240 if( parseInt( key, 10 ) === keyCode ) {
241
242 let action = this.bindings[ key ].callback;
243
244 // Callback function
245 if( typeof action === 'function' ) {
246 action.apply( null, [ event ] );
247 }
248 // String shortcuts to reveal.js API
249 else if( typeof action === 'string' && typeof this.Reveal[ action ] === 'function' ) {
250 this.Reveal[ action ].call();
251 }
252
253 triggered = true;
254 }
255 }
256 }
257
258 // 3. System defined key bindings
259 if( triggered === false ) {
260
261 // Assume true and try to prove false
262 triggered = true;
263
264 // P, PAGE UP
265 if( keyCode === 80 || keyCode === 33 ) {
Christophe Dervieux8afae132021-12-06 15:16:42 +0100266 this.Reveal.prev({skipFragments: event.altKey});
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200267 }
268 // N, PAGE DOWN
269 else if( keyCode === 78 || keyCode === 34 ) {
Christophe Dervieux8afae132021-12-06 15:16:42 +0100270 this.Reveal.next({skipFragments: event.altKey});
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200271 }
272 // H, LEFT
273 else if( keyCode === 72 || keyCode === 37 ) {
Christophe Dervieux8afae132021-12-06 15:16:42 +0100274 if( event.shiftKey ) {
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200275 this.Reveal.slide( 0 );
276 }
277 else if( !this.Reveal.overview.isActive() && useLinearMode ) {
Marc Kupietz9c036a42024-05-14 13:17:25 +0200278 if( config.rtl ) {
279 this.Reveal.next({skipFragments: event.altKey});
280 }
281 else {
282 this.Reveal.prev({skipFragments: event.altKey});
283 }
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200284 }
285 else {
Christophe Dervieux8afae132021-12-06 15:16:42 +0100286 this.Reveal.left({skipFragments: event.altKey});
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200287 }
288 }
289 // L, RIGHT
290 else if( keyCode === 76 || keyCode === 39 ) {
Christophe Dervieux8afae132021-12-06 15:16:42 +0100291 if( event.shiftKey ) {
292 this.Reveal.slide( this.Reveal.getHorizontalSlides().length - 1 );
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200293 }
294 else if( !this.Reveal.overview.isActive() && useLinearMode ) {
Marc Kupietz9c036a42024-05-14 13:17:25 +0200295 if( config.rtl ) {
296 this.Reveal.prev({skipFragments: event.altKey});
297 }
298 else {
299 this.Reveal.next({skipFragments: event.altKey});
300 }
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200301 }
302 else {
Christophe Dervieux8afae132021-12-06 15:16:42 +0100303 this.Reveal.right({skipFragments: event.altKey});
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200304 }
305 }
306 // K, UP
307 else if( keyCode === 75 || keyCode === 38 ) {
Christophe Dervieux8afae132021-12-06 15:16:42 +0100308 if( event.shiftKey ) {
309 this.Reveal.slide( undefined, 0 );
310 }
311 else if( !this.Reveal.overview.isActive() && useLinearMode ) {
312 this.Reveal.prev({skipFragments: event.altKey});
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200313 }
314 else {
Christophe Dervieux8afae132021-12-06 15:16:42 +0100315 this.Reveal.up({skipFragments: event.altKey});
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200316 }
317 }
318 // J, DOWN
319 else if( keyCode === 74 || keyCode === 40 ) {
Christophe Dervieux8afae132021-12-06 15:16:42 +0100320 if( event.shiftKey ) {
321 this.Reveal.slide( undefined, Number.MAX_VALUE );
322 }
323 else if( !this.Reveal.overview.isActive() && useLinearMode ) {
324 this.Reveal.next({skipFragments: event.altKey});
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200325 }
326 else {
Christophe Dervieux8afae132021-12-06 15:16:42 +0100327 this.Reveal.down({skipFragments: event.altKey});
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200328 }
329 }
330 // HOME
331 else if( keyCode === 36 ) {
332 this.Reveal.slide( 0 );
333 }
334 // END
335 else if( keyCode === 35 ) {
Christophe Dervieux8afae132021-12-06 15:16:42 +0100336 this.Reveal.slide( this.Reveal.getHorizontalSlides().length - 1 );
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200337 }
338 // SPACE
339 else if( keyCode === 32 ) {
340 if( this.Reveal.overview.isActive() ) {
341 this.Reveal.overview.deactivate();
342 }
343 if( event.shiftKey ) {
Christophe Dervieux8afae132021-12-06 15:16:42 +0100344 this.Reveal.prev({skipFragments: event.altKey});
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200345 }
346 else {
Christophe Dervieux8afae132021-12-06 15:16:42 +0100347 this.Reveal.next({skipFragments: event.altKey});
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200348 }
349 }
350 // TWO-SPOT, SEMICOLON, B, V, PERIOD, LOGITECH PRESENTER TOOLS "BLACK SCREEN" BUTTON
Marc Kupietz9c036a42024-05-14 13:17:25 +0200351 else if( [58, 59, 66, 86, 190].includes( keyCode ) || ( keyCode === 191 && !event.shiftKey ) ) {
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200352 this.Reveal.togglePause();
353 }
354 // F
355 else if( keyCode === 70 ) {
356 enterFullscreen( config.embedded ? this.Reveal.getViewportElement() : document.documentElement );
357 }
358 // A
359 else if( keyCode === 65 ) {
Marc Kupietz9c036a42024-05-14 13:17:25 +0200360 if( config.autoSlideStoppable ) {
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200361 this.Reveal.toggleAutoSlide( autoSlideWasPaused );
362 }
363 }
Marc Kupietz09b75752023-10-07 09:32:19 +0200364 // G
365 else if( keyCode === 71 ) {
Marc Kupietz9c036a42024-05-14 13:17:25 +0200366 if( config.jumpToSlide ) {
Marc Kupietz09b75752023-10-07 09:32:19 +0200367 this.Reveal.toggleJumpToSlide();
368 }
369 }
Marc Kupietzcf6e9982026-08-15 15:37:40 +0200370 // C
371 else if( keyCode === 67 && this.Reveal.isOverlayOpen() ) {
372 this.Reveal.closeOverlay();
373 }
Marc Kupietz9c036a42024-05-14 13:17:25 +0200374 // ?
375 else if( ( keyCode === 63 || keyCode === 191 ) && event.shiftKey ) {
376 this.Reveal.toggleHelp();
377 }
378 // F1
379 else if( keyCode === 112 ) {
380 this.Reveal.toggleHelp();
381 }
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200382 else {
383 triggered = false;
384 }
385
386 }
387
388 // If the input resulted in a triggered action we should prevent
389 // the browsers default behavior
390 if( triggered ) {
391 event.preventDefault && event.preventDefault();
392 }
393 // ESC or O key
394 else if( keyCode === 27 || keyCode === 79 ) {
395 if( this.Reveal.closeOverlay() === false ) {
396 this.Reveal.overview.toggle();
397 }
398
399 event.preventDefault && event.preventDefault();
400 }
401
402 // If auto-sliding is enabled we need to cue up
403 // another timeout
404 this.Reveal.cueAutoSlide();
405
406 }
407
Marc Kupietz9c036a42024-05-14 13:17:25 +0200408}