stremio-web/src/common/Shortcuts/Shortcuts.tsx

89 lines
3 KiB
TypeScript

import React, { createContext, useCallback, useContext, useEffect, useRef } from 'react';
import shortcuts from './shortcuts.json';
const SHORTCUTS = shortcuts.map(({ shortcuts }) => shortcuts).flat();
export type ShortcutName = string;
export type ShortcutListener = (combo: number) => void;
interface ShortcutsContext {
grouped: ShortcutGroup[],
on: (name: ShortcutName, listener: ShortcutListener) => void,
off: (name: ShortcutName, listener: ShortcutListener) => void,
}
const ShortcutsContext = createContext<ShortcutsContext>({} as ShortcutsContext);
type Props = {
children: JSX.Element,
onShortcut: (name: ShortcutName, combo: number, key: string) => void,
};
const REPEAT_THROTTLE_MS = 130;
const isInputFocused = () => {
const inputElements = ['INPUT', 'TEXTAREA', 'SELECT'];
const activeElement = document.activeElement;
return activeElement instanceof HTMLElement &&
(inputElements.includes(activeElement.tagName) || activeElement.isContentEditable);
};
const ShortcutsProvider = ({ children, onShortcut }: Props) => {
const listeners = useRef<Map<ShortcutName, Set<ShortcutListener>>>(new Map());
const lastRepeatTime = useRef<Map<string, number>>(new Map());
const onKeyDown = useCallback(({ ctrlKey, shiftKey, altKey, metaKey, code, key, repeat }: KeyboardEvent) => {
if (isInputFocused()) return;
if (repeat) {
const now = Date.now();
const last = lastRepeatTime.current.get(code) ?? 0;
if (now - last < REPEAT_THROTTLE_MS) return;
lastRepeatTime.current.set(code, now);
}
SHORTCUTS.forEach(({ name, combos }) => combos.forEach((keys) => {
const modifers = (keys.includes('Ctrl') === ctrlKey)
&& (keys.includes('Shift') === shiftKey)
&& !altKey
&& !metaKey;
if (modifers && (keys.includes(code) || keys.includes(key.toUpperCase()))) {
const combo = combos.indexOf(keys);
listeners.current.get(name)?.forEach((listener) => listener(combo));
onShortcut(name as ShortcutName, combo, key);
}
}));
}, [onShortcut]);
const on = (name: ShortcutName, listener: ShortcutListener) => {
!listeners.current.has(name) && listeners.current.set(name, new Set());
listeners.current.get(name)!.add(listener);
};
const off = (name: ShortcutName, listener: ShortcutListener) => {
listeners.current.get(name)?.delete(listener);
};
useEffect(() => {
document.addEventListener('keydown', onKeyDown);
return () => document.removeEventListener('keydown', onKeyDown);
}, [onKeyDown]);
return (
<ShortcutsContext.Provider value={{ grouped: shortcuts, on, off }}>
{children}
</ShortcutsContext.Provider>
);
};
const useShortcuts = () => {
return useContext(ShortcutsContext);
};
export {
ShortcutsProvider,
useShortcuts,
};