UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

201 lines (158 loc) 5.84 kB
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;