/* global APP, $, interfaceConfig */ import { toggleDialog } from '../../react/features/base/dialog'; import { ACTION_SHORTCUT_PRESSED as PRESSED, ACTION_SHORTCUT_RELEASED as RELEASED, createShortcutEvent, sendAnalytics } from '../../react/features/analytics'; import { KeyboardShortcutsDialog } from '../../react/features/keyboard-shortcuts'; import { SpeakerStats } from '../../react/features/speaker-stats'; const logger = require('jitsi-meet-logger').getLogger(__filename); /** * Map of shortcuts. When a shortcut is registered it enters the mapping. * @type {Map} */ const _shortcuts = new Map(); /** * Map of registered keyboard keys and translation keys describing the * action performed by the key. * @type {Map} */ const _shortcutsHelp = new Map(); /** * True if the keyboard shortcuts are enabled and false if not. * @type {boolean} */ let enabled = true; /** * Maps keycode to character, id of popover for given function and function. */ const KeyboardShortcut = { init() { this._initGlobalShortcuts(); window.onkeyup = e => { if (!enabled) { return; } const key = this._getKeyboardKey(e).toUpperCase(); const num = parseInt(key, 10); if (!($(':focus').is('input[type=text]') || $(':focus').is('input[type=password]') || $(':focus').is('textarea'))) { if (_shortcuts.has(key)) { _shortcuts.get(key).function(e); } else if (!isNaN(num) && num >= 0 && num <= 9) { APP.UI.clickOnVideo(num); } // esc while the smileys are visible hides them } else if (key === 'ESCAPE' && $('#smileysContainer').is(':visible')) { APP.UI.toggleSmileys(); } }; window.onkeydown = e => { if (!enabled) { return; } if (!($(':focus').is('input[type=text]') || $(':focus').is('input[type=password]') || $(':focus').is('textarea'))) { if (this._getKeyboardKey(e).toUpperCase() === ' ') { if (APP.conference.isLocalAudioMuted()) { sendAnalytics(createShortcutEvent( 'push.to.talk', PRESSED)); logger.log('Talk shortcut pressed'); APP.conference.muteAudio(false); } } } }; }, /** * Enables/Disables the keyboard shortcuts. * @param {boolean} value - the new value. */ enable(value) { enabled = value; }, /** * Opens the {@KeyboardShortcutsDialog} dialog. * * @returns {void} */ openDialog() { APP.store.dispatch(toggleDialog(KeyboardShortcutsDialog, { shortcutDescriptions: _shortcutsHelp })); }, /** * Registers a new shortcut. * * @param shortcutChar the shortcut character triggering the action * @param shortcutAttr the "shortcut" html element attribute mapping an * element to this shortcut and used to show the shortcut character on the * element tooltip * @param exec the function to be executed when the shortcut is pressed * @param helpDescription the description of the shortcut that would appear * in the help menu */ registerShortcut(// eslint-disable-line max-params shortcutChar, shortcutAttr, exec, helpDescription) { _shortcuts.set(shortcutChar, { character: shortcutChar, function: exec, shortcutAttr }); if (helpDescription) { this._addShortcutToHelp(shortcutChar, helpDescription); } }, /** * Unregisters a shortcut. * * @param shortcutChar unregisters the given shortcut, which means it will * no longer be usable */ unregisterShortcut(shortcutChar) { _shortcuts.delete(shortcutChar); _shortcutsHelp.delete(shortcutChar); }, /** * @param e a KeyboardEvent * @returns {string} e.key or something close if not supported */ _getKeyboardKey(e) { // If e.key is a string, then it is assumed it already plainly states // the key pressed. This may not be true in all cases, such as with Edge // and "?", when the browser cannot properly map a key press event to a // keyboard key. To be safe, when a key is "Unidentified" it must be // further analyzed by jitsi to a key using e.which. if (typeof e.key === 'string' && e.key !== 'Unidentified') { return e.key; } if (e.type === 'keypress' && ((e.which >= 32 && e.which <= 126) || (e.which >= 160 && e.which <= 255))) { return String.fromCharCode(e.which); } // try to fallback (0-9A-Za-z and QWERTY keyboard) switch (e.which) { case 27: return 'Escape'; case 191: return e.shiftKey ? '?' : '/'; } if (e.shiftKey || e.type === 'keypress') { return String.fromCharCode(e.which); } return String.fromCharCode(e.which).toLowerCase(); }, /** * Adds the given shortcut to the help dialog. * * @param shortcutChar the shortcut character * @param shortcutDescriptionKey the description of the shortcut * @private */ _addShortcutToHelp(shortcutChar, shortcutDescriptionKey) { _shortcutsHelp.set(shortcutChar, shortcutDescriptionKey); }, /** * Initialise global shortcuts. * Global shortcuts are shortcuts for features that don't have a button or * link associated with the action. In other words they represent actions * triggered _only_ with a shortcut. */ _initGlobalShortcuts() { this.registerShortcut('?', null, () => { sendAnalytics(createShortcutEvent('help')); this.openDialog(); }, 'keyboardShortcuts.toggleShortcuts'); // register SPACE shortcut in two steps to insure visibility of help // message this.registerShortcut(' ', null, () => { sendAnalytics(createShortcutEvent('push.to.talk', RELEASED)); logger.log('Talk shortcut released'); APP.conference.muteAudio(true); }); this._addShortcutToHelp('SPACE', 'keyboardShortcuts.pushToTalk'); if (!interfaceConfig.filmStripOnly) { this.registerShortcut('T', null, () => { sendAnalytics(createShortcutEvent('speaker.stats')); APP.store.dispatch(toggleDialog(SpeakerStats, { conference: APP.conference })); }, 'keyboardShortcuts.showSpeakerStats'); } /** * FIXME: Currently focus keys are directly implemented below in * onkeyup. They should be moved to the SmallVideo instead. */ this._addShortcutToHelp('0', 'keyboardShortcuts.focusLocal'); this._addShortcutToHelp('1-9', 'keyboardShortcuts.focusRemote'); } }; export default KeyboardShortcut;