initial commit
This commit is contained in:
5
src/AffectedEnum.ts
Normal file
5
src/AffectedEnum.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export enum AffectedEnum {
|
||||
NOT_FULL_FILLED = 0,
|
||||
FULL_FILLED = 1,
|
||||
FULL_FILLED_AND_CHANGED,
|
||||
}
|
||||
8
src/HotkeyDefinitionType.ts
Normal file
8
src/HotkeyDefinitionType.ts
Normal 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
9
src/HotkeyListener.ts
Normal 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
171
src/HotkeyManager.ts
Normal 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 }));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user