147 lines
4.0 KiB
TypeScript
147 lines
4.0 KiB
TypeScript
export type ButtonRect = [x: number, y: number, w: number, h: number];
|
|
|
|
export interface ButtonNavigationConfig<T extends string> {
|
|
up?: T | "last";
|
|
down?: T | "last";
|
|
left?: T;
|
|
right?: T;
|
|
horizontalMode?: "navigate" | "preview";
|
|
}
|
|
|
|
export class ButtonNavigation<T extends string> {
|
|
private selectedButton: T;
|
|
private nextButton?: T;
|
|
private buttons: Record<T, ButtonRect>;
|
|
private navigation: Record<T, ButtonNavigationConfig<T>>;
|
|
private onButtonClick?: (buttonName: T) => void;
|
|
private keydownHandler: (event: KeyboardEvent) => void;
|
|
|
|
constructor(config: {
|
|
buttons: Record<T, ButtonRect>;
|
|
initialButton: T;
|
|
navigation: Record<T, ButtonNavigationConfig<T>>;
|
|
onButtonClick?: (buttonName: T) => void;
|
|
}) {
|
|
this.buttons = config.buttons;
|
|
this.selectedButton = config.initialButton;
|
|
this.navigation = config.navigation;
|
|
this.onButtonClick = config.onButtonClick;
|
|
|
|
// TODO: this should be handled by the nds itself i think, and dispatched
|
|
this.keydownHandler = this.handleKeyPress.bind(this);
|
|
window.addEventListener("keydown", this.keydownHandler);
|
|
}
|
|
|
|
public getSelectedButton(): T {
|
|
return this.selectedButton;
|
|
}
|
|
|
|
public getSelectorPosition(): ButtonRect {
|
|
return this.buttons[this.selectedButton];
|
|
}
|
|
|
|
public handleTouch(x: number, y: number): void {
|
|
for (const [buttonName, config] of Object.entries(this.buttons) as [
|
|
T,
|
|
ButtonRect,
|
|
][]) {
|
|
const [sx, sy, sw, sh] = config;
|
|
if (x >= sx && x <= sx + sw && y >= sy && y <= sy + sh) {
|
|
if (this.selectedButton === buttonName) {
|
|
this.onButtonClick?.(buttonName);
|
|
} else {
|
|
if (
|
|
(this.navigation[buttonName].down === "last" &&
|
|
this.navigation[this.selectedButton]!.up === buttonName) ||
|
|
(this.navigation[buttonName].up === "last" &&
|
|
this.navigation[this.selectedButton]!.down === buttonName)
|
|
) {
|
|
this.nextButton = this.selectedButton;
|
|
}
|
|
|
|
this.selectedButton = buttonName;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private handleKeyPress(event: KeyboardEvent): void {
|
|
const currentButton = this.selectedButton;
|
|
const currentNav = this.navigation[currentButton];
|
|
|
|
if (!currentNav) return;
|
|
|
|
switch (event.key) {
|
|
case "ArrowUp":
|
|
if (!currentNav.up) return;
|
|
|
|
if (currentNav.up === "last") {
|
|
if (this.nextButton) {
|
|
this.selectedButton = this.nextButton;
|
|
} else {
|
|
this.selectedButton = (currentNav.left ?? currentNav.right) as T;
|
|
}
|
|
} else {
|
|
if (this.navigation[currentNav.up].down === "last") {
|
|
this.nextButton = this.selectedButton;
|
|
}
|
|
this.selectedButton = currentNav.up;
|
|
}
|
|
|
|
break;
|
|
|
|
case "ArrowDown":
|
|
if (!currentNav.down) return;
|
|
|
|
if (currentNav.down === "last") {
|
|
if (this.nextButton) {
|
|
this.selectedButton = this.nextButton;
|
|
} else {
|
|
this.selectedButton = (currentNav.left ?? currentNav.right) as T;
|
|
}
|
|
} else {
|
|
if (this.navigation[currentNav.down].up === "last") {
|
|
this.nextButton = this.selectedButton;
|
|
}
|
|
this.selectedButton = currentNav.down;
|
|
}
|
|
break;
|
|
|
|
case "ArrowLeft":
|
|
if (!currentNav.left) return;
|
|
|
|
if (currentNav.horizontalMode === "preview") {
|
|
this.nextButton = currentNav.left;
|
|
} else {
|
|
this.selectedButton = currentNav.left;
|
|
}
|
|
break;
|
|
|
|
case "ArrowRight":
|
|
if (!currentNav.right) return;
|
|
|
|
if (currentNav.horizontalMode === "preview") {
|
|
this.nextButton = currentNav.right;
|
|
} else {
|
|
this.selectedButton = currentNav.right;
|
|
}
|
|
break;
|
|
|
|
case "Enter":
|
|
case " ":
|
|
this.onButtonClick?.(this.selectedButton);
|
|
break;
|
|
|
|
default:
|
|
return;
|
|
}
|
|
|
|
event.preventDefault();
|
|
}
|
|
|
|
public destroy(): void {
|
|
window.removeEventListener("keydown", this.keydownHandler);
|
|
}
|
|
}
|