2023-05-23 19:50:14 +02:00
|
|
|
import { enterFullscreen } from "../utils/util.js";
|
2020-03-10 20:28:56 +01:00
|
|
|
|
|
|
|
/**
|
2020-03-10 20:40:35 +01:00
|
|
|
* Handles all reveal.js keyboard interactions.
|
2020-03-10 20:28:56 +01:00
|
|
|
*/
|
|
|
|
export default class Keyboard {
|
2023-05-23 19:50:14 +02:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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);
|
|
|
|
document.addEventListener("keypress", this.onDocumentKeyPress, false);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Stops listening for keyboard events.
|
|
|
|
*/
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
getShortcuts() {
|
|
|
|
return this.shortcuts;
|
|
|
|
}
|
|
|
|
|
|
|
|
getBindings() {
|
|
|
|
return this.bindings;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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 isNavigationKey =
|
|
|
|
[32, 37, 38, 39, 40, 78, 80].indexOf(event.keyCode) !== -1;
|
|
|
|
|
|
|
|
// Prevent all other events when a modifier is pressed
|
|
|
|
let unusedModifier =
|
|
|
|
!((isNavigationKey && 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 (
|
|
|
|
keyCode === 58 ||
|
|
|
|
keyCode === 59 ||
|
|
|
|
keyCode === 66 ||
|
|
|
|
keyCode === 86 ||
|
|
|
|
keyCode === 190 ||
|
|
|
|
keyCode === 191
|
|
|
|
) {
|
|
|
|
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 {
|
|
|
|
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();
|
|
|
|
}
|
|
|
|
}
|