update
This commit is contained in:
parent
17c0bfcc36
commit
6a166d2d5d
@ -1,2 +1,4 @@
|
||||
/bin/
|
||||
boostrapReactMobile.ts
|
||||
|
||||
#idea-ide
|
||||
/.idea/*
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
# Exit when a command fails
|
||||
set -e
|
||||
|
||||
REPOSITORY=git@github.com:Ainias/js-helper.git
|
||||
REPOSITORY=ainias@git.silas.link:Ainias/hotkeys.git
|
||||
|
||||
if [[ -z "$1" ]]; then
|
||||
echo "versioname not given!"
|
||||
|
||||
@ -1,5 +0,0 @@
|
||||
export enum AffectedEnum {
|
||||
NOT_FULL_FILLED = 0,
|
||||
FULL_FILLED = 1,
|
||||
FULL_FILLED_AND_CHANGED,
|
||||
}
|
||||
5
src/HotkeyConfigWithOptionals.ts
Normal file
5
src/HotkeyConfigWithOptionals.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { HotkeyEntry } from './HotkeyEntry';
|
||||
|
||||
export type HotkeyConfigWithOptionals<HotkeyConfig extends Record<string, HotkeyEntry<string>>> = {
|
||||
[key in keyof HotkeyConfig]: Partial<HotkeyConfig[key]> & Pick<HotkeyConfig[key], 'keys'>;
|
||||
};
|
||||
@ -4,5 +4,4 @@ export const specialKeys = ['meta', 'control', 'alt', 'shift', 'tab'] as const;
|
||||
|
||||
export type HotkeyDefinitionType = {
|
||||
keys: string[];
|
||||
ignoreFormElements?: boolean;
|
||||
} & URecord<(typeof specialKeys)[number], boolean>;
|
||||
} & Partial<URecord<(typeof specialKeys)[number], boolean>>;
|
||||
|
||||
8
src/HotkeyEntry.ts
Normal file
8
src/HotkeyEntry.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { HotkeyDefinitionType } from './HotkeyDefinitionType';
|
||||
import { HotkeyListener } from './HotkeyListener';
|
||||
|
||||
export type HotkeyEntry<SubKeys extends string> = {
|
||||
keys: HotkeyDefinitionType[];
|
||||
subKeys: { [key in SubKeys]: HotkeyDefinitionType[] };
|
||||
callbacks: HotkeyListener<SubKeys>[];
|
||||
};
|
||||
@ -1,9 +1,7 @@
|
||||
import { AffectedEnum } from './AffectedEnum';
|
||||
|
||||
export type HotkeyListenerEvent<SubKeys extends string | symbol | number> = {
|
||||
event: KeyboardEvent;
|
||||
type: 'keydown' | 'keyup';
|
||||
subKeys: Record<SubKeys, AffectedEnum>;
|
||||
subKeys: Record<SubKeys, boolean>;
|
||||
isPressed: boolean;
|
||||
};
|
||||
|
||||
export type HotkeyListener<SubKeys extends string | symbol | number> = (ev: HotkeyListenerEvent<SubKeys>) => unknown;
|
||||
|
||||
@ -1,22 +1,28 @@
|
||||
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>[];
|
||||
};
|
||||
import { HotkeyEntry } from './HotkeyEntry';
|
||||
import { SubKeyType } from './SubKeyType';
|
||||
import { HotkeyPressedMap } from './HotkeyPressedMap';
|
||||
import { HotkeyConfigWithOptionals } from './HotkeyConfigWithOptionals';
|
||||
|
||||
export class HotkeyManager<HotkeyConfig extends Record<string, HotkeyEntry<string>>> {
|
||||
private keyPressedMap = new Map<string, boolean>();
|
||||
private hotkeys: HotkeyConfig;
|
||||
private hotkeysPressedMap: HotkeyPressedMap<HotkeyConfig>;
|
||||
private enabled = true;
|
||||
private ignoreFormElements = true;
|
||||
|
||||
constructor(hotkeys: HotkeyConfig) {
|
||||
this.hotkeys = hotkeys;
|
||||
this.addKeyListeners();
|
||||
constructor(hotkeys: HotkeyConfigWithOptionals<HotkeyConfig>, ignoreFormElements?: boolean) {
|
||||
this.hotkeys = ObjectHelper.entries(hotkeys).reduce((acc, [key, value]) => {
|
||||
acc[key] = { subKeys: {}, callbacks: [], ...value } as unknown as HotkeyConfig[typeof key];
|
||||
return acc;
|
||||
}, {} as HotkeyConfig);
|
||||
this.hotkeysPressedMap = this.generateHotkeyPressedMap();
|
||||
|
||||
if (ignoreFormElements !== undefined) {
|
||||
this.ignoreFormElements = ignoreFormElements;
|
||||
}
|
||||
}
|
||||
|
||||
private static isFormElement(element: EventTarget | null) {
|
||||
@ -31,9 +37,20 @@ export class HotkeyManager<HotkeyConfig extends Record<string, HotkeyEntry<strin
|
||||
return JsonHelper.deepEqual(a, b);
|
||||
}
|
||||
|
||||
private static checkAffections(keyDefinitions: HotkeyDefinitionType[], key: string): boolean {
|
||||
return keyDefinitions.some((keyDefinition) => HotkeyManager.checkAffection(keyDefinition, key));
|
||||
}
|
||||
|
||||
private static checkAffection(keyDefinition: HotkeyDefinitionType, key: string): boolean {
|
||||
return (
|
||||
keyDefinition.keys.includes(key) ||
|
||||
(key in specialKeys && keyDefinition[key as (typeof specialKeys)[number]] !== undefined)
|
||||
);
|
||||
}
|
||||
|
||||
addListener<HotkeyName extends keyof HotkeyConfig>(
|
||||
hotkeyName: HotkeyName,
|
||||
callback: HotkeyListener<keyof HotkeyConfig[HotkeyName]['subKeys']>,
|
||||
callback: HotkeyListener<SubKeyType<HotkeyConfig, HotkeyName>>,
|
||||
) {
|
||||
const { callbacks } = this.hotkeys[hotkeyName];
|
||||
callbacks.push(callback);
|
||||
@ -67,105 +84,148 @@ export class HotkeyManager<HotkeyConfig extends Record<string, HotkeyEntry<strin
|
||||
|
||||
addSubKeyDefinition<HotkeyName extends keyof HotkeyConfig>(
|
||||
hotkey: HotkeyName,
|
||||
subkey: keyof HotkeyConfig[HotkeyName]['subKeys'],
|
||||
subKey: SubKeyType<HotkeyConfig, HotkeyName>,
|
||||
definition: HotkeyDefinitionType,
|
||||
) {
|
||||
this.hotkeys[hotkey].subKeys[subkey as string].push(definition);
|
||||
this.hotkeys[hotkey].subKeys[subKey as string].push(definition);
|
||||
}
|
||||
|
||||
removeSubKeyDefinition<HotkeyName extends keyof HotkeyConfig>(
|
||||
hotkey: HotkeyName,
|
||||
subkey: keyof HotkeyConfig[HotkeyName]['subKeys'],
|
||||
subKey: SubKeyType<HotkeyConfig, HotkeyName>,
|
||||
definition: HotkeyDefinitionType,
|
||||
) {
|
||||
const { subKeys } = this.hotkeys[hotkey];
|
||||
subKeys[subkey as string] = subKeys[subkey as string].filter((key) => !HotkeyManager.isEqual(key, definition));
|
||||
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'],
|
||||
subKey: SubKeyType<HotkeyConfig, HotkeyName>,
|
||||
definitions: HotkeyDefinitionType[],
|
||||
) {
|
||||
this.hotkeys[hotkey].subKeys[subkey as string] = definitions;
|
||||
this.hotkeys[hotkey].subKeys[subKey as string] = definitions;
|
||||
}
|
||||
|
||||
getConfig() {
|
||||
return this.hotkeys;
|
||||
}
|
||||
|
||||
private addKeyListeners() {
|
||||
addKeyListeners() {
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (this.ignoreFormElements && HotkeyManager.isFormElement(e.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.keyPressedMap.set(e.key.toLowerCase(), true);
|
||||
this.checkHotkeys(e, 'keydown');
|
||||
this.checkHotkeys(e);
|
||||
});
|
||||
window.addEventListener('keyup', (e) => {
|
||||
// Check first as afterwards the keys are not set
|
||||
this.checkHotkeys(e, 'keyup');
|
||||
this.keyPressedMap.set(e.key.toLowerCase(), false);
|
||||
// TODO check if meta key is still working correctly
|
||||
if (this.ignoreFormElements && HotkeyManager.isFormElement(e.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key === 'Meta') {
|
||||
this.keyPressedMap.clear();
|
||||
this.clearMap(e);
|
||||
} else {
|
||||
this.keyPressedMap.set(e.key.toLowerCase(), false);
|
||||
this.checkHotkeys(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private checkHotkeyDefinition(keyDefinition: HotkeyDefinitionType, event: KeyboardEvent): AffectedEnum {
|
||||
getHotkeysPressedMap() {
|
||||
return this.hotkeysPressedMap;
|
||||
}
|
||||
|
||||
isHotkeyPressed(hotkey: keyof HotkeyConfig) {
|
||||
return this.hotkeysPressedMap[hotkey].isPressed;
|
||||
}
|
||||
|
||||
isSubKeyPressed<Hotkey extends keyof HotkeyConfig>(hotkey: Hotkey, subkey: SubKeyType<HotkeyConfig, Hotkey>) {
|
||||
return this.hotkeysPressedMap[hotkey].isPressed && this.hotkeysPressedMap[hotkey].subKeys[subkey];
|
||||
}
|
||||
|
||||
private checkHotkeyDefinitions(keyDefinitions: HotkeyDefinitionType[]): boolean {
|
||||
return keyDefinitions.some((keyDefinition) => this.checkHotkeyDefinition(keyDefinition));
|
||||
}
|
||||
|
||||
private checkHotkeyDefinition(keyDefinition: HotkeyDefinitionType): boolean {
|
||||
if (!this.enabled) {
|
||||
return AffectedEnum.NOT_FULL_FILLED;
|
||||
}
|
||||
if (keyDefinition.ignoreFormElements !== false && HotkeyManager.isFormElement(event.target)) {
|
||||
return AffectedEnum.NOT_FULL_FILLED;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (keyDefinition.keys.some((key) => this.keyPressedMap.get(key) !== true)) {
|
||||
return AffectedEnum.NOT_FULL_FILLED;
|
||||
return false;
|
||||
}
|
||||
|
||||
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;
|
||||
return !specialKeys.some(
|
||||
(key) => keyDefinition[key] !== undefined && !!this.keyPressedMap.get(key) !== keyDefinition[key],
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
private generateHotkeyPressedMap() {
|
||||
return ObjectHelper.entries(this.hotkeys).reduce((acc, [key, value]) => {
|
||||
const isPressed = this.checkHotkeyDefinitions(value.keys);
|
||||
acc[key] = {
|
||||
isPressed,
|
||||
subKeys: ObjectHelper.keys(value.subKeys).reduce(
|
||||
(accInner, subKey) => {
|
||||
accInner[subKey as SubKeyType<HotkeyConfig, typeof key>] =
|
||||
isPressed && this.checkHotkeyDefinitions(value.subKeys[subKey]);
|
||||
return accInner;
|
||||
},
|
||||
{} as Record<string, AffectedEnum>,
|
||||
);
|
||||
{} as Record<SubKeyType<HotkeyConfig, typeof key>, boolean>,
|
||||
),
|
||||
};
|
||||
return acc;
|
||||
}, {} as HotkeyPressedMap<HotkeyConfig>);
|
||||
}
|
||||
|
||||
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 }));
|
||||
}
|
||||
private triggerListenerFor(key: keyof HotkeyConfig, event: KeyboardEvent) {
|
||||
const pressedEntry = this.hotkeysPressedMap[key];
|
||||
this.hotkeys[key].callbacks.forEach((callback) =>
|
||||
callback({ event, subKeys: pressedEntry.subKeys, isPressed: pressedEntry.isPressed }),
|
||||
);
|
||||
}
|
||||
|
||||
private checkHotkeys(event: KeyboardEvent) {
|
||||
const hotkeysToTrigger: (keyof HotkeyConfig)[] = [];
|
||||
|
||||
const oldMap = this.hotkeysPressedMap;
|
||||
this.hotkeysPressedMap = this.generateHotkeyPressedMap();
|
||||
const hotkeysToCheckAffection = ObjectHelper.entries(this.hotkeysPressedMap).filter(([key, value]) => {
|
||||
const oldValue = oldMap[key];
|
||||
if (
|
||||
oldValue.isPressed !== value.isPressed ||
|
||||
(value.isPressed && !JsonHelper.deepEqual(oldValue.subKeys, value.subKeys))
|
||||
) {
|
||||
hotkeysToTrigger.push(key);
|
||||
return false;
|
||||
}
|
||||
return value.isPressed;
|
||||
});
|
||||
|
||||
// Check for repeated pressed. Nothing changed, but key is pressed again
|
||||
const eventKey = event.key.toLowerCase();
|
||||
hotkeysToCheckAffection.forEach(([key]) => {
|
||||
const keyDefinition = this.hotkeys[key];
|
||||
if (HotkeyManager.checkAffections(keyDefinition.keys, eventKey)) {
|
||||
hotkeysToTrigger.push(key);
|
||||
} else {
|
||||
ObjectHelper.values(keyDefinition.subKeys).forEach((subKeyDefinitions) => {
|
||||
if (HotkeyManager.checkAffections(subKeyDefinitions, eventKey)) {
|
||||
hotkeysToTrigger.push(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
hotkeysToTrigger.forEach((key) => this.triggerListenerFor(key, event));
|
||||
}
|
||||
|
||||
private clearMap(event: KeyboardEvent) {
|
||||
this.keyPressedMap.clear();
|
||||
this.checkHotkeys(event);
|
||||
}
|
||||
}
|
||||
|
||||
11
src/HotkeyPressedMap.ts
Normal file
11
src/HotkeyPressedMap.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { HotkeyEntry } from './HotkeyEntry';
|
||||
import { SubKeyType } from './SubKeyType';
|
||||
|
||||
export type HotkeyPressedMap<HotkeyConfig extends Record<string, HotkeyEntry<string>>> = {
|
||||
[key in keyof HotkeyConfig]: {
|
||||
isPressed: boolean;
|
||||
subKeys: {
|
||||
[subKey in SubKeyType<HotkeyConfig, key>]: boolean;
|
||||
};
|
||||
};
|
||||
};
|
||||
7
src/SubKeyType.ts
Normal file
7
src/SubKeyType.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { HotkeyEntry } from './HotkeyEntry';
|
||||
import { HotkeyConfigWithOptionals } from './HotkeyConfigWithOptionals';
|
||||
|
||||
export type SubKeyType<
|
||||
HotkeyConfig extends HotkeyConfigWithOptionals<Record<string, HotkeyEntry<string>>>,
|
||||
HotkeyName extends keyof HotkeyConfig,
|
||||
> = keyof HotkeyConfig[HotkeyName]['subKeys'];
|
||||
Loading…
x
Reference in New Issue
Block a user