mirror of https://github.com/jitsi/jitsi-meet
ref(keyboard-shortcuts) refactor keyboard shortcuts to use redux (#13260)
* ref(keyboard-shortcuts) refactor keyboard shortcuts to use redux fix unsynced default value between config flag and local storage * code review * fix buildpull/13269/head jitsi-meet_8602
parent
a9863e65c3
commit
1402a63324
@ -1,261 +0,0 @@ |
||||
/* global APP */ |
||||
import { jitsiLocalStorage } from '@jitsi/js-utils'; |
||||
import Logger from '@jitsi/logger'; |
||||
|
||||
import { |
||||
ACTION_SHORTCUT_PRESSED as PRESSED, |
||||
ACTION_SHORTCUT_RELEASED as RELEASED, |
||||
createShortcutEvent |
||||
} from '../../react/features/analytics/AnalyticsEvents'; |
||||
import { sendAnalytics } from '../../react/features/analytics/functions'; |
||||
import { clickOnVideo } from '../../react/features/filmstrip/actions'; |
||||
import { openSettingsDialog } from '../../react/features/settings/actions'; |
||||
import { SETTINGS_TABS } from '../../react/features/settings/constants'; |
||||
|
||||
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; |
||||
}, |
||||
|
||||
getShortcutsDescriptions() { |
||||
return _shortcutsHelp; |
||||
}, |
||||
|
||||
/** |
||||
* Opens the {@SettingsDialog} dialog on the Shortcuts page. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
openDialog() { |
||||
APP.store.dispatch(openSettingsDialog(SETTINGS_TABS.SHORTCUTS, false)); |
||||
}, |
||||
|
||||
/** |
||||
* 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'); |
||||
|
||||
/** |
||||
* 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; |
@ -0,0 +1,24 @@ |
||||
/** |
||||
* The type of the action which signals that the keyboard shortcuts should be initialized. |
||||
*/ |
||||
export const INIT_KEYBOARD_SHORTCUTS = 'INIT_KEYBOARD_SHORTCUTS'; |
||||
|
||||
/** |
||||
* The type of the action which signals that a keyboard shortcut should be registered. |
||||
*/ |
||||
export const REGISTER_KEYBOARD_SHORTCUT = 'REGISTER_KEYBOARD_SHORTCUT'; |
||||
|
||||
/** |
||||
* The type of the action which signals that a keyboard shortcut should be unregistered. |
||||
*/ |
||||
export const UNREGISTER_KEYBOARD_SHORTCUT = 'UNREGISTER_KEYBOARD_SHORTCUT'; |
||||
|
||||
/** |
||||
* The type of the action which signals that a keyboard shortcut should be enabled. |
||||
*/ |
||||
export const ENABLE_KEYBOARD_SHORTCUTS = 'ENABLE_KEYBOARD_SHORTCUTS'; |
||||
|
||||
/** |
||||
* The type of the action which signals that a keyboard shortcut should be disabled. |
||||
*/ |
||||
export const DISABLE_KEYBOARD_SHORTCUTS = 'DISABLE_KEYBOARD_SHORTCUTS'; |
@ -0,0 +1,60 @@ |
||||
import { AnyAction } from 'redux'; |
||||
|
||||
import { |
||||
DISABLE_KEYBOARD_SHORTCUTS, |
||||
ENABLE_KEYBOARD_SHORTCUTS, |
||||
REGISTER_KEYBOARD_SHORTCUT, |
||||
UNREGISTER_KEYBOARD_SHORTCUT |
||||
} from './actionTypes'; |
||||
import { IKeyboardShortcut } from './types'; |
||||
|
||||
/** |
||||
* Action to register a new shortcut. |
||||
* |
||||
* @param {IKeyboardShortcut} shortcut - The shortcut to register. |
||||
* @returns {AnyAction} |
||||
*/ |
||||
export const registerShortcut = (shortcut: IKeyboardShortcut): AnyAction => { |
||||
return { |
||||
type: REGISTER_KEYBOARD_SHORTCUT, |
||||
shortcut |
||||
}; |
||||
}; |
||||
|
||||
/** |
||||
* Action to unregister a shortcut. |
||||
* |
||||
* @param {string} character - The character of the shortcut to unregister. |
||||
* @param {boolean} altKey - Whether the shortcut used altKey. |
||||
* @returns {AnyAction} |
||||
*/ |
||||
export const unregisterShortcut = (character: string, altKey = false): AnyAction => { |
||||
return { |
||||
altKey, |
||||
type: UNREGISTER_KEYBOARD_SHORTCUT, |
||||
character |
||||
}; |
||||
}; |
||||
|
||||
/** |
||||
* Action to enable keyboard shortcuts. |
||||
* |
||||
* @returns {AnyAction} |
||||
*/ |
||||
export const enableKeyboardShortcuts = (): AnyAction => { |
||||
return { |
||||
type: ENABLE_KEYBOARD_SHORTCUTS |
||||
}; |
||||
}; |
||||
|
||||
|
||||
/** |
||||
* Action to enable keyboard shortcuts. |
||||
* |
||||
* @returns {AnyAction} |
||||
*/ |
||||
export const disableKeyboardShortcuts = (): AnyAction => { |
||||
return { |
||||
type: DISABLE_KEYBOARD_SHORTCUTS |
||||
}; |
||||
}; |
@ -0,0 +1 @@ |
||||
export * from './actions.any'; |
@ -0,0 +1,119 @@ |
||||
import { batch } from 'react-redux'; |
||||
|
||||
import { ACTION_SHORTCUT_PRESSED, ACTION_SHORTCUT_RELEASED, createShortcutEvent } from '../analytics/AnalyticsEvents'; |
||||
import { sendAnalytics } from '../analytics/functions'; |
||||
import { IStore } from '../app/types'; |
||||
import { clickOnVideo } from '../filmstrip/actions.web'; |
||||
import { openSettingsDialog } from '../settings/actions.web'; |
||||
import { SETTINGS_TABS } from '../settings/constants'; |
||||
|
||||
import { registerShortcut } from './actions.any'; |
||||
import { areKeyboardShortcutsEnabled, getKeyboardShortcuts } from './functions'; |
||||
import logger from './logger'; |
||||
import { getKeyboardKey, getPriorityFocusedElement } from './utils'; |
||||
|
||||
export * from './actions.any'; |
||||
|
||||
/** |
||||
* 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. |
||||
* |
||||
* @returns {Function} |
||||
*/ |
||||
const initGlobalKeyboardShortcuts = () => |
||||
(dispatch: IStore['dispatch']) => { |
||||
batch(() => { |
||||
dispatch(registerShortcut({ |
||||
character: '?', |
||||
helpDescription: 'keyboardShortcuts.toggleShortcuts', |
||||
handler: () => { |
||||
sendAnalytics(createShortcutEvent('help')); |
||||
dispatch(openSettingsDialog(SETTINGS_TABS.SHORTCUTS, false)); |
||||
} |
||||
})); |
||||
|
||||
// register SPACE shortcut in two steps to insure visibility of help message
|
||||
dispatch(registerShortcut({ |
||||
character: ' ', |
||||
helpCharacter: 'SPACE', |
||||
helpDescription: 'keyboardShortcuts.pushToTalk', |
||||
handler: () => { |
||||
sendAnalytics(createShortcutEvent('push.to.talk', ACTION_SHORTCUT_RELEASED)); |
||||
logger.log('Talk shortcut released'); |
||||
APP.conference.muteAudio(true); |
||||
} |
||||
})); |
||||
|
||||
dispatch(registerShortcut({ |
||||
character: '0', |
||||
helpDescription: 'keyboardShortcuts.focusLocal', |
||||
handler: () => { |
||||
dispatch(clickOnVideo(0)); |
||||
} |
||||
})); |
||||
|
||||
Array(9).fill(1) |
||||
.forEach((_, index) => { |
||||
const num = index + 1; |
||||
|
||||
dispatch(registerShortcut({ |
||||
character: `${num}`, |
||||
|
||||
// only show help hint for the first shortcut
|
||||
helpCharacter: num === 1 ? '1-9' : undefined, |
||||
helpDescription: num === 1 ? 'keyboardShortcuts.focusRemote' : undefined, |
||||
handler: () => { |
||||
dispatch(clickOnVideo(num)); |
||||
} |
||||
})); |
||||
}); |
||||
}); |
||||
}; |
||||
|
||||
/** |
||||
* Initializes keyboard shortcuts. |
||||
* |
||||
* @returns {Function} |
||||
*/ |
||||
export const initKeyboardShortcuts = () => |
||||
(dispatch: IStore['dispatch'], getState: IStore['getState']) => { |
||||
dispatch(initGlobalKeyboardShortcuts()); |
||||
|
||||
window.onkeyup = (e: KeyboardEvent) => { |
||||
const state = getState(); |
||||
const enabled = areKeyboardShortcutsEnabled(state); |
||||
const shortcuts = getKeyboardShortcuts(state); |
||||
|
||||
if (!enabled || getPriorityFocusedElement()) { |
||||
return; |
||||
} |
||||
|
||||
const key = getKeyboardKey(e).toUpperCase(); |
||||
|
||||
if (shortcuts.has(key)) { |
||||
shortcuts.get(key)?.handler(e); |
||||
} |
||||
}; |
||||
|
||||
window.onkeydown = (e: KeyboardEvent) => { |
||||
const state = getState(); |
||||
const enabled = areKeyboardShortcutsEnabled(state); |
||||
|
||||
if (!enabled) { |
||||
return; |
||||
} |
||||
|
||||
const focusedElement = getPriorityFocusedElement(); |
||||
const key = getKeyboardKey(e).toUpperCase(); |
||||
|
||||
if (key === ' ' && !focusedElement) { |
||||
sendAnalytics(createShortcutEvent('push.to.talk', ACTION_SHORTCUT_PRESSED)); |
||||
logger.log('Talk shortcut pressed'); |
||||
APP.conference.muteAudio(false); |
||||
} else if (key === 'ESCAPE') { |
||||
focusedElement?.blur(); |
||||
} |
||||
}; |
||||
}; |
@ -0,0 +1,31 @@ |
||||
import { IReduxState } from '../app/types'; |
||||
|
||||
/** |
||||
* Returns whether or not the keyboard shortcuts are enabled. |
||||
* |
||||
* @param {Object} state - The redux state. |
||||
* @returns {boolean} - Whether or not the keyboard shortcuts are enabled. |
||||
*/ |
||||
export function areKeyboardShortcutsEnabled(state: IReduxState) { |
||||
return state['features/keyboard-shortcuts'].enabled; |
||||
} |
||||
|
||||
/** |
||||
* Returns the keyboard shortcuts map. |
||||
* |
||||
* @param {Object} state - The redux state. |
||||
* @returns {Map} - The keyboard shortcuts map. |
||||
*/ |
||||
export function getKeyboardShortcuts(state: IReduxState) { |
||||
return state['features/keyboard-shortcuts'].shortcuts; |
||||
} |
||||
|
||||
/** |
||||
* Returns the keyboard shortcuts help descriptions. |
||||
* |
||||
* @param {Object} state - The redux state. |
||||
* @returns {Map} - The keyboard shortcuts help descriptions. |
||||
*/ |
||||
export function getKeyboardShortcutsHelpDescriptions(state: IReduxState) { |
||||
return state['features/keyboard-shortcuts'].shortcutsHelp; |
||||
} |
@ -0,0 +1,3 @@ |
||||
import { getLogger } from '../base/logging/functions'; |
||||
|
||||
export default getLogger('features/keyboard-shortcuts'); |
@ -0,0 +1,39 @@ |
||||
import { IStore } from '../app/types'; |
||||
import { SET_CONFIG } from '../base/config/actionTypes'; |
||||
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry'; |
||||
import { CAPTURE_EVENTS } from '../remote-control/actionTypes'; |
||||
|
||||
import { disableKeyboardShortcuts, enableKeyboardShortcuts } from './actions'; |
||||
|
||||
MiddlewareRegistry.register((store: IStore) => (next: Function) => (action: any) => { |
||||
const { dispatch } = store; |
||||
|
||||
switch (action.type) { |
||||
case CAPTURE_EVENTS: |
||||
if (action.isCapturingEvents) { |
||||
dispatch(disableKeyboardShortcuts()); |
||||
} else { |
||||
dispatch(enableKeyboardShortcuts()); |
||||
} |
||||
|
||||
return next(action); |
||||
case SET_CONFIG: { |
||||
const result = next(action); |
||||
|
||||
const state = store.getState(); |
||||
const { disableShortcuts } = state['features/base/config']; |
||||
|
||||
if (disableShortcuts !== undefined) { |
||||
if (disableShortcuts) { |
||||
dispatch(disableKeyboardShortcuts()); |
||||
} else { |
||||
dispatch(enableKeyboardShortcuts()); |
||||
} |
||||
} |
||||
|
||||
return result; |
||||
} |
||||
} |
||||
|
||||
return next(action); |
||||
}); |
@ -0,0 +1,72 @@ |
||||
import PersistenceRegistry from '../base/redux/PersistenceRegistry'; |
||||
import ReducerRegistry from '../base/redux/ReducerRegistry'; |
||||
|
||||
import { |
||||
DISABLE_KEYBOARD_SHORTCUTS, |
||||
ENABLE_KEYBOARD_SHORTCUTS, |
||||
REGISTER_KEYBOARD_SHORTCUT, |
||||
UNREGISTER_KEYBOARD_SHORTCUT |
||||
} from './actionTypes'; |
||||
import { IKeyboardShortcutsState } from './types'; |
||||
|
||||
/** |
||||
* The redux subtree of this feature. |
||||
*/ |
||||
const STORE_NAME = 'features/keyboard-shortcuts'; |
||||
|
||||
const defaultState = { |
||||
enabled: true, |
||||
shortcuts: new Map(), |
||||
shortcutsHelp: new Map() |
||||
}; |
||||
|
||||
PersistenceRegistry.register(STORE_NAME, { |
||||
enabled: true |
||||
}); |
||||
|
||||
ReducerRegistry.register<IKeyboardShortcutsState>(STORE_NAME, |
||||
(state = defaultState, action): IKeyboardShortcutsState => { |
||||
switch (action.type) { |
||||
case ENABLE_KEYBOARD_SHORTCUTS: |
||||
return { |
||||
...state, |
||||
enabled: true |
||||
}; |
||||
case DISABLE_KEYBOARD_SHORTCUTS: |
||||
return { |
||||
...state, |
||||
enabled: false |
||||
}; |
||||
case REGISTER_KEYBOARD_SHORTCUT: { |
||||
const shortcutKey = action.shortcut.alt ? `:${action.shortcut.character}` : action.shortcut.character; |
||||
|
||||
return { |
||||
...state, |
||||
shortcuts: new Map(state.shortcuts) |
||||
.set(shortcutKey, action.shortcut), |
||||
shortcutsHelp: action.shortcut.helpDescription |
||||
? new Map(state.shortcutsHelp) |
||||
.set(action.shortcut.helpCharacter ?? shortcutKey, action.shortcut.helpDescription) |
||||
: state.shortcutsHelp |
||||
}; |
||||
} |
||||
case UNREGISTER_KEYBOARD_SHORTCUT: { |
||||
const shortcutKey = action.alt ? `:${action.character}` : action.character; |
||||
const shortcuts = new Map(state.shortcuts); |
||||
|
||||
shortcuts.delete(shortcutKey); |
||||
|
||||
const shortcutsHelp = new Map(state.shortcutsHelp); |
||||
|
||||
shortcutsHelp.delete(shortcutKey); |
||||
|
||||
return { |
||||
...state, |
||||
shortcuts, |
||||
shortcutsHelp |
||||
}; |
||||
} |
||||
} |
||||
|
||||
return state; |
||||
}); |
@ -0,0 +1,23 @@ |
||||
export interface IKeyboardShortcut { |
||||
|
||||
// whether or not the alt key must be pressed
|
||||
alt?: boolean; |
||||
|
||||
// the character to be pressed that triggers the action
|
||||
character: string; |
||||
|
||||
// the function to be executed when the shortcut is pressed
|
||||
handler: Function; |
||||
|
||||
// character to be displayed in the help dialog shortcuts list
|
||||
helpCharacter?: string; |
||||
|
||||
// help description of the shortcut, to be displayed in the help dialog
|
||||
helpDescription?: string; |
||||
} |
||||
|
||||
export interface IKeyboardShortcutsState { |
||||
enabled: boolean; |
||||
shortcuts: Map<string, IKeyboardShortcut>; |
||||
shortcutsHelp: Map<string, string>; |
||||
} |
@ -0,0 +1,75 @@ |
||||
/** |
||||
* 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]' |
||||
]; |
||||
|
||||
/** |
||||
* Returns the currently focused element if it is not blacklisted. |
||||
* |
||||
* @returns {HTMLElement|null} - The currently focused element. |
||||
*/ |
||||
export const getPriorityFocusedElement = (): HTMLElement | null => |
||||
document.querySelector(`:focus:is(${_elementsBlacklist.join(',')})`); |
||||
|
||||
/** |
||||
* Returns the keyboard key from a KeyboardEvent. |
||||
* |
||||
* @param {KeyboardEvent} e - The KeyboardEvent. |
||||
* @returns {string} - The keyboard key. |
||||
*/ |
||||
export const getKeyboardKey = (e: KeyboardEvent): string => { |
||||
// @ts-ignore
|
||||
const { altKey, code, key, shiftKey, type, which } = 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 (altKey) { |
||||
const replacedKey = code.replace('Key', ''); |
||||
|
||||
return `:${replacedKey}`; |
||||
} |
||||
|
||||
// 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 key === 'string' && key !== 'Unidentified') { |
||||
return key; |
||||
} |
||||
|
||||
if (type === 'keypress' |
||||
&& ((which >= 32 && which <= 126) |
||||
|| (which >= 160 && which <= 255))) { |
||||
return String.fromCharCode(which); |
||||
} |
||||
|
||||
// try to fallback (0-9A-Za-z and QWERTY keyboard)
|
||||
switch (which) { |
||||
case 27: |
||||
return 'Escape'; |
||||
case 191: |
||||
return shiftKey ? '?' : '/'; |
||||
} |
||||
|
||||
if (shiftKey || type === 'keypress') { |
||||
return String.fromCharCode(which); |
||||
} |
||||
|
||||
return String.fromCharCode(which).toLowerCase(); |
||||
}; |
Loading…
Reference in new issue