update
This commit is contained in:
parent
17c0bfcc36
commit
6a166d2d5d
@ -1,2 +1,4 @@
|
|||||||
/bin/
|
/bin/
|
||||||
boostrapReactMobile.ts
|
|
||||||
|
#idea-ide
|
||||||
|
/.idea/*
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
# Exit when a command fails
|
# Exit when a command fails
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
REPOSITORY=git@github.com:Ainias/js-helper.git
|
REPOSITORY=ainias@git.silas.link:Ainias/hotkeys.git
|
||||||
|
|
||||||
if [[ -z "$1" ]]; then
|
if [[ -z "$1" ]]; then
|
||||||
echo "versioname not given!"
|
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 = {
|
export type HotkeyDefinitionType = {
|
||||||
keys: string[];
|
keys: string[];
|
||||||
ignoreFormElements?: boolean;
|
} & Partial<URecord<(typeof specialKeys)[number], boolean>>;
|
||||||
} & 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> = {
|
export type HotkeyListenerEvent<SubKeys extends string | symbol | number> = {
|
||||||
event: KeyboardEvent;
|
event: KeyboardEvent;
|
||||||
type: 'keydown' | 'keyup';
|
subKeys: Record<SubKeys, boolean>;
|
||||||
subKeys: Record<SubKeys, AffectedEnum>;
|
isPressed: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type HotkeyListener<SubKeys extends string | symbol | number> = (ev: HotkeyListenerEvent<SubKeys>) => unknown;
|
export type HotkeyListener<SubKeys extends string | symbol | number> = (ev: HotkeyListenerEvent<SubKeys>) => unknown;
|
||||||
|
|||||||
@ -1,22 +1,28 @@
|
|||||||
import { HotkeyDefinitionType, specialKeys } from './HotkeyDefinitionType';
|
import { HotkeyDefinitionType, specialKeys } from './HotkeyDefinitionType';
|
||||||
import { HotkeyListener } from './HotkeyListener';
|
import { HotkeyListener } from './HotkeyListener';
|
||||||
import { JsonHelper, ObjectHelper } from '@ainias42/js-helper';
|
import { JsonHelper, ObjectHelper } from '@ainias42/js-helper';
|
||||||
import { AffectedEnum } from './AffectedEnum';
|
import { HotkeyEntry } from './HotkeyEntry';
|
||||||
|
import { SubKeyType } from './SubKeyType';
|
||||||
type HotkeyEntry<SubKeys extends string> = {
|
import { HotkeyPressedMap } from './HotkeyPressedMap';
|
||||||
keys: HotkeyDefinitionType[];
|
import { HotkeyConfigWithOptionals } from './HotkeyConfigWithOptionals';
|
||||||
subKeys: { [key in SubKeys]: HotkeyDefinitionType[] };
|
|
||||||
callbacks: HotkeyListener<SubKeys>[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export class HotkeyManager<HotkeyConfig extends Record<string, HotkeyEntry<string>>> {
|
export class HotkeyManager<HotkeyConfig extends Record<string, HotkeyEntry<string>>> {
|
||||||
private keyPressedMap = new Map<string, boolean>();
|
private keyPressedMap = new Map<string, boolean>();
|
||||||
private hotkeys: HotkeyConfig;
|
private hotkeys: HotkeyConfig;
|
||||||
|
private hotkeysPressedMap: HotkeyPressedMap<HotkeyConfig>;
|
||||||
private enabled = true;
|
private enabled = true;
|
||||||
|
private ignoreFormElements = true;
|
||||||
|
|
||||||
constructor(hotkeys: HotkeyConfig) {
|
constructor(hotkeys: HotkeyConfigWithOptionals<HotkeyConfig>, ignoreFormElements?: boolean) {
|
||||||
this.hotkeys = hotkeys;
|
this.hotkeys = ObjectHelper.entries(hotkeys).reduce((acc, [key, value]) => {
|
||||||
this.addKeyListeners();
|
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) {
|
private static isFormElement(element: EventTarget | null) {
|
||||||
@ -31,9 +37,20 @@ export class HotkeyManager<HotkeyConfig extends Record<string, HotkeyEntry<strin
|
|||||||
return JsonHelper.deepEqual(a, b);
|
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>(
|
addListener<HotkeyName extends keyof HotkeyConfig>(
|
||||||
hotkeyName: HotkeyName,
|
hotkeyName: HotkeyName,
|
||||||
callback: HotkeyListener<keyof HotkeyConfig[HotkeyName]['subKeys']>,
|
callback: HotkeyListener<SubKeyType<HotkeyConfig, HotkeyName>>,
|
||||||
) {
|
) {
|
||||||
const { callbacks } = this.hotkeys[hotkeyName];
|
const { callbacks } = this.hotkeys[hotkeyName];
|
||||||
callbacks.push(callback);
|
callbacks.push(callback);
|
||||||
@ -67,105 +84,148 @@ export class HotkeyManager<HotkeyConfig extends Record<string, HotkeyEntry<strin
|
|||||||
|
|
||||||
addSubKeyDefinition<HotkeyName extends keyof HotkeyConfig>(
|
addSubKeyDefinition<HotkeyName extends keyof HotkeyConfig>(
|
||||||
hotkey: HotkeyName,
|
hotkey: HotkeyName,
|
||||||
subkey: keyof HotkeyConfig[HotkeyName]['subKeys'],
|
subKey: SubKeyType<HotkeyConfig, HotkeyName>,
|
||||||
definition: HotkeyDefinitionType,
|
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>(
|
removeSubKeyDefinition<HotkeyName extends keyof HotkeyConfig>(
|
||||||
hotkey: HotkeyName,
|
hotkey: HotkeyName,
|
||||||
subkey: keyof HotkeyConfig[HotkeyName]['subKeys'],
|
subKey: SubKeyType<HotkeyConfig, HotkeyName>,
|
||||||
definition: HotkeyDefinitionType,
|
definition: HotkeyDefinitionType,
|
||||||
) {
|
) {
|
||||||
const { subKeys } = this.hotkeys[hotkey];
|
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>(
|
setSubKeyDefinitions<HotkeyName extends keyof HotkeyConfig>(
|
||||||
hotkey: HotkeyName,
|
hotkey: HotkeyName,
|
||||||
subkey: keyof HotkeyConfig[HotkeyName]['subKeys'],
|
subKey: SubKeyType<HotkeyConfig, HotkeyName>,
|
||||||
definitions: HotkeyDefinitionType[],
|
definitions: HotkeyDefinitionType[],
|
||||||
) {
|
) {
|
||||||
this.hotkeys[hotkey].subKeys[subkey as string] = definitions;
|
this.hotkeys[hotkey].subKeys[subKey as string] = definitions;
|
||||||
}
|
}
|
||||||
|
|
||||||
getConfig() {
|
getConfig() {
|
||||||
return this.hotkeys;
|
return this.hotkeys;
|
||||||
}
|
}
|
||||||
|
|
||||||
private addKeyListeners() {
|
addKeyListeners() {
|
||||||
window.addEventListener('keydown', (e) => {
|
window.addEventListener('keydown', (e) => {
|
||||||
|
if (this.ignoreFormElements && HotkeyManager.isFormElement(e.target)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.keyPressedMap.set(e.key.toLowerCase(), true);
|
this.keyPressedMap.set(e.key.toLowerCase(), true);
|
||||||
this.checkHotkeys(e, 'keydown');
|
this.checkHotkeys(e);
|
||||||
});
|
});
|
||||||
window.addEventListener('keyup', (e) => {
|
window.addEventListener('keyup', (e) => {
|
||||||
// Check first as afterwards the keys are not set
|
// TODO check if meta key is still working correctly
|
||||||
this.checkHotkeys(e, 'keyup');
|
if (this.ignoreFormElements && HotkeyManager.isFormElement(e.target)) {
|
||||||
this.keyPressedMap.set(e.key.toLowerCase(), false);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (e.key === 'Meta') {
|
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) {
|
if (!this.enabled) {
|
||||||
return AffectedEnum.NOT_FULL_FILLED;
|
return false;
|
||||||
}
|
|
||||||
if (keyDefinition.ignoreFormElements !== false && HotkeyManager.isFormElement(event.target)) {
|
|
||||||
return AffectedEnum.NOT_FULL_FILLED;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (keyDefinition.keys.some((key) => this.keyPressedMap.get(key) !== true)) {
|
if (keyDefinition.keys.some((key) => this.keyPressedMap.get(key) !== true)) {
|
||||||
return AffectedEnum.NOT_FULL_FILLED;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
return !specialKeys.some(
|
||||||
specialKeys.some(
|
(key) => keyDefinition[key] !== undefined && !!this.keyPressedMap.get(key) !== keyDefinition[key],
|
||||||
(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) {
|
private generateHotkeyPressedMap() {
|
||||||
return (acc: AffectedEnum, key: HotkeyDefinitionType) => {
|
return ObjectHelper.entries(this.hotkeys).reduce((acc, [key, value]) => {
|
||||||
if (acc === AffectedEnum.FULL_FILLED_AND_CHANGED) {
|
const isPressed = this.checkHotkeyDefinitions(value.keys);
|
||||||
return acc;
|
acc[key] = {
|
||||||
}
|
isPressed,
|
||||||
return Math.max(acc, this.checkHotkeyDefinition(key, event));
|
subKeys: ObjectHelper.keys(value.subKeys).reduce(
|
||||||
};
|
(accInner, subKey) => {
|
||||||
}
|
accInner[subKey as SubKeyType<HotkeyConfig, typeof key>] =
|
||||||
|
isPressed && this.checkHotkeyDefinitions(value.subKeys[subKey]);
|
||||||
private checkHotkeys(event: KeyboardEvent, type: 'keydown' | 'keyup') {
|
return accInner;
|
||||||
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>,
|
{} as Record<SubKeyType<HotkeyConfig, typeof key>, boolean>,
|
||||||
);
|
),
|
||||||
|
};
|
||||||
|
return acc;
|
||||||
|
}, {} as HotkeyPressedMap<HotkeyConfig>);
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
private triggerListenerFor(key: keyof HotkeyConfig, event: KeyboardEvent) {
|
||||||
isAffected === AffectedEnum.FULL_FILLED_AND_CHANGED ||
|
const pressedEntry = this.hotkeysPressedMap[key];
|
||||||
ObjectHelper.values(subKeys).some((value) => value === AffectedEnum.FULL_FILLED_AND_CHANGED)
|
this.hotkeys[key].callbacks.forEach((callback) =>
|
||||||
) {
|
callback({ event, subKeys: pressedEntry.subKeys, isPressed: pressedEntry.isPressed }),
|
||||||
hotkey.callbacks.forEach((callback) => callback({ event, subKeys, type }));
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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