blob: e3bff7aa89ceee9aac935273ea22618551ddc483 [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 );
20 this.onDocumentKeyPress = this.onDocumentKeyPress.bind( this );
21
22 }
23
24 /**
25 * Called when the reveal.js config is updated.
26 */
27 configure( config, oldConfig ) {
28
29 if( config.navigationMode === 'linear' ) {
30 this.shortcuts['→ , ↓ , SPACE , N , L , J'] = 'Next slide';
31 this.shortcuts['← , ↑ , P , H , K'] = 'Previous slide';
32 }
33 else {
34 this.shortcuts['N , SPACE'] = 'Next slide';
Christophe Dervieux8afae132021-12-06 15:16:42 +010035 this.shortcuts['P , Shift SPACE'] = 'Previous slide';
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +020036 this.shortcuts['← , H'] = 'Navigate left';
37 this.shortcuts['→ , L'] = 'Navigate right';
38 this.shortcuts['↑ , K'] = 'Navigate up';
39 this.shortcuts['↓ , J'] = 'Navigate down';
40 }
41
Christophe Dervieux8afae132021-12-06 15:16:42 +010042 this.shortcuts['Alt + ←/&#8593/→/↓'] = 'Navigate without fragments';
43 this.shortcuts['Shift + ←/&#8593/→/↓'] = 'Jump to first/last slide';
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +020044 this.shortcuts['B , .'] = 'Pause';
45 this.shortcuts['F'] = 'Fullscreen';
Marc Kupietz09b75752023-10-07 09:32:19 +020046 this.shortcuts['G'] = 'Jump to slide';
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +020047 this.shortcuts['ESC, O'] = 'Slide overview';
48
49 }
50
51 /**
52 * Starts listening for keyboard events.
53 */
54 bind() {
55
56 document.addEventListener( 'keydown', this.onDocumentKeyDown, false );
57 document.addEventListener( 'keypress', this.onDocumentKeyPress, false );
58
59 }
60
61 /**
62 * Stops listening for keyboard events.
63 */
64 unbind() {
65
66 document.removeEventListener( 'keydown', this.onDocumentKeyDown, false );
67 document.removeEventListener( 'keypress', this.onDocumentKeyPress, false );
68
69 }
70
71 /**
72 * Add a custom key binding with optional description to
73 * be added to the help screen.
74 */
75 addKeyBinding( binding, callback ) {
76
77 if( typeof binding === 'object' && binding.keyCode ) {
78 this.bindings[binding.keyCode] = {
79 callback: callback,
80 key: binding.key,
81 description: binding.description
82 };
83 }
84 else {
85 this.bindings[binding] = {
86 callback: callback,
87 key: null,
88 description: null
89 };
90 }
91
92 }
93
94 /**
95 * Removes the specified custom key binding.
96 */
97 removeKeyBinding( keyCode ) {
98
99 delete this.bindings[keyCode];
100
101 }
102
103 /**
104 * Programmatically triggers a keyboard event
105 *
106 * @param {int} keyCode
107 */
108 triggerKey( keyCode ) {
109
110 this.onDocumentKeyDown( { keyCode } );
111
112 }
113
114 /**
115 * Registers a new shortcut to include in the help overlay
116 *
117 * @param {String} key
118 * @param {String} value
119 */
120 registerKeyboardShortcut( key, value ) {
121
122 this.shortcuts[key] = value;
123
124 }
125
126 getShortcuts() {
127
128 return this.shortcuts;
129
130 }
131
132 getBindings() {
133
134 return this.bindings;
135
136 }
137
138 /**
139 * Handler for the document level 'keypress' event.
140 *
141 * @param {object} event
142 */
143 onDocumentKeyPress( event ) {
144
145 // Check if the pressed key is question mark
146 if( event.shiftKey && event.charCode === 63 ) {
147 this.Reveal.toggleHelp();
148 }
149
150 }
151
152 /**
153 * Handler for the document level 'keydown' event.
154 *
155 * @param {object} event
156 */
157 onDocumentKeyDown( event ) {
158
159 let config = this.Reveal.getConfig();
160
161 // If there's a condition specified and it returns false,
162 // ignore this event
163 if( typeof config.keyboardCondition === 'function' && config.keyboardCondition(event) === false ) {
164 return true;
165 }
166
167 // If keyboardCondition is set, only capture keyboard events
168 // for embedded decks when they are focused
169 if( config.keyboardCondition === 'focused' && !this.Reveal.isFocused() ) {
170 return true;
171 }
172
173 // Shorthand
174 let keyCode = event.keyCode;
175
176 // Remember if auto-sliding was paused so we can toggle it
177 let autoSlideWasPaused = !this.Reveal.isAutoSliding();
178
179 this.Reveal.onUserInput( event );
180
181 // Is there a focused element that could be using the keyboard?
182 let activeElementIsCE = document.activeElement && document.activeElement.isContentEditable === true;
183 let activeElementIsInput = document.activeElement && document.activeElement.tagName && /input|textarea/i.test( document.activeElement.tagName );
184 let activeElementIsNotes = document.activeElement && document.activeElement.className && /speaker-notes/i.test( document.activeElement.className);
185
Christophe Dervieux8afae132021-12-06 15:16:42 +0100186 // Whitelist certain modifiers for slide navigation shortcuts
187 let isNavigationKey = [32, 37, 38, 39, 40, 78, 80].indexOf( event.keyCode ) !== -1;
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200188
189 // Prevent all other events when a modifier is pressed
Christophe Dervieux8afae132021-12-06 15:16:42 +0100190 let unusedModifier = !( isNavigationKey && event.shiftKey || event.altKey ) &&
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200191 ( event.shiftKey || event.altKey || event.ctrlKey || event.metaKey );
192
193 // Disregard the event if there's a focused element or a
194 // keyboard modifier key is present
195 if( activeElementIsCE || activeElementIsInput || activeElementIsNotes || unusedModifier ) return;
196
197 // While paused only allow resume keyboard events; 'b', 'v', '.'
198 let resumeKeyCodes = [66,86,190,191];
199 let key;
200
201 // Custom key bindings for togglePause should be able to resume
202 if( typeof config.keyboard === 'object' ) {
203 for( key in config.keyboard ) {
204 if( config.keyboard[key] === 'togglePause' ) {
205 resumeKeyCodes.push( parseInt( key, 10 ) );
206 }
207 }
208 }
209
210 if( this.Reveal.isPaused() && resumeKeyCodes.indexOf( keyCode ) === -1 ) {
211 return false;
212 }
213
214 // Use linear navigation if we're configured to OR if
215 // the presentation is one-dimensional
216 let useLinearMode = config.navigationMode === 'linear' || !this.Reveal.hasHorizontalSlides() || !this.Reveal.hasVerticalSlides();
217
218 let triggered = false;
219
220 // 1. User defined key bindings
221 if( typeof config.keyboard === 'object' ) {
222
223 for( key in config.keyboard ) {
224
225 // Check if this binding matches the pressed key
226 if( parseInt( key, 10 ) === keyCode ) {
227
228 let value = config.keyboard[ key ];
229
230 // Callback function
231 if( typeof value === 'function' ) {
232 value.apply( null, [ event ] );
233 }
234 // String shortcuts to reveal.js API
235 else if( typeof value === 'string' && typeof this.Reveal[ value ] === 'function' ) {
236 this.Reveal[ value ].call();
237 }
238
239 triggered = true;
240
241 }
242
243 }
244
245 }
246
247 // 2. Registered custom key bindings
248 if( triggered === false ) {
249
250 for( key in this.bindings ) {
251
252 // Check if this binding matches the pressed key
253 if( parseInt( key, 10 ) === keyCode ) {
254
255 let action = this.bindings[ key ].callback;
256
257 // Callback function
258 if( typeof action === 'function' ) {
259 action.apply( null, [ event ] );
260 }
261 // String shortcuts to reveal.js API
262 else if( typeof action === 'string' && typeof this.Reveal[ action ] === 'function' ) {
263 this.Reveal[ action ].call();
264 }
265
266 triggered = true;
267 }
268 }
269 }
270
271 // 3. System defined key bindings
272 if( triggered === false ) {
273
274 // Assume true and try to prove false
275 triggered = true;
276
277 // P, PAGE UP
278 if( keyCode === 80 || keyCode === 33 ) {
Christophe Dervieux8afae132021-12-06 15:16:42 +0100279 this.Reveal.prev({skipFragments: event.altKey});
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200280 }
281 // N, PAGE DOWN
282 else if( keyCode === 78 || keyCode === 34 ) {
Christophe Dervieux8afae132021-12-06 15:16:42 +0100283 this.Reveal.next({skipFragments: event.altKey});
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200284 }
285 // H, LEFT
286 else if( keyCode === 72 || keyCode === 37 ) {
Christophe Dervieux8afae132021-12-06 15:16:42 +0100287 if( event.shiftKey ) {
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200288 this.Reveal.slide( 0 );
289 }
290 else if( !this.Reveal.overview.isActive() && useLinearMode ) {
Christophe Dervieux8afae132021-12-06 15:16:42 +0100291 this.Reveal.prev({skipFragments: event.altKey});
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200292 }
293 else {
Christophe Dervieux8afae132021-12-06 15:16:42 +0100294 this.Reveal.left({skipFragments: event.altKey});
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200295 }
296 }
297 // L, RIGHT
298 else if( keyCode === 76 || keyCode === 39 ) {
Christophe Dervieux8afae132021-12-06 15:16:42 +0100299 if( event.shiftKey ) {
300 this.Reveal.slide( this.Reveal.getHorizontalSlides().length - 1 );
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200301 }
302 else if( !this.Reveal.overview.isActive() && useLinearMode ) {
Christophe Dervieux8afae132021-12-06 15:16:42 +0100303 this.Reveal.next({skipFragments: event.altKey});
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200304 }
305 else {
Christophe Dervieux8afae132021-12-06 15:16:42 +0100306 this.Reveal.right({skipFragments: event.altKey});
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200307 }
308 }
309 // K, UP
310 else if( keyCode === 75 || keyCode === 38 ) {
Christophe Dervieux8afae132021-12-06 15:16:42 +0100311 if( event.shiftKey ) {
312 this.Reveal.slide( undefined, 0 );
313 }
314 else if( !this.Reveal.overview.isActive() && useLinearMode ) {
315 this.Reveal.prev({skipFragments: event.altKey});
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200316 }
317 else {
Christophe Dervieux8afae132021-12-06 15:16:42 +0100318 this.Reveal.up({skipFragments: event.altKey});
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200319 }
320 }
321 // J, DOWN
322 else if( keyCode === 74 || keyCode === 40 ) {
Christophe Dervieux8afae132021-12-06 15:16:42 +0100323 if( event.shiftKey ) {
324 this.Reveal.slide( undefined, Number.MAX_VALUE );
325 }
326 else if( !this.Reveal.overview.isActive() && useLinearMode ) {
327 this.Reveal.next({skipFragments: event.altKey});
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200328 }
329 else {
Christophe Dervieux8afae132021-12-06 15:16:42 +0100330 this.Reveal.down({skipFragments: event.altKey});
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200331 }
332 }
333 // HOME
334 else if( keyCode === 36 ) {
335 this.Reveal.slide( 0 );
336 }
337 // END
338 else if( keyCode === 35 ) {
Christophe Dervieux8afae132021-12-06 15:16:42 +0100339 this.Reveal.slide( this.Reveal.getHorizontalSlides().length - 1 );
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200340 }
341 // SPACE
342 else if( keyCode === 32 ) {
343 if( this.Reveal.overview.isActive() ) {
344 this.Reveal.overview.deactivate();
345 }
346 if( event.shiftKey ) {
Christophe Dervieux8afae132021-12-06 15:16:42 +0100347 this.Reveal.prev({skipFragments: event.altKey});
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200348 }
349 else {
Christophe Dervieux8afae132021-12-06 15:16:42 +0100350 this.Reveal.next({skipFragments: event.altKey});
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200351 }
352 }
353 // TWO-SPOT, SEMICOLON, B, V, PERIOD, LOGITECH PRESENTER TOOLS "BLACK SCREEN" BUTTON
354 else if( keyCode === 58 || keyCode === 59 || keyCode === 66 || keyCode === 86 || keyCode === 190 || keyCode === 191 ) {
355 this.Reveal.togglePause();
356 }
357 // F
358 else if( keyCode === 70 ) {
359 enterFullscreen( config.embedded ? this.Reveal.getViewportElement() : document.documentElement );
360 }
361 // A
362 else if( keyCode === 65 ) {
363 if ( config.autoSlideStoppable ) {
364 this.Reveal.toggleAutoSlide( autoSlideWasPaused );
365 }
366 }
Marc Kupietz09b75752023-10-07 09:32:19 +0200367 // G
368 else if( keyCode === 71 ) {
369 if ( config.jumpToSlide ) {
370 this.Reveal.toggleJumpToSlide();
371 }
372 }
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200373 else {
374 triggered = false;
375 }
376
377 }
378
379 // If the input resulted in a triggered action we should prevent
380 // the browsers default behavior
381 if( triggered ) {
382 event.preventDefault && event.preventDefault();
383 }
384 // ESC or O key
385 else if( keyCode === 27 || keyCode === 79 ) {
386 if( this.Reveal.closeOverlay() === false ) {
387 this.Reveal.overview.toggle();
388 }
389
390 event.preventDefault && event.preventDefault();
391 }
392
393 // If auto-sliding is enabled we need to cue up
394 // another timeout
395 this.Reveal.cueAutoSlide();
396
397 }
398
399}