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 9.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. /* global APP, $, interfaceConfig */
  2. import { toggleDialog } from '../../react/features/base/dialog';
  3. import { sendEvent } from '../../react/features/analytics';
  4. import { SpeakerStats } from '../../react/features/speaker-stats';
  5. const logger = require('jitsi-meet-logger').getLogger(__filename);
  6. /**
  7. * The reference to the shortcut dialogs when opened.
  8. */
  9. let keyboardShortcutDialog = null;
  10. /**
  11. * Shows or hides the keyboard shortcuts dialog.
  12. * @param {boolean} show whether to show or hide the dialog
  13. */
  14. function showKeyboardShortcutsPanel(show) {
  15. if (show
  16. && !APP.UI.messageHandler.isDialogOpened()
  17. && keyboardShortcutDialog === null) {
  18. const msg = $('#keyboard-shortcuts').html();
  19. const buttons = { Close: true };
  20. keyboardShortcutDialog = APP.UI.messageHandler.openDialog(
  21. 'keyboardShortcuts.keyboardShortcuts', msg, true, buttons);
  22. } else if (keyboardShortcutDialog !== null) {
  23. keyboardShortcutDialog.close();
  24. keyboardShortcutDialog = null;
  25. }
  26. }
  27. /**
  28. * Map of shortcuts. When a shortcut is registered it enters the mapping.
  29. * @type {{}}
  30. */
  31. const _shortcuts = {};
  32. /**
  33. * True if the keyboard shortcuts are enabled and false if not.
  34. * @type {boolean}
  35. */
  36. let enabled = true;
  37. /**
  38. * Maps keycode to character, id of popover for given function and function.
  39. */
  40. const KeyboardShortcut = {
  41. init() {
  42. this._initGlobalShortcuts();
  43. window.onkeyup = e => {
  44. if (!enabled) {
  45. return;
  46. }
  47. const key = this._getKeyboardKey(e).toUpperCase();
  48. const num = parseInt(key, 10);
  49. if (!($(':focus').is('input[type=text]')
  50. || $(':focus').is('input[type=password]')
  51. || $(':focus').is('textarea'))) {
  52. if (_shortcuts.hasOwnProperty(key)) {
  53. _shortcuts[key].function(e);
  54. } else if (!isNaN(num) && num >= 0 && num <= 9) {
  55. APP.UI.clickOnVideo(num);
  56. }
  57. // esc while the smileys are visible hides them
  58. } else if (key === 'ESCAPE'
  59. && $('#smileysContainer').is(':visible')) {
  60. APP.UI.toggleSmileys();
  61. }
  62. };
  63. window.onkeydown = e => {
  64. if (!enabled) {
  65. return;
  66. }
  67. if (!($(':focus').is('input[type=text]')
  68. || $(':focus').is('input[type=password]')
  69. || $(':focus').is('textarea'))) {
  70. if (this._getKeyboardKey(e).toUpperCase() === ' ') {
  71. if (APP.conference.isLocalAudioMuted()) {
  72. sendEvent('shortcut.talk.released');
  73. logger.log('Talk shortcut released');
  74. APP.conference.muteAudio(false);
  75. }
  76. }
  77. }
  78. };
  79. },
  80. /**
  81. * Enables/Disables the keyboard shortcuts.
  82. * @param {boolean} value - the new value.
  83. */
  84. enable(value) {
  85. enabled = value;
  86. },
  87. /**
  88. * Registers a new shortcut.
  89. *
  90. * @param shortcutChar the shortcut character triggering the action
  91. * @param shortcutAttr the "shortcut" html element attribute mappring an
  92. * element to this shortcut and used to show the shortcut character on the
  93. * element tooltip
  94. * @param exec the function to be executed when the shortcut is pressed
  95. * @param helpDescription the description of the shortcut that would appear
  96. * in the help menu
  97. */
  98. registerShortcut(// eslint-disable-line max-params
  99. shortcutChar,
  100. shortcutAttr,
  101. exec,
  102. helpDescription) {
  103. _shortcuts[shortcutChar] = {
  104. character: shortcutChar,
  105. shortcutAttr,
  106. function: exec
  107. };
  108. if (helpDescription) {
  109. this._addShortcutToHelp(shortcutChar, helpDescription);
  110. }
  111. },
  112. /**
  113. * Unregisters a shortcut.
  114. *
  115. * @param shortcutChar unregisters the given shortcut, which means it will
  116. * no longer be usable
  117. */
  118. unregisterShortcut(shortcutChar) {
  119. _shortcuts.remove(shortcutChar);
  120. this._removeShortcutFromHelp(shortcutChar);
  121. },
  122. /**
  123. * Returns the tooltip string for the given shortcut attribute.
  124. *
  125. * @param shortcutAttr indicates the popover associated with the shortcut
  126. * @returns {string} the tooltip string to add to the given shortcut popover
  127. * or an empty string if the shortcutAttr is null, an empty string or not
  128. * found in the shortcut mapping
  129. */
  130. getShortcutTooltip(shortcutAttr) {
  131. if (typeof shortcutAttr === 'string' && shortcutAttr.length > 0) {
  132. for (const key in _shortcuts) {
  133. if (_shortcuts.hasOwnProperty(key)
  134. && _shortcuts[key].shortcutAttr
  135. && _shortcuts[key].shortcutAttr === shortcutAttr) {
  136. return ` (${_shortcuts[key].character})`;
  137. }
  138. }
  139. }
  140. return '';
  141. },
  142. /**
  143. * @param e a KeyboardEvent
  144. * @returns {string} e.key or something close if not supported
  145. */
  146. _getKeyboardKey(e) {
  147. if (typeof e.key === 'string') {
  148. return e.key;
  149. }
  150. if (e.type === 'keypress'
  151. && ((e.which >= 32 && e.which <= 126)
  152. || (e.which >= 160 && e.which <= 255))) {
  153. return String.fromCharCode(e.which);
  154. }
  155. // try to fallback (0-9A-Za-z and QWERTY keyboard)
  156. switch (e.which) {
  157. case 27:
  158. return 'Escape';
  159. case 191:
  160. return e.shiftKey ? '?' : '/';
  161. }
  162. if (e.shiftKey || e.type === 'keypress') {
  163. return String.fromCharCode(e.which);
  164. }
  165. return String.fromCharCode(e.which).toLowerCase();
  166. },
  167. /**
  168. * Adds the given shortcut to the help dialog.
  169. *
  170. * @param shortcutChar the shortcut character
  171. * @param shortcutDescriptionKey the description of the shortcut
  172. * @private
  173. */
  174. _addShortcutToHelp(shortcutChar, shortcutDescriptionKey) {
  175. const listElement = document.createElement('li');
  176. const itemClass = 'shortcuts-list__item';
  177. listElement.className = itemClass;
  178. listElement.id = shortcutChar;
  179. const spanElement = document.createElement('span');
  180. spanElement.className = 'item-action';
  181. const kbdElement = document.createElement('kbd');
  182. const classes = 'aui-label regular-key';
  183. kbdElement.className = classes;
  184. kbdElement.innerHTML = shortcutChar;
  185. spanElement.appendChild(kbdElement);
  186. const descriptionElement = document.createElement('span');
  187. const descriptionClass = 'shortcuts-list__description';
  188. descriptionElement.className = descriptionClass;
  189. descriptionElement.setAttribute('data-i18n', shortcutDescriptionKey);
  190. APP.translation.translateElement($(descriptionElement));
  191. listElement.appendChild(spanElement);
  192. listElement.appendChild(descriptionElement);
  193. const parentListElement
  194. = document.getElementById('keyboard-shortcuts-list');
  195. if (parentListElement) {
  196. parentListElement.appendChild(listElement);
  197. }
  198. },
  199. /**
  200. * Removes the list element corresponding to the given shortcut from the
  201. * help dialog
  202. * @private
  203. */
  204. _removeShortcutFromHelp(shortcutChar) {
  205. const parentListElement
  206. = document.getElementById('keyboard-shortcuts-list');
  207. const shortcutElement = document.getElementById(shortcutChar);
  208. if (shortcutElement) {
  209. parentListElement.removeChild(shortcutElement);
  210. }
  211. },
  212. /**
  213. * Initialise global shortcuts.
  214. * Global shortcuts are shortcuts for features that don't have a button or
  215. * link associated with the action. In other words they represent actions
  216. * triggered _only_ with a shortcut.
  217. */
  218. _initGlobalShortcuts() {
  219. this.registerShortcut('ESCAPE', null, () => {
  220. showKeyboardShortcutsPanel(false);
  221. });
  222. this.registerShortcut('?', null, () => {
  223. sendEvent('shortcut.shortcut.help');
  224. showKeyboardShortcutsPanel(true);
  225. }, 'keyboardShortcuts.toggleShortcuts');
  226. // register SPACE shortcut in two steps to insure visibility of help
  227. // message
  228. this.registerShortcut(' ', null, () => {
  229. sendEvent('shortcut.talk.clicked');
  230. logger.log('Talk shortcut pressed');
  231. APP.conference.muteAudio(true);
  232. });
  233. this._addShortcutToHelp('SPACE', 'keyboardShortcuts.pushToTalk');
  234. if (!interfaceConfig.filmStripOnly) {
  235. this.registerShortcut('T', null, () => {
  236. sendEvent('shortcut.speakerStats.clicked');
  237. APP.store.dispatch(toggleDialog(SpeakerStats, {
  238. conference: APP.conference
  239. }));
  240. }, 'keyboardShortcuts.showSpeakerStats');
  241. }
  242. /**
  243. * FIXME: Currently focus keys are directly implemented below in
  244. * onkeyup. They should be moved to the SmallVideo instead.
  245. */
  246. this._addShortcutToHelp('0', 'keyboardShortcuts.focusLocal');
  247. this._addShortcutToHelp('1-9', 'keyboardShortcuts.focusRemote');
  248. }
  249. };
  250. export default KeyboardShortcut;