Description
I am storing and checking the key from a keydown
event. I have created five enum flags (I had read that enum flags are a way to efficiently store and represent a collection of boolean values) to represent various types of keys on the keyboard:
- Alphabet
- Modifier
- Number
- Navigation
- Special
I have a Keyboard
class that encapsulates all of these enums. The Keyboard
class:
- has get/set properties for each enum,
- has a
set(key: string)
method, which adds the keyboard key to the appropriate enum, and - has
is...(key: enum)
methods for each enum (isAlphabet
,isNumber
), which returns if a given key has been stored as hit in the enum flag
Problem
The Keyboard
class is anything but DRY. Code is repeated for every enum. I have tried to figure out how to make the solution more generic, but am having issues using generic syntax with enums.
Question: Any ideas for how to make this code less repetitive?
Code
Enums
export enum modifierKeys {
none = 0,
shift = 1 << 0,
control = 1 << 1,
alt = 1 << 2
}
export enum alphabetKeys {
none = 0,
a = 1 << 0,
b = 1 << 1,
c = 1 << 2,
d = 1 << 3,
e = 1 << 4,
f = 1 << 5,
g = 1 << 6,
h = 1 << 7,
i = 1 << 8,
j = 1 << 9,
k = 1 << 10,
l = 1 << 11,
m = 1 << 12,
n = 1 << 13,
o = 1 << 14,
p = 1 << 15,
q = 1 << 16,
r = 1 << 17,
s = 1 << 18,
t = 1 << 19,
u = 1 << 20,
v = 1 << 21,
w = 1 << 22,
x = 1 << 23,
y = 1 << 24,
z = 1 << 25
}
export enum numberKeys {
none = 0,
zero = 1 << 0,
one = 1 << 1,
two = 1 << 2,
three = 1 << 3,
four = 1 << 4,
five = 1 << 5,
six = 1 << 6,
seven = 1 << 7,
eight = 1 << 8,
nine = 1 << 9
}
export enum specialKeys {
none = 0,
space = 1 << 0,
enter = 1 << 1,
backspace = 1 << 2,
escape = 1 << 3,
delete = 1 << 4
}
export enum navigationKeys {
none = 0,
arrowup = 1 << 0,
arrowright = 1 << 1,
arrowdown = 1 << 2,
arrowleft = 1 << 3
}
export enum keyAlias {
space = ' ',
zero = '0',
one = '1',
two = '2',
three = '3',
four = '4',
five = '5',
six = '6',
seven = '7',
eight = '8',
nine = '9'
}
Keyboard Class
export class Keyboard {
private _modifier: modifierKeys;
private _special: specialKeys;
private _navigation: navigationKeys;
private _alphabet: alphabetKeys;
private _number: numberKeys;
public constructor() {
this.reset();
}
public get modifier(): modifierKeys {
return this._modifier;
}
public set modifier(value: modifierKeys) {
this._modifier = value;
}
public get special(): specialKeys {
return this._special;
}
public set special(value: specialKeys) {
this._special = value;
}
public get navigation(): navigationKeys {
return this._navigation;
}
public set navigation(value: navigationKeys) {
this._navigation = value;
}
public get alphabet(): alphabetKeys {
return this._alphabet;
}
public set alphabet(value: alphabetKeys) {
this._alphabet = value;
}
public get number(): numberKeys {
return this._number;
}
public set number(value: numberKeys) {
this._number = value;
}
public reset(): void {
this._modifier = modifierKeys.none;
this._special = specialKeys.none;
this._navigation = navigationKeys.none;
this._alphabet = alphabetKeys.none;
this._number = numberKeys.none
}
public set(key: string): void {
// If key matches value of 'keyAlias', set key equal to key of 'keyAlias'
Object.entries(keyAlias).find(([k, v]) => (key == v) ? key = k : false);
// Set key
switch(true) {
case Object.keys(alphabetKeys).includes(key):
this._alphabet |= alphabetKeys[key as keyof typeof alphabetKeys];
break;
case Object.keys(numberKeys).includes(key):
this._number |= numberKeys[key as keyof typeof numberKeys];
break;
case Object.keys(navigationKeys).includes(key):
this._navigation |= navigationKeys[key as keyof typeof navigationKeys];
break;
case Object.keys(modifierKeys).includes(key):
this._modifier |= modifierKeys[key as keyof typeof modifierKeys];
break;
case Object.keys(specialKeys).includes(key):
this._special |= specialKeys[key as keyof typeof specialKeys];
break;
}
}
public isAlphabet(key: alphabetKeys): boolean {
return ((this._alphabet & key) == key);
}
public isNumber(key: numberKeys): boolean {
return ((this._number & key) == key);
}
public isSpecial(key: specialKeys): boolean {
return ((this._special & key) == key);
}
public isModifier(key: modifierKeys): boolean {
return ((this._modifier & key) == key);
}
public isNavigation(key: navigationKeys): boolean {
return ((this._navigation & key) == key);
}
}
Update
Based on the comments, it appeared enum flags were not the best way to store keyboard key state. Instead, I made:
- an enum called
KeyType
, which specifies the types of keys, - an enum called
Keys
, which stores the keyboard keys, - a 2D array of
KeyType
andKeys
calledKeyMap
- a generic interface called
IFlag<T, V>
, which encapsulates properties for type, value and flag, and - a generic class called
FlagContainer<T, V>
, which has an array ofIFlag<T, V>
's that it operates over
This way each keyboard key is stored in an IFlag<T, V>
object rather than an enum flag, and the FlagContainer<T, V>
can be used for more than keyboard keys.
Code
Helper Enums & Array
export enum KeyType {
alphabet,
modifier,
navigation,
number,
special
}
export enum Keys {
shift = 'shift',
control = 'control',
delete = 'delete',
alt = 'alt',
a = 'a',
b = 'b',
c = 'c',
d = 'd',
e = 'e',
f = 'f',
g = 'g',
h = 'h',
i = 'i',
j = 'j',
k = 'k',
l = 'l',
m = 'm',
n = 'n',
o = 'o',
p = 'p',
q = 'q',
r = 'r',
s = 's',
t = 't',
u = 'u',
v = 'v',
w = 'w',
x = 'x',
y = 'y',
z = 'z',
zero = '0',
one = '1',
two = '2',
three = '3',
four = '4',
five = '5',
six = '6',
seven = '7',
eight = '8',
nine = '9',
space = ' ',
enter = 'enter',
backspace = 'backspace',
escape = 'escape',
arrowup = 'arrowup',
arrowright = 'arrowright',
arrowdown = 'arrowdown',
arrowleft = 'arrowleft'
}
const KeyMap: [KeyType, Keys][] = [
[KeyType.alphabet, Keys.a],
[KeyType.alphabet, Keys.b],
[KeyType.alphabet, Keys.c],
[KeyType.alphabet, Keys.d],
[KeyType.alphabet, Keys.e],
[KeyType.alphabet, Keys.f],
[KeyType.alphabet, Keys.g],
[KeyType.alphabet, Keys.h],
[KeyType.alphabet, Keys.i],
[KeyType.alphabet, Keys.j],
[KeyType.alphabet, Keys.k],
[KeyType.alphabet, Keys.l],
[KeyType.alphabet, Keys.m],
[KeyType.alphabet, Keys.n],
[KeyType.alphabet, Keys.o],
[KeyType.alphabet, Keys.p],
[KeyType.alphabet, Keys.q],
[KeyType.alphabet, Keys.r],
[KeyType.alphabet, Keys.s],
[KeyType.alphabet, Keys.t],
[KeyType.alphabet, Keys.u],
[KeyType.alphabet, Keys.v],
[KeyType.alphabet, Keys.w],
[KeyType.alphabet, Keys.x],
[KeyType.alphabet, Keys.y],
[KeyType.alphabet, Keys.z],
[KeyType.modifier, Keys.alt],
[KeyType.modifier, Keys.control],
[KeyType.modifier, Keys.shift],
[KeyType.navigation, Keys.arrowup],
[KeyType.navigation, Keys.arrowright],
[KeyType.navigation, Keys.arrowdown],
[KeyType.navigation, Keys.arrowleft],
[KeyType.number, Keys.zero],
[KeyType.number, Keys.one],
[KeyType.number, Keys.two],
[KeyType.number, Keys.three],
[KeyType.number, Keys.four],
[KeyType.number, Keys.five],
[KeyType.number, Keys.six],
[KeyType.number, Keys.seven],
[KeyType.number, Keys.eight],
[KeyType.number, Keys.nine],
[KeyType.special, Keys.space],
[KeyType.special, Keys.enter],
[KeyType.special, Keys.backspace],
[KeyType.special, Keys.escape],
[KeyType.special, Keys.delete],
];
Generic Interface / Class
export interface IFlag<T, V> {
type: Type;
value: Value;
flag: boolean;
}
export class FlagContainer<T, V> {
private readonly _flag: IFlag<T, V>[];
public constructor(map: [T, V][]) {
this._flag = [];
map.forEach(m => {
this._flag.push(
{
type: m[0],
value: m[1],
flag: false
});
}
);
}
public clear(): void {
this._flag.forEach(f => f.flag = false);
}
public flagged(value: V): boolean {
return this._flag.some(f => f.value == value && f.flag == true);
}
public flag(value: V): void {
this._flag.forEach(f => {
if(f.value == value) f.flag = true
});
}
public any(type: T): boolean {
return this._flag.some(f => f.type == type && f.flag == true);
}
}
And now the Keyboard
object is simplified:
Class SomeParentClass {
...
private readonly _keyboard: FlagContainer<KeyType, Keys>;
...
}
I wouldn't say this answers the original question, but I would say this question is closed.