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({} 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>>(new Map()); const lastRepeatTime = useRef>(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 ( {children} ); }; const useShortcuts = () => { return useContext(ShortcutsContext); }; export { ShortcutsProvider, useShortcuts, };