import { Children , ReactNode } from "react" ; import { isElementOfType } from "../../common/utils" ; import { Hotkey , IHotkeyProps } from "./hotkey" ; import { comboMatches , getKeyCombo , IKeyCombo , parseKeyCombo } from "./hotkeyParser" ; import { hideHotkeysDialogAfterDelay , isHotkeysDialogShowing , showHotkeysDialog } from "./hotkeysDialog" ; import { IHotkeysProps } from "./hotkeysTypes" ; const SHOW_DIALOG_KEY = "?" ; export enum HotkeyScope { LOCAL = "local" , GLOBAL = "global" } export interface IHotkeyAction { combo : IKeyCombo ; props : IHotkeyProps ; } export class HotkeysEvents { private actions = [ ] as IHotkeyAction [ ] ; public constructor ( private scope ) { } public count ( ) { return this . actions . length ; } public clear ( ) { this . actions = [ ] ; } public setHotkeys ( props ) { const actions = [ ] as IHotkeyAction [ ] ; Children . forEach ( props . children , ( child ) => { if ( isElementOfType ( child , Hotkey ) && this . isScope ( child . props ) ) { actions . push ( { combo : parseKeyCombo ( child . props . combo ) , props : child . props , } ) ; } } ) ; this . actions = actions ; } public handleKeyDown = ( e ) => { const combo = getKeyCombo ( e ) ; const isTextInput = this . isTextInput ( e ) ; if ( ! isTextInput && comboMatches ( parseKeyCombo ( SHOW_DIALOG_KEY ) , combo ) ) { if ( isHotkeysDialogShowing ( ) ) { hideHotkeysDialogAfterDelay ( ) ; } else { showHotkeysDialog ( this . actions . map ( action => action . props ) ) ; } return ; } else if ( isHotkeysDialogShowing ( ) ) { return ; } this . invokeNamedCallbackIfComboRecognized ( combo , "onKeyDown" , e ) ; } ; public handleKeyUp = ( e ) => { if ( isHotkeysDialogShowing ( ) ) { return ; } this . invokeNamedCallbackIfComboRecognized ( getKeyCombo ( e ) , "onKeyUp" , e ) ; } ; private invokeNamedCallbackIfComboRecognized ( combo , callbackName , e ) { const isTextInput = this . isTextInput ( e ) ; for ( const action of this . actions ) { const shouldIgnore = ( isTextInput && ! action . props . allowInInput ) || action . props . disabled ; if ( ! shouldIgnore && comboMatches ( action . combo , combo ) ) { if ( action . props . preventDefault ) { e . preventDefault ( ) ; } if ( action . props . stopPropagation ) { ( e as any ) . isPropagationStopped = true ; e . stopPropagation ( ) ; } action . props [ callbackName ] ?. ( e ) ; } } } private isScope ( props ) { return ( props . global ? HotkeyScope . GLOBAL : HotkeyScope . LOCAL ) === this . scope ; } private isTextInput ( 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 ; } }