From b04ba4e62249b8c38c4f9c93b002936e068f7287 Mon Sep 17 00:00:00 2001
From: Tangent Wantwight <tangent128@gmail.com>
Date: Sat, 27 Jan 2024 23:57:36 -0500
Subject: [PATCH] WIP KeyControl event source

---
 lib/keys.ts | 67 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 67 insertions(+)
 create mode 100644 lib/keys.ts

diff --git a/lib/keys.ts b/lib/keys.ts
new file mode 100644
index 0000000..c3b335f
--- /dev/null
+++ b/lib/keys.ts
@@ -0,0 +1,67 @@
+import { Source } from "./source";
+
+export type KeyName = "up" | "down" | "left" | "right" | "a" | "b" | "menu";
+
+const KEY_NAMES: Record<string, KeyName> = {
+  // compact keys (WASD+ZXC)
+  KeyZ: "a",
+  KeyX: "b",
+  KeyC: "menu",
+  KeyW: "up",
+  KeyS: "down",
+  KeyA: "left",
+  KeyD: "right",
+  // full-board keys (arrows+space/shift/enter)
+  Space: "a",
+  ShiftLeft: "b",
+  ShiftRight: "b",
+  Enter: "menu",
+  ArrowUp: "up",
+  ArrowDown: "down",
+  ArrowLeft: "left",
+  ArrowRight: "right",
+};
+
+/** A keypress/release event for an abstract button, or else ["focus", "release"] if for some reason future release events might not be registered. */
+export type KeyEvent = [KeyName | "focus", "press" | "release"];
+
+export function keyControl(source: HTMLElement): Source<KeyEvent> {
+  const tabIndex = source.getAttribute("tabIndex");
+  source.setAttribute(
+    "tabindex",
+    tabIndex == "" || tabIndex == null ? "-1" : tabIndex
+  );
+
+  return ((callback?: (keyEvent: KeyEvent) => void) => {
+    if (callback) {
+      const handle = (evt: KeyboardEvent, action: "press" | "release") => {
+        const keyName = KEY_NAMES[evt.code];
+        if (keyName != undefined) {
+          evt.preventDefault();
+          evt.stopPropagation();
+
+          callback([keyName, action]);
+        }
+      };
+      const keyUp = (evt: KeyboardEvent) => handle(evt, "release");
+      const keyDown = (evt: KeyboardEvent) => handle(evt, "press");
+      const focus = () => callback(["focus", "press"]);
+      const blur = () => callback(["focus", "release"]);
+
+      source.addEventListener("keyup", keyUp, false);
+      source.addEventListener("keydown", keyDown, false);
+      source.addEventListener("focus", focus, false);
+      source.addEventListener("blur", blur, false);
+      source.focus({ focusVisible: true } as FocusOptions);
+
+      return () => {
+        source.removeEventListener("keyup", keyUp, false);
+        source.removeEventListener("keydown", keyDown, false);
+        source.removeEventListener("focus", focus, false);
+        source.removeEventListener("blur", blur, false);
+      };
+    } else {
+      return ["blur", "release"];
+    }
+  }) as Source<KeyEvent>;
+}