import * as plugins from './domtools.plugins.js'; export enum Key { Backspace = 8, Tab = 9, Enter = 13, Shift = 16, Ctrl = 17, Alt = 18, PauseBreak = 19, CapsLock = 20, Escape = 27, Space = 32, PageUp = 33, PageDown = 34, End = 35, Home = 36, LeftArrow = 37, UpArrow = 38, RightArrow = 39, DownArrow = 40, Insert = 45, Delete = 46, Zero = 48, ClosedParen = Zero, One = 49, ExclamationMark = One, Two = 50, AtSign = Two, Three = 51, PoundSign = Three, Hash = PoundSign, Four = 52, DollarSign = Four, Five = 53, PercentSign = Five, Six = 54, Caret = Six, Hat = Caret, Seven = 55, Ampersand = Seven, Eight = 56, Star = Eight, Asterik = Star, Nine = 57, OpenParen = Nine, A = 65, B = 66, C = 67, D = 68, E = 69, F = 70, G = 71, H = 72, I = 73, J = 74, K = 75, L = 76, M = 77, N = 78, O = 79, P = 80, Q = 81, R = 82, S = 83, T = 84, U = 85, V = 86, W = 87, X = 88, Y = 89, Z = 90, LeftWindowKey = 91, RightWindowKey = 92, SelectKey = 93, Numpad0 = 96, Numpad1 = 97, Numpad2 = 98, Numpad3 = 99, Numpad4 = 100, Numpad5 = 101, Numpad6 = 102, Numpad7 = 103, Numpad8 = 104, Numpad9 = 105, Multiply = 106, Add = 107, Subtract = 109, DecimalPoint = 110, Divide = 111, F1 = 112, F2 = 113, F3 = 114, F4 = 115, F5 = 116, F6 = 117, F7 = 118, F8 = 119, F9 = 120, F10 = 121, F11 = 122, F12 = 123, NumLock = 144, ScrollLock = 145, SemiColon = 186, Equals = 187, Comma = 188, Dash = 189, Period = 190, UnderScore = Dash, PlusSign = Equals, ForwardSlash = 191, Tilde = 192, GraveAccent = Tilde, OpenBracket = 219, ClosedBracket = 221, Quote = 222, } export class Keyboard { private mapCombosToHandlers = new Map< string, { keys: Key[]; subjects: plugins.smartrx.rxjs.Subject[]; } >(); private pressedKeys = new Set(); private listening = false; constructor(private domNode: Element | Document) { this.startListening(); } public keyEnum = Key; public on(keys: Key[]) { const subject = new plugins.smartrx.rxjs.Subject(); this.registerKeys(keys, subject); return subject; } public triggerKeyPress(keysArg: Key[]) { const normalizedKeys = this.normalizeKeys(keysArg); for (const key of normalizedKeys) { this.pressedKeys.add(key); } this.checkMatchingKeyboardSubjects(this.createKeyboardEvent(normalizedKeys)); for (const key of normalizedKeys) { this.pressedKeys.delete(key); } } public startListening() { if (this.listening) { return; } this.domNode.addEventListener('keydown', this.handleKeyDown as EventListener); this.domNode.addEventListener('keyup', this.handleKeyUp as EventListener); this.listening = true; } public stopListening() { if (!this.listening) { return; } this.domNode.removeEventListener('keydown', this.handleKeyDown as EventListener); this.domNode.removeEventListener('keyup', this.handleKeyUp as EventListener); this.listening = false; } public clear() { this.stopListening(); for (const comboEntry of this.mapCombosToHandlers.values()) { for (const subject of comboEntry.subjects) { subject.complete(); } } this.mapCombosToHandlers.clear(); this.pressedKeys.clear(); } public dispose() { this.clear(); } private handleKeyDown = (event: KeyboardEvent) => { const resolvedKey = this.resolveKey(event); if (resolvedKey === null) { return; } this.pressedKeys.add(resolvedKey); this.checkMatchingKeyboardSubjects(event); }; private checkMatchingKeyboardSubjects(payloadArg: KeyboardEvent) { this.mapCombosToHandlers.forEach((comboEntry) => { if (this.areAllKeysPressed(comboEntry.keys)) { for (const subjectArg of comboEntry.subjects) { subjectArg.next(payloadArg); } } }); } private handleKeyUp = (event: KeyboardEvent) => { const resolvedKey = this.resolveKey(event); if (resolvedKey === null) { return; } this.pressedKeys.delete(resolvedKey); }; private areAllKeysPressed(keysArg: Key[]) { let result = true; keysArg.forEach((key) => { if (!this.pressedKeys.has(key)) { result = false; } }); return result; } private registerKeys( keysArg: Array, subjectArg: plugins.smartrx.rxjs.Subject ) { const normalizedKeys = this.normalizeKeys(keysArg); const comboKey = normalizedKeys.join('+'); const existingEntry = this.mapCombosToHandlers.get(comboKey); if (!existingEntry) { this.mapCombosToHandlers.set(comboKey, { keys: normalizedKeys, subjects: [subjectArg], }); return; } existingEntry.subjects.push(subjectArg); } private normalizeKeys(keysArg: Key[]) { return [...new Set(keysArg)].sort((leftKey, rightKey) => leftKey - rightKey); } private resolveKey(event: KeyboardEvent): Key | null { return this.resolveKeyFromCode(event.code) ?? this.resolveKeyFromKey(event.key) ?? this.resolveKeyFromKeyCode(event.keyCode); } private resolveKeyFromCode(codeArg: string): Key | null { if (!codeArg) { return null; } if (codeArg.startsWith('Key') && codeArg.length === 4) { return Key[codeArg.slice(3) as keyof typeof Key] ?? null; } if (codeArg.startsWith('Digit') && codeArg.length === 6) { return this.resolveKeyFromKey(codeArg.slice(5)); } switch (codeArg) { case 'Backspace': return Key.Backspace; case 'Tab': return Key.Tab; case 'Enter': case 'NumpadEnter': return Key.Enter; case 'ShiftLeft': case 'ShiftRight': return Key.Shift; case 'ControlLeft': case 'ControlRight': return Key.Ctrl; case 'AltLeft': case 'AltRight': return Key.Alt; case 'Pause': return Key.PauseBreak; case 'CapsLock': return Key.CapsLock; case 'Escape': return Key.Escape; case 'Space': return Key.Space; case 'PageUp': return Key.PageUp; case 'PageDown': return Key.PageDown; case 'End': return Key.End; case 'Home': return Key.Home; case 'ArrowLeft': return Key.LeftArrow; case 'ArrowUp': return Key.UpArrow; case 'ArrowRight': return Key.RightArrow; case 'ArrowDown': return Key.DownArrow; case 'Insert': return Key.Insert; case 'Delete': return Key.Delete; case 'MetaLeft': return Key.LeftWindowKey; case 'MetaRight': return Key.RightWindowKey; case 'ContextMenu': return Key.SelectKey; case 'Numpad0': return Key.Numpad0; case 'Numpad1': return Key.Numpad1; case 'Numpad2': return Key.Numpad2; case 'Numpad3': return Key.Numpad3; case 'Numpad4': return Key.Numpad4; case 'Numpad5': return Key.Numpad5; case 'Numpad6': return Key.Numpad6; case 'Numpad7': return Key.Numpad7; case 'Numpad8': return Key.Numpad8; case 'Numpad9': return Key.Numpad9; case 'NumpadMultiply': return Key.Multiply; case 'NumpadAdd': return Key.Add; case 'NumpadSubtract': return Key.Subtract; case 'NumpadDecimal': return Key.DecimalPoint; case 'NumpadDivide': return Key.Divide; case 'F1': return Key.F1; case 'F2': return Key.F2; case 'F3': return Key.F3; case 'F4': return Key.F4; case 'F5': return Key.F5; case 'F6': return Key.F6; case 'F7': return Key.F7; case 'F8': return Key.F8; case 'F9': return Key.F9; case 'F10': return Key.F10; case 'F11': return Key.F11; case 'F12': return Key.F12; case 'NumLock': return Key.NumLock; case 'ScrollLock': return Key.ScrollLock; case 'Semicolon': return Key.SemiColon; case 'Equal': return Key.Equals; case 'Comma': return Key.Comma; case 'Minus': return Key.Dash; case 'Period': return Key.Period; case 'Slash': return Key.ForwardSlash; case 'Backquote': return Key.Tilde; case 'BracketLeft': return Key.OpenBracket; case 'BracketRight': return Key.ClosedBracket; case 'Quote': return Key.Quote; default: return null; } } private resolveKeyFromKey(keyArg: string): Key | null { if (!keyArg) { return null; } if (keyArg.length === 1) { const upperKey = keyArg.toUpperCase(); if (upperKey >= 'A' && upperKey <= 'Z') { return Key[upperKey as keyof typeof Key] ?? null; } switch (keyArg) { case '0': return Key.Zero; case '1': return Key.One; case '2': return Key.Two; case '3': return Key.Three; case '4': return Key.Four; case '5': return Key.Five; case '6': return Key.Six; case '7': return Key.Seven; case '8': return Key.Eight; case '9': return Key.Nine; case ' ': return Key.Space; case ';': return Key.SemiColon; case '=': return Key.Equals; case ',': return Key.Comma; case '-': return Key.Dash; case '.': return Key.Period; case '/': return Key.ForwardSlash; case '`': return Key.Tilde; case '[': return Key.OpenBracket; case ']': return Key.ClosedBracket; case '\'': return Key.Quote; default: return null; } } switch (keyArg) { case 'Backspace': return Key.Backspace; case 'Tab': return Key.Tab; case 'Enter': return Key.Enter; case 'Shift': return Key.Shift; case 'Control': return Key.Ctrl; case 'Alt': return Key.Alt; case 'Pause': return Key.PauseBreak; case 'CapsLock': return Key.CapsLock; case 'Escape': return Key.Escape; case 'PageUp': return Key.PageUp; case 'PageDown': return Key.PageDown; case 'End': return Key.End; case 'Home': return Key.Home; case 'ArrowLeft': return Key.LeftArrow; case 'ArrowUp': return Key.UpArrow; case 'ArrowRight': return Key.RightArrow; case 'ArrowDown': return Key.DownArrow; case 'Insert': return Key.Insert; case 'Delete': return Key.Delete; case 'Meta': return Key.LeftWindowKey; case 'ContextMenu': return Key.SelectKey; case 'NumLock': return Key.NumLock; case 'ScrollLock': return Key.ScrollLock; default: return null; } } private resolveKeyFromKeyCode(keyCodeArg: number): Key | null { if (typeof keyCodeArg !== 'number') { return null; } return typeof Key[keyCodeArg as Key] !== 'undefined' ? (keyCodeArg as Key) : null; } private createKeyboardEvent(keysArg: Key[]) { const triggerKey = keysArg[keysArg.length - 1] ?? Key.Enter; const keyDescriptor = this.getKeyDescriptor(triggerKey); return new KeyboardEvent('keydown', { key: keyDescriptor.key, code: keyDescriptor.code, ctrlKey: keysArg.includes(Key.Ctrl), shiftKey: keysArg.includes(Key.Shift), altKey: keysArg.includes(Key.Alt), metaKey: keysArg.includes(Key.LeftWindowKey) || keysArg.includes(Key.RightWindowKey), }); } private getKeyDescriptor(keyArg: Key): { key: string; code: string } { switch (keyArg) { case Key.Ctrl: return { key: 'Control', code: 'ControlLeft' }; case Key.Shift: return { key: 'Shift', code: 'ShiftLeft' }; case Key.Alt: return { key: 'Alt', code: 'AltLeft' }; case Key.Enter: return { key: 'Enter', code: 'Enter' }; case Key.Space: return { key: ' ', code: 'Space' }; default: { const keyName = Key[keyArg]; if (keyName && keyName.length === 1) { if (keyName >= 'A' && keyName <= 'Z') { return { key: keyName.toLowerCase(), code: `Key${keyName}` }; } } if (keyArg >= Key.Zero && keyArg <= Key.Nine) { const digit = String(keyArg - Key.Zero); return { key: digit, code: `Digit${digit}` }; } return { key: keyName ?? 'Unidentified', code: 'Unidentified' }; } } } }