import * as React from "react" ; import { comboMatches , getKeyCombo , IKeyCombo , parseKeyCombo } from "../../components/hotkeys/hotkeyParser" ; import { HotkeysContext } from "../../context" ; import { HotkeyConfig } from "./hotkeyConfig" ; export interface UseHotkeysOptions { default document ? : Document ; default showDialogKeyCombo ? : string ; } export interface UseHotkeysReturnValue { handleKeyDown : React . KeyboardEventHandler < HTMLElement > ; handleKeyUp : React . KeyboardEventHandler < HTMLElement > ; } param keys param options export function useHotkeys ( keys , options = { } ) { const { document = getDefaultDocument ( ) , showDialogKeyCombo = "?" } = options ; const localKeys = React . useMemo ( ( ) => keys . filter ( k => ! k . global ) . map ( k => ( { combo : parseKeyCombo ( k . combo ) , config : k , } ) ) , [ keys ] ) ; const globalKeys = React . useMemo ( ( ) => keys . filter ( k => k . global ) . map ( k => ( { combo : parseKeyCombo ( k . combo ) , config : k , } ) ) , [ keys ] ) ; const [ , dispatch ] = React . useContext ( HotkeysContext ) ; React . useEffect ( ( ) => { const payload = [ ... globalKeys . map ( k => k . config ) , ... localKeys . map ( k => k . config ) ] ; dispatch ( { type : "ADD_HOTKEYS" , payload } ) ; return ( ) => dispatch ( { type : "REMOVE_HOTKEYS" , payload } ) ; } , [ keys ] ) ; const invokeNamedCallbackIfComboRecognized = ( global , combo , callbackName , e ) => { const isTextInput = isTargetATextInput ( e ) ; for ( const key of global ? globalKeys : localKeys ) { const { allowInInput = false , disabled = false , preventDefault = false , stopPropagation = false , } = key . config ; const shouldIgnore = ( isTextInput && ! allowInInput ) || disabled ; if ( ! shouldIgnore && comboMatches ( key . combo , combo ) ) { if ( preventDefault ) { e . preventDefault ( ) ; } if ( stopPropagation ) { ( e as any ) . isPropagationStopped = true ; e . stopPropagation ( ) ; } key . config [ callbackName ] ?. ( e ) ; } } } ; const handleGlobalKeyDown = React . useCallback ( ( e ) => { const combo = getKeyCombo ( e ) ; const isTextInput = isTargetATextInput ( e ) ; if ( ! isTextInput && comboMatches ( parseKeyCombo ( showDialogKeyCombo ) , combo ) ) { dispatch ( { type : "OPEN_DIALOG" } ) ; } else { invokeNamedCallbackIfComboRecognized ( true , getKeyCombo ( e ) , "onKeyDown" , e ) ; } } , [ globalKeys ] ) ; const handleGlobalKeyUp = React . useCallback ( ( e ) => invokeNamedCallbackIfComboRecognized ( true , getKeyCombo ( e ) , "onKeyUp" , e ) , [ globalKeys ] ) ; const handleLocalKeyDown = React . useCallback ( ( e ) => invokeNamedCallbackIfComboRecognized ( false , getKeyCombo ( e . nativeEvent ) , "onKeyDown" , e . nativeEvent ) , [ localKeys ] ) ; const handleLocalKeyUp = React . useCallback ( ( e ) => invokeNamedCallbackIfComboRecognized ( false , getKeyCombo ( e . nativeEvent ) , "onKeyUp" , e . nativeEvent ) , [ localKeys ] ) ; React . useEffect ( ( ) => { document ! . addEventListener ( "keydown" , handleGlobalKeyDown ) ; document ! . addEventListener ( "keyup" , handleGlobalKeyUp ) ; return ( ) => { document ! . removeEventListener ( "keydown" , handleGlobalKeyDown ) ; document ! . removeEventListener ( "keyup" , handleGlobalKeyUp ) ; } ; } , [ handleGlobalKeyDown , handleGlobalKeyUp ] ) ; return { handleKeyDown : handleLocalKeyDown , handleKeyUp : handleLocalKeyUp } ; } returns function isTargetATextInput ( e ) { const elem = e . target as HTMLElement ; if ( elem == null || elem . closest == null ) { return false ; } const editable = elem . closest ( "input, textarea, [contenteditable=true]" ) ; if ( editable == null ) { return false ; } if ( editable . tagName . toLowerCase ( ) === "input" ) { const inputType = ( editable as HTMLInputElement ) . type ; if ( inputType === "checkbox" || inputType === "radio" ) { return false ; } } if ( ( editable as HTMLInputElement ) . readOnly ) { return false ; } return true ; } function getDefaultDocument ( ) { if ( typeof window === "undefined" ) { return undefined ; } return window . document ; }