From e6dc76e93c3544d95341dd7f282e6d6d379f4c65 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Sun, 21 Aug 2022 11:00:58 -0400 Subject: [PATCH] InputWritr: a first revamp (#319) Cleans up InputWritr a bit by improving some of the docs and function names. Adds partial unit test coverage Moves alias conversion to UserWrappr, where it belongs Renames makePipe to createPipe (I like "create" more for this)_ Removes a few unused areas of code Filed #317 to tackle a more complete overhaul of the package long term. --- docs/walkthrough/5. Inputs.md | 2 +- .../FullScreenSaver/src/sections/Inputs.ts | 2 +- packages/eightbittr/src/sections/Inputs.ts | 4 +- packages/inputwritr/README.md | 2 +- packages/inputwritr/src/AliasConverter.ts | 124 --------- packages/inputwritr/src/InputWritr.test.ts | 142 ++++++++++- packages/inputwritr/src/InputWritr.ts | 241 +++++------------- packages/inputwritr/src/index.ts | 1 - packages/inputwritr/src/types.ts | 11 +- .../userwrappr/src/getAliasesAsKeyStrings.ts | 64 +++++ packages/userwrappr/src/index.ts | 1 + 11 files changed, 280 insertions(+), 314 deletions(-) delete mode 100644 packages/inputwritr/src/AliasConverter.ts create mode 100644 packages/userwrappr/src/getAliasesAsKeyStrings.ts diff --git a/docs/walkthrough/5. Inputs.md b/docs/walkthrough/5. Inputs.md index 0d18278ac..dbbda515f 100644 --- a/docs/walkthrough/5. Inputs.md +++ b/docs/walkthrough/5. Inputs.md @@ -41,7 +41,7 @@ export class Inputs extends InputsBase { gameWindow.addEventListener( "keydown", - this.game.inputWriter.makePipe("onkeydown", "keyCode") + this.game.inputWriter.createPipe("onkeydown", "keyCode") ); } } diff --git a/examples/FullScreenSaver/src/sections/Inputs.ts b/examples/FullScreenSaver/src/sections/Inputs.ts index 74152145d..85b0a6bfa 100644 --- a/examples/FullScreenSaver/src/sections/Inputs.ts +++ b/examples/FullScreenSaver/src/sections/Inputs.ts @@ -50,7 +50,7 @@ export class Inputs extends InputsBase { gameWindow.addEventListener( "keydown", - this.game.inputWriter.makePipe("onkeydown", "keyCode") + this.game.inputWriter.createPipe("onkeydown", "keyCode") ); } } diff --git a/packages/eightbittr/src/sections/Inputs.ts b/packages/eightbittr/src/sections/Inputs.ts index a0d21bffb..cb9a86fb9 100644 --- a/packages/eightbittr/src/sections/Inputs.ts +++ b/packages/eightbittr/src/sections/Inputs.ts @@ -14,9 +14,9 @@ export class Inputs extends Section { public readonly aliases?: Aliases; /** - * Whether input events are allowed to trigger (by default, true). + * Whether input events are allowed to trigger. */ - public readonly canInputsTrigger: boolean | CanTrigger = true; + public readonly canInputsTrigger?: CanTrigger; /** * Mapping of events to their key codes, to their callbacks. diff --git a/packages/inputwritr/README.md b/packages/inputwritr/README.md index 1bdf95f8f..598a92249 100644 --- a/packages/inputwritr/README.md +++ b/packages/inputwritr/README.md @@ -7,7 +7,7 @@ [![NPM version](https://badge.fury.io/js/inputwritr.svg)](http://badge.fury.io/js/inputwritr) [![Join the chat at https://gitter.im/FullScreenShenanigans/community](https://badges.gitter.im/FullScreenShenanigans/community.svg)](https://gitter.im/FullScreenShenanigans/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -Pipes input events to action callbacks. +Pipes input DOM events to callbacks based on their device codes. diff --git a/packages/inputwritr/src/AliasConverter.ts b/packages/inputwritr/src/AliasConverter.ts deleted file mode 100644 index bfdcd2e28..000000000 --- a/packages/inputwritr/src/AliasConverter.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { Aliases, AliasesToCodes, AliasKeys, CodesToAliases } from "./types"; - -/** - * Converts between character aliases and their key strings. - */ -export class AliasConverter { - /** - * Known, allowed aliases for triggers. - */ - private readonly aliases: Aliases; - - /** - * A quick lookup table of key aliases to their character codes. - */ - private readonly keyAliasesToCodes: AliasesToCodes; - - /** - * A quick lookup table of character codes to their key aliases. - */ - private readonly keyCodesToAliases: CodesToAliases; - - /** - * Initializes a new instance of the AliasConverter class. - * - * @param aliases Known, allowed aliases for triggers. - */ - public constructor(aliases: Aliases = {}) { - this.aliases = aliases; - - this.keyAliasesToCodes = { - backspace: 8, - ctrl: 17, - down: 40, - enter: 13, - escape: 27, - left: 37, - right: 39, - shift: 16, - space: 32, - up: 38, - }; - - this.keyCodesToAliases = { - 8: "backspace", - 13: "enter", - 16: "shift", - 17: "ctrl", - 27: "escape", - 32: "space", - 37: "left", - 38: "up", - 39: "right", - 40: "down", - }; - } - - /** - * @returns The stored mapping of aliases to values, with values - * mapped to their equivalent key Strings. - */ - public getAliasesAsKeyStrings(): AliasKeys { - const output: AliasKeys = {}; - - for (const alias in this.aliases) { - output[alias] = this.getAliasAsKeyStrings(alias); - } - - return output; - } - - /** - * Determines the allowed key strings for a given alias. - * - * @param alias An alias allowed to be passed in, typically a - * character code. - * @returns The mapped key Strings corresponding to that alias, - * typically the human-readable Strings representing - * input names, such as "a" or "left". - */ - public getAliasAsKeyStrings(alias: string): string[] { - return this.aliases[alias].map((aliases) => this.convertAliasToKeyString(aliases)); - } - - /** - * @param alias The alias of an input, typically a character code. - * @returns The human-readable String representing the input name, - * such as "a" or "left". - */ - public convertAliasToKeyString(alias: number | string): string { - if (typeof alias === "string") { - return alias; - } - - if (alias > 96 && alias < 105) { - return String.fromCharCode(alias - 48); - } - - if (alias > 64 && alias < 97) { - return String.fromCharCode(alias); - } - - return typeof this.keyCodesToAliases[alias] !== "undefined" - ? this.keyCodesToAliases[alias] - : "?"; - } - - /** - * @param key The number code of an input. - * @returns The machine-usable character code of the input. - */ - public convertKeyStringToAlias(key: number | string): number | string { - if (typeof key === "number") { - return key; - } - - if (key.length === 1) { - return key.charCodeAt(0) - 32; - } - - return typeof this.keyAliasesToCodes[key] !== "undefined" - ? this.keyAliasesToCodes[key] - : -1; - } -} diff --git a/packages/inputwritr/src/InputWritr.test.ts b/packages/inputwritr/src/InputWritr.test.ts index b38449e19..e92ea3d30 100644 --- a/packages/inputwritr/src/InputWritr.test.ts +++ b/packages/inputwritr/src/InputWritr.test.ts @@ -1,5 +1,143 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; + +import { InputWritr } from "./InputWritr"; +import { InputWritrSettings } from "./types"; + +const createInputWritr = (overrides?: Partial) => { + const keyDownLeft = sinon.spy(); + const inputWriter = new InputWritr({ + aliases: { + keyDownLeft: [65], + }, + triggers: { + onkeydown: { keyDownLeft }, + }, + ...overrides, + }); + + return { inputWriter, keyDownLeft }; +}; + describe("InputWritr", () => { - it("_", () => { - /* ... */ + describe("callEvent", () => { + it("does not throw an error when the event type does not exist", () => { + // Arrange + const { inputWriter } = createInputWritr(); + + // Act + const act = () => inputWriter.callEvent("onkeyup", 65); + + // Assert + expect(act).not.to.throw(); + }); + + it("does not throw an error when the key code does not exist ", () => { + // Arrange + const { inputWriter } = createInputWritr(); + + // Act + const act = () => inputWriter.callEvent("onkeydown", -1); + + // Assert + expect(act).not.to.throw(); + }); + + it("does not trigger the event when canTrigger returns false", () => { + // Arrange + const { inputWriter, keyDownLeft } = createInputWritr({ + canTrigger: () => false, + }); + + // Act + inputWriter.callEvent("keyDownLeft", 65); + + // Assert + expect(keyDownLeft).to.have.callCount(0); + }); + + it("triggers the event when canTrigger returns true", () => { + // Arrange + const { inputWriter, keyDownLeft } = createInputWritr({ + canTrigger: () => true, + }); + + // Act + inputWriter.callEvent("onkeydown", 65); + + // Assert + expect(keyDownLeft).to.have.callCount(1); + }); + }); + + describe("createPipe", () => { + it("throws an error when the event type does not exist", () => { + // Arrange + const { inputWriter } = createInputWritr(); + + // Act + const act = () => inputWriter.createPipe("onkeyup", "keyCode"); + + // Assert + expect(act).to.throw(`No trigger of type 'onkeyup' defined.`); + }); + + it("does not call to the piped function when the event code label does not match", () => { + // Arrange + const { inputWriter, keyDownLeft } = createInputWritr(); + + const pipe = inputWriter.createPipe("onkeydown", "keyCode"); + const event = new KeyboardEvent("onkeydown", { keyCode: -1 }); + + // Act + pipe(event); + + // Assert + expect(keyDownLeft).to.have.callCount(0); + }); + + it("calls to the piped function when the event type and its code label match", () => { + // Arrange + const { keyDownLeft, inputWriter } = createInputWritr(); + + const pipe = inputWriter.createPipe("onkeydown", "keyCode"); + const event = new KeyboardEvent("onkeydown", { keyCode: 65 }); + + // Act + pipe(event); + + // Assert + expect(keyDownLeft).to.have.been.calledWith(event); + }); + + it("it does not call preventDefault when preventDefault is false", () => { + // Arrange + const { inputWriter } = createInputWritr(); + + const pipe = inputWriter.createPipe("onkeydown", "keyCode", false); + const event = new KeyboardEvent("onkeydown", { keyCode: 65 }); + event.preventDefault = sinon.spy(); + + // Act + pipe(event); + + // Assert + expect(event.preventDefault).to.have.callCount(0); + }); + + it("it calls preventDefault when preventDefault is true", () => { + // Arrange + const { inputWriter } = createInputWritr(); + + const pipe = inputWriter.createPipe("onkeydown", "keyCode", true); + const event = new KeyboardEvent("onkeydown", { keyCode: 65 }); + event.preventDefault = sinon.spy(); + + // Act + pipe(event); + + // Assert + expect(event.preventDefault).to.have.callCount(1); + }); }); }); diff --git a/packages/inputwritr/src/InputWritr.ts b/packages/inputwritr/src/InputWritr.ts index 9567c29af..b1e06816f 100644 --- a/packages/inputwritr/src/InputWritr.ts +++ b/packages/inputwritr/src/InputWritr.ts @@ -1,13 +1,10 @@ /* eslint-disable @typescript-eslint/no-dynamic-delete */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unnecessary-condition */ -import { AliasConverter } from "./AliasConverter"; import { Aliases, CanTrigger, InputWritrSettings, Pipe, - TriggerCallback, TriggerContainer, TriggerGroup, } from "./types"; @@ -16,23 +13,19 @@ import { * Pipes input events to action callbacks. */ export class InputWritr { + // TODO correct types.ts comments too /** - * Converts between character aliases and their key strings. - */ - public readonly aliasConverter: AliasConverter; - - /** - * A mapping of events to their key codes, to their callbacks. + * Maps event types to their key codes, to their callbacks. */ private readonly triggers: TriggerContainer; /** - * Known, allowed aliases for triggers. + * Maps event types to their lists of aliases. */ private readonly aliases: Aliases; /** - * An optional Boolean callback to disable or enable input triggers. + * Determines whether events are allowed to be called. */ private readonly canTrigger: CanTrigger; @@ -42,195 +35,64 @@ export class InputWritr { * @param settings Settings to be used for initialization. */ public constructor(settings: InputWritrSettings = {}) { + this.aliases = {}; + this.canTrigger = settings.canTrigger ?? (() => true); this.triggers = settings.triggers ?? {}; - if ("canTrigger" in settings) { - this.canTrigger = - typeof settings.canTrigger === "function" - ? settings.canTrigger - : () => settings.canTrigger as boolean; - } else { - this.canTrigger = () => true; + if (settings.aliases) { + for (const aliasName in settings.aliases) { + this.addEventAliasValues(aliasName, settings.aliases[aliasName]); + } } - - this.aliases = {}; - this.aliasConverter = new AliasConverter(this.aliases); - this.addAliases(settings.aliases ?? {}); } /** * Adds a list of values by which an event may be triggered. * - * @param name The name of the event that is being given aliases, - * such as "left". - * @param values An array of aliases by which the event will also - * be callable. + * @param eventType Event type to be aliased, such as "keyDownLeft". + * @param values Aliases by which the event will also be callable, such as [37, 65]. */ - public addAliasValues(name: string, values: (number | string)[]): void { - if (!this.aliases[name]) { - this.aliases[name] = values; + public addEventAliasValues(eventType: string, values: (number | string)[]): void { + if (this.aliases[eventType]) { + this.aliases[eventType].push(...values); } else { - this.aliases[name].push.apply(this.aliases[name], values); - } - - // TriggerName = "onkeydown", "onkeyup", ... - for (const triggerName in this.triggers) { - // TriggerGroup = { "left": function, ... }, ... - const triggerGroup: TriggerGroup = this.triggers[triggerName]; - - if (triggerGroup[name]) { - // Values[i] = 37, 65, ... - for (const value of values) { - triggerGroup[value] = triggerGroup[name]; - } - } - } - } - - /** - * Removes a list of values by which an event may be triggered. - * - * @param name The name of the event that is having aliases removed, - * such as "left". - * @param values Aliases by which the event will no longer be callable. - */ - public removeAliasValues(name: string, values: (number | string)[]): void { - if (!this.aliases[name]) { - return; + this.aliases[eventType] = values; } - for (const value of values) { - this.aliases[name].splice(this.aliases[name].indexOf(value, 1)); - } - - // TriggerName = "onkeydown", "onkeyup", ... + // triggerName: "onkeydown", "onkeyup", ... for (const triggerName in this.triggers) { - // TriggerGroup = { "left": function, ... }, ... - const triggerGroup: TriggerGroup = this.triggers[triggerName]; + // triggerGroup: { "keyDownLeft": function, ... }, ... + const triggerGroup = this.triggers[triggerName]; - if (triggerGroup[name]) { - // Values[i] = 37, 65, ... + if (triggerGroup[eventType]) { for (const value of values) { - if (triggerGroup[value]) { - delete triggerGroup[value]; - } + triggerGroup[value] = triggerGroup[eventType]; } } } } /** - * Shortcut to remove old alias values and add new ones in. - * - * @param name The name of the event that is having aliases - * added and removed, such as "left". - * @param valuesOld An array of aliases by which the event will no - * longer be callable. - * @param valuesNew An array of aliases by which the event will - * now be callable. - */ - public switchAliasValues( - name: string, - valuesOld: (number | string)[], - valuesNew: (number | string)[] - ): void { - this.removeAliasValues(name, valuesOld); - this.addAliasValues(name, valuesNew); - } - - /** - * Adds a set of aliases from an Object containing "name" => [values] pairs. - * - * @param aliasesRaw Aliases to be added via this.addAliasValues. - */ - public addAliases(aliasesRaw: Record): void { - for (const aliasName in aliasesRaw) { - this.addAliasValues(aliasName, aliasesRaw[aliasName]); - } - } - - /** - * Adds a triggerable event by marking a new callback under the trigger's - * triggers. Any aliases for the label are also given the callback. + * Calls a triggered event under the key code, if it exists. * - * @param trigger The name of the triggered event. - * @param label The code within the trigger to call within, - * typically either a character code or an alias. - * @param callback The callback Function to be triggered. - */ - public addEvent(trigger: string, label: string, callback: TriggerCallback): void { - if (!this.triggers[trigger]) { - throw new Error(`Unknown trigger requested: '${trigger}'.`); - } - - this.triggers[trigger][label] = callback; - - if (this.aliases[label]) { - for (const alias of this.aliases[label]) { - this.triggers[trigger][alias] = callback; - } - } - } - - /** - * Removes a triggerable event by deleting any callbacks under the trigger's - * triggers. Any aliases for the label are also given the callback. - * - * @param trigger The name of the triggered event. - * @param label The code within the trigger to call within, - * typically either a character code or an alias. - */ - public removeEvent(trigger: string, label: string): void { - if (!this.triggers[trigger]) { - throw new Error(`Unknown trigger requested: '${trigger}'.`); - } - - delete this.triggers[trigger][label]; - - if (this.aliases[label]) { - for (const alias of this.aliases[label]) { - if (this.triggers[trigger][alias]) { - delete this.triggers[trigger][alias]; - } - } - } - } - - /** - * Primary driver function to run a triggers event. - * - * @param eventRaw The event function (or string alias thereof) to call. - * @param keyCode The alias of the event Function under triggers[event], - * if event is a string. + * @param eventAlias The aliased name of the event to call. + * @param keyCode The alias of the event Function under triggers[event]. * @param sourceEvent The raw event that caused the calling Pipe * to be triggered, such as a MouseEvent. * @returns The result of calling the triggered event. */ - public callEvent( - eventRaw: Function | string, - keyCode?: number | string, - sourceEvent?: Event - ): any { - if (!eventRaw) { - throw new Error("Blank event given to InputWritr."); - } - - if (!this.canTrigger(eventRaw, keyCode, sourceEvent)) { - return; + public callEvent(eventRaw: string, keyCode: number | string, sourceEvent?: Event): any { + if (this.canTrigger(eventRaw, keyCode, sourceEvent)) { + return this.triggers[eventRaw]?.[keyCode as string]?.(sourceEvent); } - - const event = - typeof eventRaw === "string" ? this.triggers[eventRaw][keyCode as string] : eventRaw; - - return event(sourceEvent); } /** * Creates and returns a pipe to run a trigger. * - * @param trigger The label for the array of functions that the + * @param type The label for the array of functions that the * pipe function should choose from. - * @param codeLabel A mapping string for the alias to get the + * @param eventKey A mapping string for the alias to get the * alias from the event. * @param preventDefaults Whether the input to the pipe Function * will be an DOM-style event, where @@ -238,24 +100,53 @@ export class InputWritr { * @returns A Function that, when called on an event, runs this.callEvent * on the appropriate trigger event. */ - public makePipe(trigger: string, codeLabel: string, preventDefaults?: boolean): Pipe { - const functions: TriggerGroup = this.triggers[trigger]; + public createPipe(type: string, eventKey: string, preventDefaults?: boolean): Pipe { + const functions = this.triggers[type]; if (!functions) { - throw new Error(`No trigger of label '${trigger}' defined.`); + throw new Error(`No trigger of type '${type}' defined.`); } return (event: Event): void => { - const alias: number | string = (event as any)[codeLabel]; + const alias = (event as unknown as Record)[eventKey]; // Typical usage means alias will be an event from a key/mouse input if (preventDefaults && event.preventDefault instanceof Function) { event.preventDefault(); } - // If there's a Function under that alias, run it - if (functions[alias]) { - this.callEvent(functions[alias], alias, event); - } + this.callEvent(type, alias, event); }; } + + /** + * Removes a list of values by which an event may be triggered. + * + * @param name The name of the event that is having aliases removed, + * such as "left". + * @param values Aliases by which the event will no longer be callable. + */ + public removeEventAliasValues(name: string, values: (number | string)[]): void { + if (!this.aliases[name]) { + return; + } + + for (const value of values) { + this.aliases[name].splice(this.aliases[name].indexOf(value, 1)); + } + + // TriggerName = "onkeydown", "onkeyup", ... + for (const triggerName in this.triggers) { + // TriggerGroup = { "left": function, ... }, ... + const triggerGroup: TriggerGroup = this.triggers[triggerName]; + + if (triggerGroup[name]) { + // Values[i] = 37, 65, ... + for (const value of values) { + if (triggerGroup[value]) { + delete triggerGroup[value]; + } + } + } + } + } } diff --git a/packages/inputwritr/src/index.ts b/packages/inputwritr/src/index.ts index 9e9c39288..eb397eea5 100644 --- a/packages/inputwritr/src/index.ts +++ b/packages/inputwritr/src/index.ts @@ -1,3 +1,2 @@ -export * from "./AliasConverter"; export * from "./InputWritr"; export * from "./types"; diff --git a/packages/inputwritr/src/types.ts b/packages/inputwritr/src/types.ts index 61b630d4f..21ee2e82c 100644 --- a/packages/inputwritr/src/types.ts +++ b/packages/inputwritr/src/types.ts @@ -13,10 +13,7 @@ export type TriggerContainer = Record; /** * A mapping of key codes to callbacks. */ -export interface TriggerGroup { - [i: string]: TriggerCallback; - [j: number]: TriggerCallback; -} +export type TriggerGroup = Record; /** * Known, allowed aliases for triggers. @@ -26,7 +23,7 @@ export type Aliases = Record; /** * Determines whether triggering is possible for an event. * - * @param event The event function (or string alias thereof) to call. + * @param eventType The event type to call. * @param keyCode The alias of the event Function under triggers[event], * if event is a string. * @param sourceEvent The raw event that caused the calling Pipe @@ -34,7 +31,7 @@ export type Aliases = Record; * @returns Whether triggering is possible. */ export type CanTrigger = ( - event: Function | string, + eventType: string, keyCode?: number | string, sourceEvent?: Event ) => boolean; @@ -83,5 +80,5 @@ export interface InputWritrSettings { /** * Whether events are initially allowed to trigger (by default, true). */ - canTrigger?: boolean | CanTrigger; + canTrigger?: CanTrigger; } diff --git a/packages/userwrappr/src/getAliasesAsKeyStrings.ts b/packages/userwrappr/src/getAliasesAsKeyStrings.ts new file mode 100644 index 000000000..4cbcfa0e5 --- /dev/null +++ b/packages/userwrappr/src/getAliasesAsKeyStrings.ts @@ -0,0 +1,64 @@ +import { Aliases, AliasKeys } from "inputwritr"; + +const keyCodesToAliases: Record = { + 8: "backspace", + 13: "enter", + 16: "shift", + 17: "ctrl", + 27: "escape", + 32: "space", + 37: "left", + 38: "up", + 39: "right", + 40: "down", +}; + +/** + * @param alias The alias of an input, typically a character code. + * @returns The human-readable String representing the input name, + * such as "a" or "left". + */ +const convertAliasToKeyString = (alias: number | string) => { + if (typeof alias === "string") { + return alias; + } + + if (alias > 96 && alias < 105) { + return String.fromCharCode(alias - 48); + } + + if (alias > 64 && alias < 97) { + return String.fromCharCode(alias); + } + + return typeof keyCodesToAliases[alias] !== "undefined" ? keyCodesToAliases[alias] : "?"; +}; + +/** + * Determines the allowed key strings for a given alias. + * + * @param aliases Maps event types to their lists of aliases. + * @param alias An alias allowed to be passed in, typically a + * character code. + * @returns The mapped key Strings corresponding to that alias, + * typically the human-readable Strings representing + * input names, such as "a" or "left". + */ +const getAliasAsKeyStrings = (aliases: Aliases, alias: string) => { + return aliases[alias].map((aliases) => convertAliasToKeyString(aliases)); +}; + +/** + * @param aliases Maps event types to their lists of aliases. + * @returns The stored mapping of aliases to values, with values + * mapped to their equivalent key Strings. + */ +export const getAliasesAsKeyStrings = (aliases: Aliases) => { + const output: AliasKeys = {}; + + for (const alias in aliases) { + output[alias] = getAliasAsKeyStrings(aliases, alias); + } + + return output; +}; diff --git a/packages/userwrappr/src/index.ts b/packages/userwrappr/src/index.ts index 98bf03d82..7175f9d1d 100644 --- a/packages/userwrappr/src/index.ts +++ b/packages/userwrappr/src/index.ts @@ -1,3 +1,4 @@ +export * from "./getAliasesAsKeyStrings"; export * from "./UserWrappr"; export { MenuSchema } from "./Menus/MenuSchemas"; export * from "./Menus/Options/OptionSchemas";