import autoBind from "auto-bind" ; export type KeyboardListener = ( e : KeyboardEvent ) => void ; export type ValidResponses = string [ ] | "ALL_KEYS" | "NO_KEYS" ; export interface GetKeyboardResponseOptions { callback_function : any ; valid_responses ? : ValidResponses ; rt_method ? : "performance" | "audio" ; persist ? : boolean ; audio_context ? : AudioContext ; audio_context_start_time ? : number ; allow_held_key ? : boolean ; minimum_valid_rt ? : number ; } export class KeyboardListenerAPI { constructor ( private getRootElement , private areResponsesCaseSensitive = false , private minimumValidRt = 0 ) { autoBind ( this ) ; this . registerRootListeners ( ) ; } private listeners = new Set < KeyboardListener > ( ) ; private heldKeys = new Set < string > ( ) ; private areRootListenersRegistered = false ; /**
     * If not previously done and `this.getRootElement()` returns an element, adds the root key
     * listeners to that element.
     */ private registerRootListeners ( ) { if ( ! this . areRootListenersRegistered ) { const rootElement = this . getRootElement ( ) ; if ( rootElement ) { rootElement . addEventListener ( "keydown" , this . rootKeydownListener ) ; rootElement . addEventListener ( "keyup" , this . rootKeyupListener ) ; this . areRootListenersRegistered = true ; } } } private rootKeydownListener ( e ) { for ( const listener of Array . from ( this . listeners ) ) { listener ( e ) ; } this . heldKeys . add ( this . toLowerCaseIfInsensitive ( e . key ) ) ; } private toLowerCaseIfInsensitive ( string ) { return this . areResponsesCaseSensitive ? string : string . toLowerCase ( ) ; } private rootKeyupListener ( e ) { this . heldKeys . delete ( this . toLowerCaseIfInsensitive ( e . key ) ) ; } private isResponseValid ( validResponses , allowHeldKey , key ) { if ( ! allowHeldKey && this . heldKeys . has ( key ) ) { return false ; } if ( validResponses === "ALL_KEYS" ) { return true ; } if ( validResponses === "NO_KEYS" ) { return false ; } return validResponses . includes ( key ) ; } getKeyboardResponse ( { callback_function , valid_responses = "ALL_KEYS" , rt_method = "performance" , persist , audio_context , audio_context_start_time , allow_held_key = false , minimum_valid_rt = this . minimumValidRt , } ) { if ( rt_method !== "performance" && rt_method !== "audio" ) { console . log ( 'Invalid RT method specified in getKeyboardResponse. Defaulting to "performance" method.' ) ; rt_method = "performance" ; } const usePerformanceRt = rt_method === "performance" ; const startTime = usePerformanceRt ? performance . now ( ) : audio_context_start_time * 1000 ; this . registerRootListeners ( ) ; if ( ! this . areResponsesCaseSensitive && typeof valid_responses !== "string" ) { valid_responses = valid_responses . map ( ( r ) => r . toLowerCase ( ) ) ; } const listener = ( e ) => { const rt = Math . round ( ( rt_method == "performance" ? performance . now ( ) : audio_context . currentTime * 1000 ) - startTime ) ; if ( rt < minimum_valid_rt ) { return ; } const key = this . toLowerCaseIfInsensitive ( e . key ) ; if ( this . isResponseValid ( valid_responses , allow_held_key , key ) ) { e . preventDefault ( ) ; if ( ! persist ) { this . cancelKeyboardResponse ( listener ) ; } callback_function ( { key , rt } ) ; } } ; this . listeners . add ( listener ) ; return listener ; } cancelKeyboardResponse ( listener ) { this . listeners . delete ( listener ) ; } cancelAllKeyboardResponses ( ) { this . listeners . clear ( ) ; } compareKeys ( key1 , key2 ) { if ( ( typeof key1 !== "string" && key1 !== null ) || ( typeof key2 !== "string" && key2 !== null ) ) { console . error ( "Error in jsPsych.pluginAPI.compareKeys: arguments must be key strings or null." ) ; return undefined ; } if ( typeof key1 === "string" && typeof key2 === "string" ) { return this . areResponsesCaseSensitive ? key1 === key2 : key1 . toLowerCase ( ) === key2 . toLowerCase ( ) ; } return key1 === null && key2 === null ; } }