You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

keyboardshortcut.js 8.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. /* global APP */
  2. import { jitsiLocalStorage } from '@jitsi/js-utils';
  3. import Logger from 'jitsi-meet-logger';
  4. import {
  5. ACTION_SHORTCUT_PRESSED as PRESSED,
  6. ACTION_SHORTCUT_RELEASED as RELEASED,
  7. createShortcutEvent,
  8. sendAnalytics
  9. } from '../../react/features/analytics';
  10. import { toggleDialog } from '../../react/features/base/dialog';
  11. import { clickOnVideo } from '../../react/features/filmstrip/actions';
  12. import { KeyboardShortcutsDialog }
  13. from '../../react/features/keyboard-shortcuts';
  14. import { SpeakerStats } from '../../react/features/speaker-stats';
  15. const logger = Logger.getLogger(__filename);
  16. /**
  17. * Map of shortcuts. When a shortcut is registered it enters the mapping.
  18. * @type {Map}
  19. */
  20. const _shortcuts = new Map();
  21. /**
  22. * Map of registered keyboard keys and translation keys describing the
  23. * action performed by the key.
  24. * @type {Map}
  25. */
  26. const _shortcutsHelp = new Map();
  27. /**
  28. * The key used to save in local storage if keyboard shortcuts are enabled.
  29. */
  30. const _enableShortcutsKey = 'enableShortcuts';
  31. /**
  32. * Prefer keyboard handling of these elements over global shortcuts.
  33. * If a button is triggered using the Spacebar it should not trigger PTT.
  34. * If an input element is focused and M is pressed it should not mute audio.
  35. */
  36. const _elementsBlacklist = [
  37. 'input',
  38. 'textarea',
  39. 'button',
  40. '[role=button]',
  41. '[role=menuitem]',
  42. '[role=radio]',
  43. '[role=tab]',
  44. '[role=option]',
  45. '[role=switch]',
  46. '[role=range]',
  47. '[role=log]'
  48. ];
  49. /**
  50. * An element selector for elements that have their own keyboard handling.
  51. */
  52. const _focusedElementsSelector = `:focus:is(${_elementsBlacklist.join(',')})`;
  53. /**
  54. * Maps keycode to character, id of popover for given function and function.
  55. */
  56. const KeyboardShortcut = {
  57. init() {
  58. this._initGlobalShortcuts();
  59. window.onkeyup = e => {
  60. if (!this.getEnabled()) {
  61. return;
  62. }
  63. const key = this._getKeyboardKey(e).toUpperCase();
  64. const num = parseInt(key, 10);
  65. if (!document.querySelector(_focusedElementsSelector)) {
  66. if (_shortcuts.has(key)) {
  67. _shortcuts.get(key).function(e);
  68. } else if (!isNaN(num) && num >= 0 && num <= 9) {
  69. APP.store.dispatch(clickOnVideo(num));
  70. }
  71. }
  72. };
  73. window.onkeydown = e => {
  74. if (!this.getEnabled()) {
  75. return;
  76. }
  77. const focusedElement = document.querySelector(_focusedElementsSelector);
  78. if (!focusedElement) {
  79. if (this._getKeyboardKey(e).toUpperCase() === ' ') {
  80. if (APP.conference.isLocalAudioMuted()) {
  81. sendAnalytics(createShortcutEvent(
  82. 'push.to.talk',
  83. PRESSED));
  84. logger.log('Talk shortcut pressed');
  85. APP.conference.muteAudio(false);
  86. }
  87. }
  88. } else if (this._getKeyboardKey(e).toUpperCase() === 'ESCAPE') {
  89. // Allow to remove focus from selected elements using ESC key.
  90. if (focusedElement && focusedElement.blur) {
  91. focusedElement.blur();
  92. }
  93. }
  94. };
  95. },
  96. /**
  97. * Enables/Disables the keyboard shortcuts.
  98. * @param {boolean} value - the new value.
  99. */
  100. enable(value) {
  101. jitsiLocalStorage.setItem(_enableShortcutsKey, value);
  102. },
  103. getEnabled() {
  104. // Should be enabled if not explicitly set to false
  105. // eslint-disable-next-line no-unneeded-ternary
  106. return jitsiLocalStorage.getItem(_enableShortcutsKey) === 'false' ? false : true;
  107. },
  108. /**
  109. * Opens the {@KeyboardShortcutsDialog} dialog.
  110. *
  111. * @returns {void}
  112. */
  113. openDialog() {
  114. APP.store.dispatch(toggleDialog(KeyboardShortcutsDialog, {
  115. shortcutDescriptions: _shortcutsHelp
  116. }));
  117. },
  118. /**
  119. * Registers a new shortcut.
  120. *
  121. * @param shortcutChar the shortcut character triggering the action
  122. * @param shortcutAttr the "shortcut" html element attribute mapping an
  123. * element to this shortcut and used to show the shortcut character on the
  124. * element tooltip
  125. * @param exec the function to be executed when the shortcut is pressed
  126. * @param helpDescription the description of the shortcut that would appear
  127. * in the help menu
  128. * @param altKey whether or not the alt key must be pressed.
  129. */
  130. registerShortcut(// eslint-disable-line max-params
  131. shortcutChar,
  132. shortcutAttr,
  133. exec,
  134. helpDescription,
  135. altKey = false) {
  136. _shortcuts.set(altKey ? `:${shortcutChar}` : shortcutChar, {
  137. character: shortcutChar,
  138. function: exec,
  139. shortcutAttr,
  140. altKey
  141. });
  142. if (helpDescription) {
  143. this._addShortcutToHelp(altKey ? `:${shortcutChar}` : shortcutChar, helpDescription);
  144. }
  145. },
  146. /**
  147. * Unregisters a shortcut.
  148. *
  149. * @param shortcutChar unregisters the given shortcut, which means it will
  150. * no longer be usable
  151. * @param altKey whether or not shortcut is combo with alt key
  152. */
  153. unregisterShortcut(shortcutChar, altKey = false) {
  154. _shortcuts.delete(altKey ? `:${shortcutChar}` : shortcutChar);
  155. _shortcutsHelp.delete(shortcutChar);
  156. },
  157. /**
  158. * @param e a KeyboardEvent
  159. * @returns {string} e.key or something close if not supported
  160. */
  161. _getKeyboardKey(e) {
  162. // If alt is pressed a different char can be returned so this takes
  163. // the char from the code. It also prefixes with a colon to differentiate
  164. // alt combo from simple keypress.
  165. if (e.altKey) {
  166. const key = e.code.replace('Key', '');
  167. return `:${key}`;
  168. }
  169. // If e.key is a string, then it is assumed it already plainly states
  170. // the key pressed. This may not be true in all cases, such as with Edge
  171. // and "?", when the browser cannot properly map a key press event to a
  172. // keyboard key. To be safe, when a key is "Unidentified" it must be
  173. // further analyzed by jitsi to a key using e.which.
  174. if (typeof e.key === 'string' && e.key !== 'Unidentified') {
  175. return e.key;
  176. }
  177. if (e.type === 'keypress'
  178. && ((e.which >= 32 && e.which <= 126)
  179. || (e.which >= 160 && e.which <= 255))) {
  180. return String.fromCharCode(e.which);
  181. }
  182. // try to fallback (0-9A-Za-z and QWERTY keyboard)
  183. switch (e.which) {
  184. case 27:
  185. return 'Escape';
  186. case 191:
  187. return e.shiftKey ? '?' : '/';
  188. }
  189. if (e.shiftKey || e.type === 'keypress') {
  190. return String.fromCharCode(e.which);
  191. }
  192. return String.fromCharCode(e.which).toLowerCase();
  193. },
  194. /**
  195. * Adds the given shortcut to the help dialog.
  196. *
  197. * @param shortcutChar the shortcut character
  198. * @param shortcutDescriptionKey the description of the shortcut
  199. * @private
  200. */
  201. _addShortcutToHelp(shortcutChar, shortcutDescriptionKey) {
  202. _shortcutsHelp.set(shortcutChar, shortcutDescriptionKey);
  203. },
  204. /**
  205. * Initialise global shortcuts.
  206. * Global shortcuts are shortcuts for features that don't have a button or
  207. * link associated with the action. In other words they represent actions
  208. * triggered _only_ with a shortcut.
  209. */
  210. _initGlobalShortcuts() {
  211. this.registerShortcut('?', null, () => {
  212. sendAnalytics(createShortcutEvent('help'));
  213. this.openDialog();
  214. }, 'keyboardShortcuts.toggleShortcuts');
  215. // register SPACE shortcut in two steps to insure visibility of help
  216. // message
  217. this.registerShortcut(' ', null, () => {
  218. sendAnalytics(createShortcutEvent('push.to.talk', RELEASED));
  219. logger.log('Talk shortcut released');
  220. APP.conference.muteAudio(true);
  221. });
  222. this._addShortcutToHelp('SPACE', 'keyboardShortcuts.pushToTalk');
  223. this.registerShortcut('T', null, () => {
  224. sendAnalytics(createShortcutEvent('speaker.stats'));
  225. APP.store.dispatch(toggleDialog(SpeakerStats, {
  226. conference: APP.conference
  227. }));
  228. }, 'keyboardShortcuts.showSpeakerStats');
  229. /**
  230. * FIXME: Currently focus keys are directly implemented below in
  231. * onkeyup. They should be moved to the SmallVideo instead.
  232. */
  233. this._addShortcutToHelp('0', 'keyboardShortcuts.focusLocal');
  234. this._addShortcutToHelp('1-9', 'keyboardShortcuts.focusRemote');
  235. }
  236. };
  237. export default KeyboardShortcut;