initial commit

This commit is contained in:
sguenter
2023-09-29 17:45:29 +02:00
commit 40cae008d8
13 changed files with 667 additions and 0 deletions

5
src/AffectedEnum.ts Normal file
View File

@@ -0,0 +1,5 @@
export enum AffectedEnum {
NOT_FULL_FILLED = 0,
FULL_FILLED = 1,
FULL_FILLED_AND_CHANGED,
}

View File

@@ -0,0 +1,8 @@
import { URecord } from '@ainias42/js-helper';
export const specialKeys = ['meta', 'control', 'alt', 'shift', 'tab'] as const;
export type HotkeyDefinitionType = {
keys: string[];
ignoreFormElements?: boolean;
} & URecord<(typeof specialKeys)[number], boolean>;

9
src/HotkeyListener.ts Normal file
View File

@@ -0,0 +1,9 @@
import { AffectedEnum } from './AffectedEnum';
export type HotkeyListenerEvent<SubKeys extends string | symbol | number> = {
event: KeyboardEvent;
type: 'keydown' | 'keyup';
subKeys: Record<SubKeys, AffectedEnum>;
};
export type HotkeyListener<SubKeys extends string | symbol | number> = (ev: HotkeyListenerEvent<SubKeys>) => unknown;

171
src/HotkeyManager.ts Normal file
View File

@@ -0,0 +1,171 @@
import { HotkeyDefinitionType, specialKeys } from './HotkeyDefinitionType';
import { HotkeyListener } from './HotkeyListener';
import { JsonHelper, ObjectHelper } from '@ainias42/js-helper';
import { AffectedEnum } from './AffectedEnum';
type HotkeyEntry<SubKeys extends string> = {
keys: HotkeyDefinitionType[];
subKeys: { [key in SubKeys]: HotkeyDefinitionType[] };
callbacks: HotkeyListener<SubKeys>[];
};
export class HotkeyManager<HotkeyConfig extends Record<string, HotkeyEntry<string>>> {
private keyPressedMap = new Map<string, boolean>();
private hotkeys: HotkeyConfig;
private enabled = true;
constructor(hotkeys: HotkeyConfig) {
this.hotkeys = hotkeys;
this.addKeyListeners();
}
private static isFormElement(element: EventTarget | null) {
return (
element instanceof HTMLInputElement ||
element instanceof HTMLSelectElement ||
element instanceof HTMLTextAreaElement
);
}
private static isEqual(a: HotkeyDefinitionType, b: HotkeyDefinitionType) {
return JsonHelper.deepEqual(a, b);
}
addListener<HotkeyName extends keyof HotkeyConfig>(
hotkeyName: HotkeyName,
callback: HotkeyListener<keyof HotkeyConfig[HotkeyName]['subKeys']>,
) {
const { callbacks } = this.hotkeys[hotkeyName];
callbacks.push(callback);
return () => {
const index = callbacks.indexOf(callback);
if (index > -1) {
callbacks.splice(index, 1);
}
};
}
enable() {
this.enabled = true;
}
disable() {
this.enabled = false;
}
addHotKeyDefinition(hotkey: keyof HotkeyConfig, definition: HotkeyDefinitionType) {
this.hotkeys[hotkey].keys.push(definition);
}
removeHotkeyDefinition(hotkey: keyof HotkeyConfig, definition: HotkeyDefinitionType) {
this.hotkeys[hotkey].keys = this.hotkeys[hotkey].keys.filter((key) => !HotkeyManager.isEqual(key, definition));
}
setHotkeyDefinitions(hotkey: keyof HotkeyConfig, definitions: HotkeyDefinitionType[]) {
this.hotkeys[hotkey].keys = definitions;
}
addSubKeyDefinition<HotkeyName extends keyof HotkeyConfig>(
hotkey: HotkeyName,
subkey: keyof HotkeyConfig[HotkeyName]['subKeys'],
definition: HotkeyDefinitionType,
) {
this.hotkeys[hotkey].subKeys[subkey as string].push(definition);
}
removeSubKeyDefinition<HotkeyName extends keyof HotkeyConfig>(
hotkey: HotkeyName,
subkey: keyof HotkeyConfig[HotkeyName]['subKeys'],
definition: HotkeyDefinitionType,
) {
const { subKeys } = this.hotkeys[hotkey];
subKeys[subkey as string] = subKeys[subkey as string].filter((key) => !HotkeyManager.isEqual(key, definition));
}
setSubKeyDefinitions<HotkeyName extends keyof HotkeyConfig>(
hotkey: HotkeyName,
subkey: keyof HotkeyConfig[HotkeyName]['subKeys'],
definitions: HotkeyDefinitionType[],
) {
this.hotkeys[hotkey].subKeys[subkey as string] = definitions;
}
getConfig() {
return this.hotkeys;
}
private addKeyListeners() {
window.addEventListener('keydown', (e) => {
this.keyPressedMap.set(e.key.toLowerCase(), true);
this.checkHotkeys(e, 'keydown');
});
window.addEventListener('keyup', (e) => {
// Check first as afterwards the keys are not set
this.checkHotkeys(e, 'keyup');
this.keyPressedMap.set(e.key.toLowerCase(), false);
if (e.key === 'Meta') {
this.keyPressedMap.clear();
}
});
}
private checkHotkeyDefinition(keyDefinition: HotkeyDefinitionType, event: KeyboardEvent): AffectedEnum {
if (!this.enabled) {
return AffectedEnum.NOT_FULL_FILLED;
}
if (keyDefinition.ignoreFormElements !== false && HotkeyManager.isFormElement(event.target)) {
return AffectedEnum.NOT_FULL_FILLED;
}
if (keyDefinition.keys.some((key) => this.keyPressedMap.get(key) !== true)) {
return AffectedEnum.NOT_FULL_FILLED;
}
if (
specialKeys.some(
(key) => keyDefinition[key] !== undefined && this.keyPressedMap.get(key) !== keyDefinition[key],
)
) {
return AffectedEnum.NOT_FULL_FILLED;
}
// Check if key is inside keyDefinition
const changedKey = event.key.toLowerCase();
if (!keyDefinition.keys.includes(changedKey) && !(changedKey in keyDefinition)) {
return AffectedEnum.FULL_FILLED;
}
return AffectedEnum.FULL_FILLED_AND_CHANGED;
}
private getKeyReducer(event: KeyboardEvent) {
return (acc: AffectedEnum, key: HotkeyDefinitionType) => {
if (acc === AffectedEnum.FULL_FILLED_AND_CHANGED) {
return acc;
}
return Math.max(acc, this.checkHotkeyDefinition(key, event));
};
}
private checkHotkeys(event: KeyboardEvent, type: 'keydown' | 'keyup') {
ObjectHelper.values(this.hotkeys).forEach((hotkey) => {
const isAffected = hotkey.keys.reduce(this.getKeyReducer(event), AffectedEnum.NOT_FULL_FILLED);
if (isAffected !== AffectedEnum.NOT_FULL_FILLED) {
const subKeys = ObjectHelper.entries(hotkey.subKeys).reduce(
(acc, [key, value]) => {
acc[key] = value.reduce(this.getKeyReducer(event), AffectedEnum.NOT_FULL_FILLED);
return acc;
},
{} as Record<string, AffectedEnum>,
);
if (
isAffected === AffectedEnum.FULL_FILLED_AND_CHANGED ||
ObjectHelper.values(subKeys).some((value) => value === AffectedEnum.FULL_FILLED_AND_CHANGED)
) {
hotkey.callbacks.forEach((callback) => callback({ event, subKeys, type }));
}
}
});
}
}