blob: 4e013a3d330468f36df1aa9def87aa5e8d974187 [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';
46 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 );
56 document.addEventListener( 'keypress', this.onDocumentKeyPress, false );
57
58 }
59
60 /**
61 * Stops listening for keyboard events.
62 */
63 unbind() {
64
65 document.removeEventListener( 'keydown', this.onDocumentKeyDown, false );
66 document.removeEventListener( 'keypress', this.onDocumentKeyPress, false );
67
68 }
69
70 /**
71 * Add a custom key binding with optional description to
72 * be added to the help screen.
73 */
74 addKeyBinding( binding, callback ) {
75
76 if( typeof binding === 'object' && binding.keyCode ) {
77 this.bindings[binding.keyCode] = {
78 callback: callback,
79 key: binding.key,
80 description: binding.description
81 };
82 }
83 else {
84 this.bindings[binding] = {
85 callback: callback,
86 key: null,
87 description: null
88 };
89 }
90
91 }
92
93 /**
94 * Removes the specified custom key binding.
95 */
96 removeKeyBinding( keyCode ) {
97
98 delete this.bindings[keyCode];
99
100 }
101
102 /**
103 * Programmatically triggers a keyboard event
104 *
105 * @param {int} keyCode
106 */
107 triggerKey( keyCode ) {
108
109 this.onDocumentKeyDown( { keyCode } );
110
111 }
112
113 /**
114 * Registers a new shortcut to include in the help overlay
115 *
116 * @param {String} key
117 * @param {String} value
118 */
119 registerKeyboardShortcut( key, value ) {
120
121 this.shortcuts[key] = value;
122
123 }
124
125 getShortcuts() {
126
127 return this.shortcuts;
128
129 }
130
131 getBindings() {
132
133 return this.bindings;
134
135 }
136
137 /**
138 * Handler for the document level 'keypress' event.
139 *
140 * @param {object} event
141 */
142 onDocumentKeyPress( event ) {
143
144 // Check if the pressed key is question mark
145 if( event.shiftKey && event.charCode === 63 ) {
146 this.Reveal.toggleHelp();
147 }
148
149 }
150
151 /**
152 * Handler for the document level 'keydown' event.
153 *
154 * @param {object} event
155 */
156 onDocumentKeyDown( event ) {
157
158 let config = this.Reveal.getConfig();
159
160 // If there's a condition specified and it returns false,
161 // ignore this event
162 if( typeof config.keyboardCondition === 'function' && config.keyboardCondition(event) === false ) {
163 return true;
164 }
165
166 // If keyboardCondition is set, only capture keyboard events
167 // for embedded decks when they are focused
168 if( config.keyboardCondition === 'focused' && !this.Reveal.isFocused() ) {
169 return true;
170 }
171
172 // Shorthand
173 let keyCode = event.keyCode;
174
175 // Remember if auto-sliding was paused so we can toggle it
176 let autoSlideWasPaused = !this.Reveal.isAutoSliding();
177
178 this.Reveal.onUserInput( event );
179
180 // Is there a focused element that could be using the keyboard?
181 let activeElementIsCE = document.activeElement && document.activeElement.isContentEditable === true;
182 let activeElementIsInput = document.activeElement && document.activeElement.tagName && /input|textarea/i.test( document.activeElement.tagName );
183 let activeElementIsNotes = document.activeElement && document.activeElement.className && /speaker-notes/i.test( document.activeElement.className);
184
Christophe Dervieux8afae132021-12-06 15:16:42 +0100185 // Whitelist certain modifiers for slide navigation shortcuts
186 let isNavigationKey = [32, 37, 38, 39, 40, 78, 80].indexOf( event.keyCode ) !== -1;
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200187
188 // Prevent all other events when a modifier is pressed
Christophe Dervieux8afae132021-12-06 15:16:42 +0100189 let unusedModifier = !( isNavigationKey && event.shiftKey || event.altKey ) &&
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200190 ( event.shiftKey || event.altKey || event.ctrlKey || event.metaKey );
191
192 // Disregard the event if there's a focused element or a
193 // keyboard modifier key is present
194 if( activeElementIsCE || activeElementIsInput || activeElementIsNotes || unusedModifier ) return;
195
196 // While paused only allow resume keyboard events; 'b', 'v', '.'
197 let resumeKeyCodes = [66,86,190,191];
198 let key;
199
200 // Custom key bindings for togglePause should be able to resume
201 if( typeof config.keyboard === 'object' ) {
202 for( key in config.keyboard ) {
203 if( config.keyboard[key] === 'togglePause' ) {
204 resumeKeyCodes.push( parseInt( key, 10 ) );
205 }
206 }
207 }
208
209 if( this.Reveal.isPaused() && resumeKeyCodes.indexOf( keyCode ) === -1 ) {
210 return false;
211 }
212
213 // Use linear navigation if we're configured to OR if
214 // the presentation is one-dimensional
215 let useLinearMode = config.navigationMode === 'linear' || !this.Reveal.hasHorizontalSlides() || !this.Reveal.hasVerticalSlides();
216
217 let triggered = false;
218
219 // 1. User defined key bindings
220 if( typeof config.keyboard === 'object' ) {
221
222 for( key in config.keyboard ) {
223
224 // Check if this binding matches the pressed key
225 if( parseInt( key, 10 ) === keyCode ) {
226
227 let value = config.keyboard[ key ];
228
229 // Callback function
230 if( typeof value === 'function' ) {
231 value.apply( null, [ event ] );
232 }
233 // String shortcuts to reveal.js API
234 else if( typeof value === 'string' && typeof this.Reveal[ value ] === 'function' ) {
235 this.Reveal[ value ].call();
236 }
237
238 triggered = true;
239
240 }
241
242 }
243
244 }
245
246 // 2. Registered custom key bindings
247 if( triggered === false ) {
248
249 for( key in this.bindings ) {
250
251 // Check if this binding matches the pressed key
252 if( parseInt( key, 10 ) === keyCode ) {
253
254 let action = this.bindings[ key ].callback;
255
256 // Callback function
257 if( typeof action === 'function' ) {
258 action.apply( null, [ event ] );
259 }
260 // String shortcuts to reveal.js API
261 else if( typeof action === 'string' && typeof this.Reveal[ action ] === 'function' ) {
262 this.Reveal[ action ].call();
263 }
264
265 triggered = true;
266 }
267 }
268 }
269
270 // 3. System defined key bindings
271 if( triggered === false ) {
272
273 // Assume true and try to prove false
274 triggered = true;
275
276 // P, PAGE UP
277 if( keyCode === 80 || keyCode === 33 ) {
Christophe Dervieux8afae132021-12-06 15:16:42 +0100278 this.Reveal.prev({skipFragments: event.altKey});
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200279 }
280 // N, PAGE DOWN
281 else if( keyCode === 78 || keyCode === 34 ) {
Christophe Dervieux8afae132021-12-06 15:16:42 +0100282 this.Reveal.next({skipFragments: event.altKey});
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200283 }
284 // H, LEFT
285 else if( keyCode === 72 || keyCode === 37 ) {
Christophe Dervieux8afae132021-12-06 15:16:42 +0100286 if( event.shiftKey ) {
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200287 this.Reveal.slide( 0 );
288 }
289 else if( !this.Reveal.overview.isActive() && useLinearMode ) {
Christophe Dervieux8afae132021-12-06 15:16:42 +0100290 this.Reveal.prev({skipFragments: event.altKey});
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200291 }
292 else {
Christophe Dervieux8afae132021-12-06 15:16:42 +0100293 this.Reveal.left({skipFragments: event.altKey});
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200294 }
295 }
296 // L, RIGHT
297 else if( keyCode === 76 || keyCode === 39 ) {
Christophe Dervieux8afae132021-12-06 15:16:42 +0100298 if( event.shiftKey ) {
299 this.Reveal.slide( this.Reveal.getHorizontalSlides().length - 1 );
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200300 }
301 else if( !this.Reveal.overview.isActive() && useLinearMode ) {
Christophe Dervieux8afae132021-12-06 15:16:42 +0100302 this.Reveal.next({skipFragments: event.altKey});
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200303 }
304 else {
Christophe Dervieux8afae132021-12-06 15:16:42 +0100305 this.Reveal.right({skipFragments: event.altKey});
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200306 }
307 }
308 // K, UP
309 else if( keyCode === 75 || keyCode === 38 ) {
Christophe Dervieux8afae132021-12-06 15:16:42 +0100310 if( event.shiftKey ) {
311 this.Reveal.slide( undefined, 0 );
312 }
313 else if( !this.Reveal.overview.isActive() && useLinearMode ) {
314 this.Reveal.prev({skipFragments: event.altKey});
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200315 }
316 else {
Christophe Dervieux8afae132021-12-06 15:16:42 +0100317 this.Reveal.up({skipFragments: event.altKey});
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200318 }
319 }
320 // J, DOWN
321 else if( keyCode === 74 || keyCode === 40 ) {
Christophe Dervieux8afae132021-12-06 15:16:42 +0100322 if( event.shiftKey ) {
323 this.Reveal.slide( undefined, Number.MAX_VALUE );
324 }
325 else if( !this.Reveal.overview.isActive() && useLinearMode ) {
326 this.Reveal.next({skipFragments: event.altKey});
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200327 }
328 else {
Christophe Dervieux8afae132021-12-06 15:16:42 +0100329 this.Reveal.down({skipFragments: event.altKey});
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200330 }
331 }
332 // HOME
333 else if( keyCode === 36 ) {
334 this.Reveal.slide( 0 );
335 }
336 // END
337 else if( keyCode === 35 ) {
Christophe Dervieux8afae132021-12-06 15:16:42 +0100338 this.Reveal.slide( this.Reveal.getHorizontalSlides().length - 1 );
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200339 }
340 // SPACE
341 else if( keyCode === 32 ) {
342 if( this.Reveal.overview.isActive() ) {
343 this.Reveal.overview.deactivate();
344 }
345 if( event.shiftKey ) {
Christophe Dervieux8afae132021-12-06 15:16:42 +0100346 this.Reveal.prev({skipFragments: event.altKey});
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200347 }
348 else {
Christophe Dervieux8afae132021-12-06 15:16:42 +0100349 this.Reveal.next({skipFragments: event.altKey});
Christophe Dervieuxe1893ae2021-10-07 17:09:02 +0200350 }
351 }
352 // TWO-SPOT, SEMICOLON, B, V, PERIOD, LOGITECH PRESENTER TOOLS "BLACK SCREEN" BUTTON
353 else if( keyCode === 58 || keyCode === 59 || keyCode === 66 || keyCode === 86 || keyCode === 190 || keyCode === 191 ) {
354 this.Reveal.togglePause();
355 }
356 // F
357 else if( keyCode === 70 ) {
358 enterFullscreen( config.embedded ? this.Reveal.getViewportElement() : document.documentElement );
359 }
360 // A
361 else if( keyCode === 65 ) {
362 if ( config.autoSlideStoppable ) {
363 this.Reveal.toggleAutoSlide( autoSlideWasPaused );
364 }
365 }
366 else {
367 triggered = false;
368 }
369
370 }
371
372 // If the input resulted in a triggered action we should prevent
373 // the browsers default behavior
374 if( triggered ) {
375 event.preventDefault && event.preventDefault();
376 }
377 // ESC or O key
378 else if( keyCode === 27 || keyCode === 79 ) {
379 if( this.Reveal.closeOverlay() === false ) {
380 this.Reveal.overview.toggle();
381 }
382
383 event.preventDefault && event.preventDefault();
384 }
385
386 // If auto-sliding is enabled we need to cue up
387 // another timeout
388 this.Reveal.cueAutoSlide();
389
390 }
391
392}