isomorphic-qwerty
Version:
Isomorphic coordinate-system for the QWERTY keyboard with sustain using the Shift key
164 lines • 6.18 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Keyboard = void 0;
const coordinates_1 = require("./coordinates");
/**
* Keyboard event listener that filters out repeated keydown events and normalizes keycodes to coordinates.
* The Shift keys toggle 'sustain'.
*/
class Keyboard {
/**
* Construct a new keyboard event listener.
* @param autobind Start listening to "keydown" and "keyup" events immediately.
* @param log Logging function.
*/
constructor(autobind = false, log) {
this.keydownCallbacks = [];
this.keyupCallbacks = new Map();
this.activeKeys = new Set();
this.pendingKeys = new Set();
this.stickyKeys = new Set();
if (autobind) {
this._keydown = (event) => this.keydown(event);
this._keyup = (event) => this.keyup(event);
window.addEventListener('keydown', this._keydown);
window.addEventListener('keyup', this._keyup);
}
if (log === undefined) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
this.log = (msg) => { };
}
else {
this.log = log;
}
}
/**
* Stop listening to "keydown" and "keyup" events if constructed with `autobind = true`.
*/
dispose() {
if (this._keydown) {
window.removeEventListener('keydown', this._keydown);
}
if (this._keyup) {
window.removeEventListener('keyup', this._keyup);
}
}
/**
* Register a listener for processed keydown events.
* @param listener Function to call when a new key is pressed.
*/
addKeydownListener(listener) {
this.keydownCallbacks.push(listener);
}
/**
* Unregister a listener for processed keydown events.
* @param listener Callback registered with {@link Keyboard.addKeydownListener}.
*/
removeEventListener(listener) {
this.keydownCallbacks.splice(this.keydownCallbacks.indexOf(listener), 1);
}
fireKeydown(event) {
event.coordinates = coordinates_1.COORDS_BY_CODE.get(event.code);
const keyupCallbacks = this.keyupCallbacks.get(event.code) || [];
for (const callback of keyupCallbacks) {
console.warn('Unresolved keyup detected');
callback();
}
this.log(`Firing keydown listeners with ${event.code} @ ${event.coordinates}`);
this.keydownCallbacks.forEach(callback => keyupCallbacks.push(callback(event)));
this.keyupCallbacks.set(event.code, keyupCallbacks);
}
fireKeyup(event) {
this.log(`Firing keyup listeners with ${event.code}`);
for (const callback of this.keyupCallbacks.get(event.code) || []) {
callback();
}
this.keyupCallbacks.delete(event.code);
}
/**
* Listener to be registered with `window.addEventListener("keydown", ...)`.
* @param event Keyboard event of a key being pressed down.
*/
keydown(event) {
this.log(`${event.code} keydown received`);
if (event.ctrlKey || event.altKey || event.metaKey || event.repeat) {
this.log(`${event.code} keydown filtered out`);
return;
}
// The pending state isn't strictly necessary as we filter out repeated events,
// but it's kept in case event.repeat isn't 100% reliable.
if (event.key === 'Shift') {
for (const code of this.activeKeys) {
this.log(`Adding ${code} to pending state due to a 'Shift' press`);
this.pendingKeys.add(code);
}
return;
}
if (this.stickyKeys.has(event.code)) {
this.log(`Stricky toggle for ${event.code}`);
this.activeKeys.delete(event.code);
this.stickyKeys.delete(event.code);
this.pendingKeys.delete(event.code);
this.fireKeyup(event);
return;
}
if (this.pendingKeys.has(event.code)) {
this.log(`${event.code} is pending`);
return;
}
if (this.activeKeys.has(event.code)) {
this.log(`${event.code} is already active`);
return;
}
if (coordinates_1.COORDS_BY_CODE.has(event.code)) {
this.log(`Adding ${event.code} to active state`);
this.activeKeys.add(event.code);
if (event.shiftKey) {
this.log(`Adding ${event.code} to pending state due to being pressed with 'Shift'`);
this.pendingKeys.add(event.code);
}
this.fireKeydown(event);
return;
}
}
/**
* Listener to be registered with `window.addEventListener("keyup", ...)`.
* @param event Keyboard event of a pressed key being released.
*/
keyup(event) {
this.log(`${event.code} keyup received`);
if (event.shiftKey && this.activeKeys.has(event.code)) {
this.log(`Sticking ${event.code} due being released while 'Shift' is pressed`);
this.stickyKeys.add(event.code);
}
if (this.pendingKeys.has(event.code)) {
this.log(`Promoting ${event.code} from pending to sticky`);
this.pendingKeys.delete(event.code);
this.stickyKeys.add(event.code);
}
if (this.stickyKeys.has(event.code)) {
this.log(`Not firing keyup due to ${event.code} being sticky`);
return;
}
if (this.activeKeys.has(event.code)) {
this.activeKeys.delete(event.code);
this.fireKeyup(event);
return;
}
this.log(`${event.code} keyup fell through`);
}
/**
* Release keys sustained due to being pressed down with 'Shift'.
*/
deactivate() {
this.log('Releasing all sustained and active keys');
this.pendingKeys.clear();
this.stickyKeys.clear();
for (const code of this.activeKeys.keys()) {
this.fireKeyup({ code });
}
this.activeKeys.clear();
}
}
exports.Keyboard = Keyboard;
//# sourceMappingURL=keyboard.js.map