apple2js/js/components/util/keyboard.ts
Ian Flanigan 4688cae5b2
Refactor key handling to use event.key (#152)
Before, keyboard input used key codes to map events to Apple II keys.
This worked reasonably well, but `event.keyCode` was deprecated and
slated to be removed.

The refactored code now uses `event.key` which returns the localized,
keyboard-mapped key that the user pressed, which may be a letter or a
"symbolic" key.  This is then transformed into an Apple II key.

One side effect of the refactoring is that the keys now light up as
you type and that combinations of mouse clicks on modifiers and plain
keys will take the modifiers into account.
2022-08-24 18:23:22 -07:00

257 lines
7.0 KiB
TypeScript

import { JSX } from 'preact';
import { DeepMemberOf, KnownKeys } from '../../types';
export const SPECIAL_KEY_MAP = {
'Shift': 'SHIFT',
'Enter': 'RETURN',
'CapsLock': 'LOCK',
'Control': 'CTRL',
'Escape': 'ESC',
'Delete': 'RESET',
'Tab': 'TAB',
'Backspace': 'DELETE',
'ArrowUp': '↑',
'ArrowDown': '↓',
'ArrowRight': '→',
'ArrowLeft': '←',
// UiKit symbols
'UIKeyInputLeftArrow': '←',
'UIKeyInputRightArrow': '→',
'UIKeyInputUpArrow': '↑',
'UIKeyInputDownArrow': '↓',
'UIKeyInputEscape': 'ESC',
} as const;
export const isSpecialKey = (k: string): k is KnownKeys<typeof SPECIAL_KEY_MAP> => {
return k in SPECIAL_KEY_MAP;
};
export const SPECIAL_KEY_CODE = {
'TAB': 9,
'RETURN': 13,
'ESC': 27,
'&uarr;': 11,
'&darr;': 10,
'&rarr;': 21,
'&larr;': 8,
'DELETE': 127,
} as const;
export const hasSpecialKeyCode = (k: string): k is KnownKeys<typeof SPECIAL_KEY_CODE> => {
return k in SPECIAL_KEY_CODE;
};
/**
* Keyboard layout for the Apple ][ / ][+
*/
export const keys2 = [
[
['1', '2', '3', '4', '5', '6', '7', '8', '9', '0', ':', '-', 'RESET'],
['ESC', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', 'REPT', 'RETURN'],
['CTRL', 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', ';', '&larr;', '&rarr;'],
['SHIFT', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', ',', '.', '/', 'SHIFT'],
['POWER', '&nbsp;']
], [
['!', '"', '#', '$', '%', '&', '\'', '(', ')', '0', '*', '=', 'RESET'],
['ESC', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', '@', 'REPT', 'RETURN'],
['CTRL', 'A', 'S', 'D', 'F', 'BELL', 'H', 'J', 'K', 'L', '+', '&larr;', '&rarr;'],
['SHIFT', 'Z', 'X', 'C', 'V', 'B', '^', ']', '<', '>', '?', 'SHIFT'],
['POWER', '&nbsp;']
]
] as const;
export type Key2 = DeepMemberOf<typeof keys2>;
/**
* Keyboard layout for the Apple //e
*/
export const keys2e = [
[
['ESC', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '=', 'DELETE'],
['TAB', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '[', ']', '\\'],
['CTRL', 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', ';', '"', 'RETURN'],
['SHIFT', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', ',', '.', '/', 'SHIFT'],
['LOCK', '`', 'POW', 'OPEN_APPLE', '&nbsp;', 'CLOSED_APPLE', '&larr;', '&rarr;', '&darr;', '&uarr;']
], [
['ESC', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '_', '+', 'DELETE'],
['TAB', 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '{', '}', '|'],
['CTRL', 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', ':', '\'', 'RETURN'],
['SHIFT', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', '<', '>', '?', 'SHIFT'],
['CAPS', '~', 'POW', 'OPEN_APPLE', '&nbsp;', 'CLOSED_APPLE', '&larr;', '&rarr;', '&darr;', '&uarr;']
]
] as const;
/** Shifted */
const SHIFTED = {
'!' : '1' ,
'@' : '2' ,
'#' : '3' ,
'$' : '4' ,
'%' : '5' ,
'^' : '6' ,
'&' : '7' ,
'*' : '8' ,
'(' : '9' ,
')' : '0' ,
'_' : '-' ,
'+' : '=' ,
'{' : '[' ,
'}' : ']' ,
'|' : '\\',
':' : ';' ,
'\'' : '"',
'<' : ',' ,
'>' : '.' ,
'?' : '/' ,
'~' : '`' ,
} as const;
export const isShiftyKey = (k: string): k is KnownKeys<typeof SHIFTED> => {
return k in SHIFTED;
};
export type Key2e = DeepMemberOf<typeof keys2e>;
export type Key = Key2 | Key2e;
export type KeyFunction = (key: KeyboardEvent) => void;
/**
* Convert a DOM keyboard event into an ASCII equivalent that
* an Apple II can recognize.
*
* @param evt Event to convert
* @param caps Caps Lock state
* @returns a tuple of:
* * `key`: the symbol of the key
* * `keyLabel`: the label on the keycap
* * `keyCode`: the corresponding byte for the Apple II
*/
export const mapKeyboardEvent = (event: KeyboardEvent, caps: boolean = false, control: boolean = false) => {
let key: string;
if (isSpecialKey(event.key)) {
key = SPECIAL_KEY_MAP[event.key];
} else if (event.key === 'Alt') {
key = event.location === 1 ? 'OPEN_APPLE' : 'CLOSED_APPLE';
} else {
key = event.key;
}
let keyLabel = key;
if (key.length === 1) {
if (isShiftyKey(key)) {
keyLabel = SHIFTED[key];
} else {
keyLabel = key.toUpperCase();
}
}
let keyCode = 0xff;
if (hasSpecialKeyCode(key)) {
keyCode = SPECIAL_KEY_CODE[key];
} else if (key.length === 1) {
keyCode = key.charCodeAt(0);
}
if ((caps || control) && keyCode >= 0x61 && keyCode <= 0x7A) {
keyCode -= 0x20;
}
if (control && keyCode >= 0x40 && keyCode < 0x60) {
keyCode -= 0x40;
}
return { key, keyLabel, keyCode};
};
/**
* Convert a mouse event into an ASCII character based on the
* data attached to the target DOM element.
*
* @param event Event to convert
* @param shifted Shift key state
* @param controlled Control key state
* @param caps Caps Lock state
* @param e //e status
* @returns ASCII character
*/
export const mapMouseEvent = (
event: JSX.TargetedMouseEvent<HTMLElement>,
shifted: boolean,
controlled: boolean,
caps: boolean,
e: boolean,
) => {
const keyLabel = event.currentTarget?.dataset.key2 ?? '';
let key = event.currentTarget?.dataset[shifted ? 'key2' : 'key1'] ?? '';
let keyCode = 0xff;
switch (key) {
case 'BELL':
key = 'G';
break;
case 'RETURN':
key = '\r';
break;
case 'TAB':
key = '\t';
break;
case 'DELETE':
key = '\x7F';
break;
case '&larr;':
key = '\x08';
break;
case '&rarr;':
key = '\x15';
break;
case '&darr;':
key = '\x0A';
break;
case '&uarr;':
key = '\x0B';
break;
case '&nbsp;':
key = ' ';
break;
case 'ESC':
key = '\x1B';
break;
default:
break;
}
if (key.length === 1) {
if (controlled && key >= '@' && key <= '_') {
keyCode = key.charCodeAt(0) - 0x40;
} else if (
e && !shifted && !caps &&
key >= 'A' && key <= 'Z'
) {
keyCode = key.charCodeAt(0) + 0x20;
} else {
keyCode = key.charCodeAt(0);
}
}
return { keyCode, key, keyLabel };
};
/**
* Remap keys so that upper and lower are a tuple per row instead of
* separate rows
*
* @param inKeys keys2 or keys2e
* @returns Keys remapped
*/
export const keysAsTuples = (inKeys: typeof keys2e | typeof keys2): string[][][] => {
const rows = [];
for (let idx = 0; idx < inKeys[0].length; idx++) {
const upper = inKeys[0][idx];
const lower = inKeys[1][idx];
const keys = [];
for (let jdx = 0; jdx < upper.length; jdx++) {
keys.push([upper[jdx], lower[jdx]]);
}
rows.push(keys);
}
return rows;
};