Files
dees-domtools/ts/domtools.classes.keyboard.ts
T

573 lines
13 KiB
TypeScript

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<KeyboardEvent>[];
}
>();
private pressedKeys = new Set<Key>();
private listening = false;
constructor(private domNode: Element | Document) {
this.startListening();
}
public keyEnum = Key;
public on(keys: Key[]) {
const subject = new plugins.smartrx.rxjs.Subject<KeyboardEvent>();
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<Key>,
subjectArg: plugins.smartrx.rxjs.Subject<KeyboardEvent>
) {
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' };
}
}
}
}