const KeyInfo = () => ({ Q : false, W : false, E : false, R : false, T : false, Y : false, U : false, I : false, O : false, P : false, A : false, S : false, D : false, F : false, G : false, H : false, J : false, K : false, L : false, Z : false, X : false, C : false, V : false, B : false, N : false, M : false, Up : false, Down : false, Left : false, Right : false, ">" : false, "<" : false, "+" : false, "-" : false, Shift : false, Spacebar: false, Enter : false, }); const KeyInfoMap = (() => { let m = KeyInfo(); for (let k of Object.keys(m)) { m[k as keyof KeyInfoType] = true; } return m; })(); export type KeyInfoType = ReturnType<typeof KeyInfo>; interface QueuedKeyboardEvent { isDown: boolean; event : KeyboardEvent; } export class KeyboardState { public down = KeyInfo(); public justDown = KeyInfo(); public justUp = KeyInfo(); private _queuedEvents: QueuedKeyboardEvent[] = []; constructor() { document.addEventListener("keydown", e => this.keyDown(e), false); document.addEventListener("keyup" , e => this.keyUp(e), false); window.addEventListener("blur" , () => { this.clear(); }, false); } public clear() { this.down = KeyInfo(); this.justDown = KeyInfo(); this.justUp = KeyInfo(); this._queuedEvents = []; } private keyUp(e: KeyboardEvent): void { // Since events usually happen between two ticks, we queue them up to be // processed on the next tick. this._queuedEvents.push({ event: e, isDown: false }); } private keyDown(e: KeyboardEvent): void { this._queuedEvents.push({ event: e, isDown: true }); } private formatKeyString(str: string): string { if (str === " ") { return "Spacebar"; } if (str.length === 1) { return str.toUpperCase(); } if (str.slice(0, 5) === "Arrow") { return str.slice(5); } return str[0].toUpperCase() + str.slice(1); } private eventToKey(event: KeyboardEvent): string { // prefer event.key let str: string = event.key; str = this.formatKeyString(str); if (str) { return str; } // use keycode or which if they are supported, and convert them to key string const number = event.keyCode || event.which; switch (number) { case 13: str = "Enter"; break; case 16: str = "Shift"; break; case 37: str = "Left" ; break; case 38: str = "Up" ; break; case 39: str = "Right"; break; case 40: str = "Down" ; break; /* A-Z */ default: str = String.fromCharCode(number); } return this.formatKeyString(str); } update(): void { for (const key of Object.keys(this.justDown)) { this.justDown[key as keyof KeyInfoType] = false; this.justUp[key as keyof KeyInfoType] = false; } for (const queuedEvent of this._queuedEvents) { const key = this.eventToKey(queuedEvent.event); if (!KeyInfoMap[key as keyof KeyInfoType]) { console.log("got unrecognized keypress: [", key,"]", queuedEvent) // DEBUG } if (queuedEvent.isDown) { if (!this.down[key as keyof KeyInfoType]) { this.justDown[key as keyof KeyInfoType] = true; } this.down[key as keyof KeyInfoType] = true; } else { if (this.down[key as keyof KeyInfoType]) { this.justUp[key as keyof KeyInfoType] = true; } this.down[key as keyof KeyInfoType] = false; } } this._queuedEvents = []; } }