@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
201 lines (158 loc) • 5.84 kB
JavaScript
import Signal from "../../../core/events/signal/Signal.js";
import { KeyboardEvents } from "./events/KeyboardEvents.js";
import { InputDeviceSwitch } from "./InputDeviceSwitch.js";
import { isHTMLElementFocusable } from "./isHTMLElementFocusable.js";
import { KeyCodes } from './KeyCodes.js';
/**
* @readonly
* @type {string[]}
*/
const codeToKeyNameMap = [];
/**
* Provides keyboard input.
* Listens for key events on a DOM element and provides signals and state for key presses.
* NOTE: when losing focus of the application, any "pressed" keys will be automatically released.
* For example, if you hold "A" and click over into another application window - A will be forcibly released with appropriate signal.
*
* @author Alex Goldring
* @copyright Company Named Limited (c) 2025
*/
class KeyboardDevice {
/**
* @readonly
*/
on = {
/**
* Fires when any key is pressed
* @type {Signal<KeyboardEvent>}
*/
down: new Signal(),
/**
* Fires when any key is released
* @type {Signal<KeyboardEvent>}
*/
up: new Signal()
};
/**
* See {@link KeyCodes} for valid IDs
* @readonly
* @type {Object<InputDeviceSwitch>}
*
* @example
* const is_enter_pressed = keyboard.keys.enter.is_down;
*/
keys = {};
/**
* @param {EventTarget|Element} domElement The DOM element to listen for keyboard events on. This element *must* be focusable (e.g., have a `tabindex` attribute).
* @throws {TypeError} If the provided `domElement` is not focusable and doesn't have a `tabindex` attribute.
*/
constructor(domElement) {
/*
Only element in focus receives keyboard events, so the element supplied here must be focusable in order to be able to receive events
*/
if (
!isHTMLElementFocusable(domElement)
&& domElement.getAttribute('tabindex') === null
) {
new TypeError('Supplied element is not inherently focusable and does not have tabindex attribute, so it must have a tabindex attribute in order to be able receive keyboard events. Something like tabindex=0 would suffice.');
}
/**
* The DOM element being listened to.
* @type {EventTarget}
*/
this.domElement = domElement;
//initialize separate events for each key
for (let keyName in KeyCodes) {
const keyCode = KeyCodes[keyName];
codeToKeyNameMap[keyCode] = keyName;
this.keys[keyName] = new InputDeviceSwitch();
}
}
/**
*
* @param {KeyboardEvent} event
* @private
*/
#handlerKeyDown = (event) => {
if (event.repeat) {
// ignore automatic repetition
// see https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/repeat
return;
}
this.on.down.send1(event);
let should_prevent_default = false;
//hook up dispatch handler for individual keys
const keyCode = event.keyCode;
const keyName = codeToKeyNameMap[keyCode];
if (keyName !== undefined) {
const button = this.keys[keyName];
button.press();
if (button.down.hasHandlers()) {
should_prevent_default = true;
}
}
if (should_prevent_default) {
event.preventDefault();
}
}
/**
*
* @param {KeyboardEvent} event
* @private
*/
#handlerKeyUp = (event) => {
this.on.up.send1(event);
let should_prevent_default = false;
//hook up dispatch handler for individual keys
const keyCode = event.keyCode;
const keyName = codeToKeyNameMap[keyCode];
if (keyName !== undefined) {
const button = this.keys[keyName];
button.release();
if (button.down.hasHandlers()) {
should_prevent_default = true;
}
}
if (should_prevent_default) {
event.preventDefault();
}
}
/**
*
* @param {FocusEvent} event
*/
#handleGlobalBlurEvent = (event) => {
// Element lost focus, we won't be able to capture key-up events
// release all keys
for (let keyName in KeyCodes) {
this.keys[keyName].release();
}
}
/**
* Starts listening for keyboard events on the associated DOM element.
* @returns {void}
*/
start() {
const el = this.domElement;
el.addEventListener(KeyboardEvents.KeyDown, this.#handlerKeyDown);
el.addEventListener(KeyboardEvents.KeyUp, this.#handlerKeyUp);
el.addEventListener('blur', this.#handleGlobalBlurEvent);
//https://w3c.github.io/uievents/#event-type-focusout
el.addEventListener('focusout', this.#handleGlobalBlurEvent);
window.addEventListener('blur', this.#handleGlobalBlurEvent);
}
/**
* Stops listening for keyboard events.
* @returns {void}
*/
stop() {
const el = this.domElement;
el.removeEventListener(KeyboardEvents.KeyDown, this.#handlerKeyDown);
el.removeEventListener(KeyboardEvents.KeyUp, this.#handlerKeyUp);
el.removeEventListener('blur', this.#handleGlobalBlurEvent);
//https://w3c.github.io/uievents/#event-type-focusout
el.removeEventListener('focusout', this.#handleGlobalBlurEvent);
window.removeEventListener('blur', this.#handleGlobalBlurEvent);
}
}
export default KeyboardDevice;