123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268 |
- /* global APP */
- import { jitsiLocalStorage } from '@jitsi/js-utils';
- import Logger from 'jitsi-meet-logger';
-
- import {
- ACTION_SHORTCUT_PRESSED as PRESSED,
- ACTION_SHORTCUT_RELEASED as RELEASED,
- createShortcutEvent,
- sendAnalytics
- } from '../../react/features/analytics';
- import { toggleDialog } from '../../react/features/base/dialog';
- import { clickOnVideo } from '../../react/features/filmstrip/actions';
- import { KeyboardShortcutsDialog }
- from '../../react/features/keyboard-shortcuts';
- import { SpeakerStats } from '../../react/features/speaker-stats';
-
- const logger = 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();
-
- /**
- * The key used to save in local storage if keyboard shortcuts are enabled.
- */
- const _enableShortcutsKey = 'enableShortcuts';
-
- /**
- * Prefer keyboard handling of these elements over global shortcuts.
- * If a button is triggered using the Spacebar it should not trigger PTT.
- * If an input element is focused and M is pressed it should not mute audio.
- */
- const _elementsBlacklist = [
- 'input',
- 'textarea',
- 'button',
- '[role=button]',
- '[role=menuitem]',
- '[role=radio]',
- '[role=tab]',
- '[role=option]',
- '[role=switch]',
- '[role=range]',
- '[role=log]'
- ];
-
- /**
- * An element selector for elements that have their own keyboard handling.
- */
- const _focusedElementsSelector = `:focus:is(${_elementsBlacklist.join(',')})`;
-
- /**
- * Maps keycode to character, id of popover for given function and function.
- */
- const KeyboardShortcut = {
-
- init() {
- this._initGlobalShortcuts();
-
- window.onkeyup = e => {
- if (!this.getEnabled()) {
- return;
- }
- const key = this._getKeyboardKey(e).toUpperCase();
- const num = parseInt(key, 10);
-
- if (!document.querySelector(_focusedElementsSelector)) {
- if (_shortcuts.has(key)) {
- _shortcuts.get(key).function(e);
- } else if (!isNaN(num) && num >= 0 && num <= 9) {
- APP.store.dispatch(clickOnVideo(num));
- }
- }
- };
-
- window.onkeydown = e => {
- if (!this.getEnabled()) {
- return;
- }
- const focusedElement = document.querySelector(_focusedElementsSelector);
-
- if (!focusedElement) {
- if (this._getKeyboardKey(e).toUpperCase() === ' ') {
- if (APP.conference.isLocalAudioMuted()) {
- sendAnalytics(createShortcutEvent(
- 'push.to.talk',
- PRESSED));
- logger.log('Talk shortcut pressed');
- APP.conference.muteAudio(false);
- }
- }
- } else if (this._getKeyboardKey(e).toUpperCase() === 'ESCAPE') {
- // Allow to remove focus from selected elements using ESC key.
- if (focusedElement && focusedElement.blur) {
- focusedElement.blur();
- }
- }
- };
- },
-
- /**
- * Enables/Disables the keyboard shortcuts.
- * @param {boolean} value - the new value.
- */
- enable(value) {
- jitsiLocalStorage.setItem(_enableShortcutsKey, value);
- },
-
- getEnabled() {
- // Should be enabled if not explicitly set to false
- // eslint-disable-next-line no-unneeded-ternary
- return jitsiLocalStorage.getItem(_enableShortcutsKey) === 'false' ? false : true;
- },
-
- /**
- * 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
- * @param altKey whether or not the alt key must be pressed.
- */
- registerShortcut(// eslint-disable-line max-params
- shortcutChar,
- shortcutAttr,
- exec,
- helpDescription,
- altKey = false) {
- _shortcuts.set(altKey ? `:${shortcutChar}` : shortcutChar, {
- character: shortcutChar,
- function: exec,
- shortcutAttr,
- altKey
- });
-
- if (helpDescription) {
- this._addShortcutToHelp(altKey ? `:${shortcutChar}` : shortcutChar, helpDescription);
- }
- },
-
- /**
- * Unregisters a shortcut.
- *
- * @param shortcutChar unregisters the given shortcut, which means it will
- * no longer be usable
- * @param altKey whether or not shortcut is combo with alt key
- */
- unregisterShortcut(shortcutChar, altKey = false) {
- _shortcuts.delete(altKey ? `:${shortcutChar}` : shortcutChar);
- _shortcutsHelp.delete(shortcutChar);
- },
-
- /**
- * @param e a KeyboardEvent
- * @returns {string} e.key or something close if not supported
- */
- _getKeyboardKey(e) {
- // If alt is pressed a different char can be returned so this takes
- // the char from the code. It also prefixes with a colon to differentiate
- // alt combo from simple keypress.
- if (e.altKey) {
- const key = e.code.replace('Key', '');
-
- return `:${key}`;
- }
-
- // 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');
-
- 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;
|