/**
 * This modules handles low-level keyboard events and normalize them across
 * browsers.
 *
 * See https://dvcs.w3.org/hg/d4e/raw-file/tip/key-event-test.html
 * (also at https://w3c.github.io/uievents/tools/key-event-viewer.html)
 *
 *
 * - **KeyboardEvent.key** (the printable value associated with the key or a string
 * for special keys)
 *  https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values
 *  https://www.w3.org/TR/uievents-key/
 *
 * - **KeyboardEvent.code** (the physical key being pressed. On an AZERTY keyboard
 * the key labelled "A" is KeyQ)
 *  https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code/code_values
 *  https://www.w3.org/TR/uievents-code/
 *
 * Note:
 * - `charCode`, `keyCode` and `which` are deprecated
 *
 * For background, see this info regarding keybinding in VSCode:
 * - https://github.com/microsoft/vscode/tree/master/src/vs/workbench/services/keybinding
 * - https://github.com/microsoft/vscode/wiki/Keybinding-Issues
 */ import { isBrowser , isTouchCapable } from '../common/capabilities' ; import { normalizeKeyboardEvent } from './keyboard-layout' ; const PRINTABLE_KEYCODE = new Set ( [ 'Backquote' , 'Digit0' , 'Digit1' , 'Digit2' , 'Digit3' , 'Digit4' , 'Digit5' , 'Digit6' , 'Digit7' , 'Digit8' , 'Digit9' , 'Minus' , 'Equal' , 'IntlYen' , 'KeyQ' , 'KeyW' , 'KeyE' , 'KeyR' , 'KeyT' , 'KeyY' , 'KeyU' , 'KeyI' , 'KeyO' , 'KeyP' , 'BracketLeft' , 'BracketRight' , 'Backslash' , 'KeyA' , 'KeyS' , 'KeyD' , 'KeyF' , 'KeyG' , 'KeyH' , 'KeyJ' , 'KeyK' , 'KeyL' , 'Semicolon' , 'Quote' , 'IntlBackslash' , 'KeyZ' , 'KeyX' , 'KeyC' , 'KeyV' , 'KeyB' , 'KeyN' , 'KeyM' , 'Comma' , 'Period' , 'Slash' , 'IntlRo' , 'Space' , 'Numpad0' , 'Numpad1' , 'Numpad2' , 'Numpad3' , 'Numpad4' , 'Numpad5' , 'Numpad6' , 'Numpad7' , 'Numpad8' , 'Numpad9' , 'NumpadAdd' , 'NumpadComma' , 'NumpadDecimal' , 'NumpadDivide' , 'NumpadEqual' , 'NumpadHash' , 'NumpadMultiply' , 'NumpadParenLeft' , 'NumpadParenRight' , 'NumpadStar' , 'NumpadSubstract' , ] ) ; export function mightProducePrintableCharacter ( evt ) { if ( evt . ctrlKey || evt . metaKey ) { return false ; } if ( evt . key === 'Dead' ) return false ; if ( evt . code === '' ) return true ; return PRINTABLE_KEYCODE . has ( evt . code ) ; } /**
 * Create a normalized representation of a keyboard event,
 * i.e., key code and modifier keys. For example:
 * - `ctrl+Shift+alt+[KeyF]`
 *
 * Note: the key code corresponds to a physical key, e.g. 'KeyQ' is
 * the key labeled 'A' on a French keyboard
 *
 */ function keyboardEventToString ( evt ) { evt = normalizeKeyboardEvent ( evt ) ; const modifiers = [ ] ; if ( evt . ctrlKey ) modifiers . push ( 'ctrl' ) ; if ( evt . metaKey ) modifiers . push ( 'meta' ) ; if ( evt . altKey ) modifiers . push ( 'alt' ) ; if ( evt . shiftKey ) modifiers . push ( 'shift' ) ; if ( modifiers . length === 0 ) return '[' + evt . code + ']' ; modifiers . push ( '[' + evt . code + ']' ) ; return modifiers . join ( '+' ) ; } export interface KeyboardDelegate { cancelComposition : ( ) => void ; blur : ( ) => void ; focus : ( ) => void ; hasFocus : ( ) => boolean ; setValue : ( value : string ) => void ; setAriaLabel : ( value : string ) => void ; moveTo : ( x : number , y : number ) => void ; } param { HTMLElement } textarea param { Object . < string , any > } handlers param { function } handlers . keystroke param { function } handlers . typedtext export function delegateKeyboardEvents ( textarea , handlers ) { let keydownEvent = null ; let keypressEvent = null ; let compositionInProgress = false ; let focusInProgress = false ; let blurInProgress = false ; let callbackTimeoutID ; function defer ( cb ) { clearTimeout ( callbackTimeoutID ) ; callbackTimeoutID = setTimeout ( ( ) => { clearTimeout ( callbackTimeoutID ) ; cb ( ) ; } ) ; } function handleTypedText ( ) { if ( textarea . selectionStart !== textarea . selectionEnd ) return ; const text = textarea . value ; textarea . value = '' ; if ( text . length > 0 ) handlers . typedText ( text ) ; } const target = textarea ; target . addEventListener ( 'keydown' , ( event ) : void => { if ( compositionInProgress || event . key === 'Process' || event . code === 'CapsLock' || /(Control|Meta|Alt|Shift)(Left|Right)/ . test ( event . code ) ) { keydownEvent = null ; return ; } keydownEvent = event ; keypressEvent = null ; if ( ! handlers . keystroke ( keyboardEventToString ( event ) , event ) ) { keydownEvent = null ; textarea . value = '' ; } else if ( isTouchCapable ( ) ) { handlers . typedText ( event . key ) ; } } , true ) ; target . addEventListener ( 'keypress' , ( event ) => { if ( compositionInProgress ) return ; if ( ! compositionInProgress ) { if ( keydownEvent && keypressEvent ) { handlers . keystroke ( keyboardEventToString ( keydownEvent ) , keydownEvent ) ; } keypressEvent = event ; defer ( handleTypedText ) ; } } , true ) ; target . addEventListener ( 'keyup' , ( ) => { if ( compositionInProgress ) return ; if ( keydownEvent && ! keypressEvent ) { handleTypedText ( ) ; } } , true ) ; target . addEventListener ( 'paste' , ( event ) => { textarea . focus ( ) ; textarea . value = '' ; handlers . paste ( event ) ; } , true ) ; target . addEventListener ( 'cut' , ( ev ) => handlers . cut ( ev ) , true ) ; target . addEventListener ( 'copy' , ( ev ) => handlers . copy ( ev ) , true ) ; target . addEventListener ( 'blur' , ( event ) => { if ( event . relatedTarget === ( ( event . target as HTMLElement ) . getRootNode ( ) as any as ShadowRoot ) . host ) { event . preventDefault ( ) ; event . stopPropagation ( ) ; return ; } if ( blurInProgress || focusInProgress ) return ; blurInProgress = true ; keydownEvent = null ; keypressEvent = null ; if ( handlers . blur ) handlers . blur ( ) ; blurInProgress = false ; } , true ) ; target . addEventListener ( 'focus' , ( _ev ) => { if ( blurInProgress || focusInProgress ) return ; focusInProgress = true ; if ( handlers . focus ) handlers . focus ( ) ; focusInProgress = false ; } , true ) ; target . addEventListener ( 'compositionstart' , ( event ) => { compositionInProgress = true ; textarea . value = '' ; if ( handlers . compositionStart ) { handlers . compositionStart ( event . data ) ; } } , true ) ; target . addEventListener ( 'compositionupdate' , ( ev ) => { if ( ! compositionInProgress ) return ; if ( handlers . compositionUpdate ) handlers . compositionUpdate ( ev . data ) ; } , true ) ; target . addEventListener ( 'compositionend' , ( ev ) => { textarea . value = '' ; if ( ! compositionInProgress ) return ; compositionInProgress = false ; if ( handlers . compositionEnd ) handlers . compositionEnd ( ev . data ) ; } , true ) ; target . addEventListener ( 'input' , ( ev ) => { if ( compositionInProgress ) return ; if ( ev . inputType === 'insertCompositionText' ) return ; if ( ev . inputType === 'insertFromPaste' ) { ev . preventDefault ( ) ; ev . stopPropagation ( ) ; return ; } defer ( handleTypedText ) ; ev . preventDefault ( ) ; ev . stopPropagation ( ) ; } ) ; return { cancelComposition : ( ) : void => { const savedBlur = handlers . blur ; const savedFocus = handlers . focus ; handlers . blur = null ; handlers . focus = null ; textarea . blur ( ) ; textarea . focus ( ) ; handlers . blur = savedBlur ; handlers . focus = savedFocus ; } , blur : ( ) : void => { if ( typeof textarea . blur === 'function' ) { textarea . blur ( ) ; } } , focus : ( ) : void => { if ( typeof textarea . focus === 'function' ) { textarea . focus ( ) ; } } , hasFocus : ( ) : boolean => { return deepActiveElement ( ) === textarea ; } , setValue : ( value ) : void => { if ( value ) { textarea . value = value ; if ( deepActiveElement ( ) === textarea && textarea . select ) { textarea . select ( ) ; } } else { textarea . value = '' ; textarea . setAttribute ( 'aria-label' , '' ) ; } } , setAriaLabel : ( value ) : void => { textarea . setAttribute ( 'aria-label' , 'after: ' + value ) ; } , moveTo : ( x , y ) : void => { textarea . style . top = `${ y }px` ; textarea . style . left = `${ x }px` ; } , } ; } function deepActiveElement ( ) { if ( ! isBrowser ( ) ) return null ; let a = document . activeElement ; while ( a ?. shadowRoot ?. activeElement ) { a = a . shadowRoot . activeElement ; } return a ; } export function eventToChar ( evt ? ) { if ( ! evt ) return '' ; let result ; if ( evt . key === 'Unidentified' ) { if ( evt . target ) { result = ( evt . target as HTMLInputElement ) . value ; } } result = result ?? evt . key ?? evt . code ; if ( /^(Dead|Return|Enter|Tab|Escape|Delete|PageUp|PageDown|Home|End|Help|ArrowLeft|ArrowRight|ArrowUp|ArrowDown)$/ . test ( result ) ) { result = '' ; } return result ; }