Separate menu components

This commit is contained in:
Idrees Hassan
2025-10-26 15:29:36 -04:00
parent ef764153b9
commit 9f15703689
5 changed files with 1046 additions and 646 deletions

View File

@@ -13,6 +13,7 @@ import Frame from './frame.js';
import Layer from './layer.js';
import Anim from './anim.js';
import { StickyNote, createNewStickyNote, drawStickyNotes } from './stickyNotes.js';
import { MenuItem, DebugMenuItem, Separator, insertMenu, removeMenu, isMenuOpen, switchMenuItems } from './menu.js';
// @ts-ignore
const SHARED_CONFIG = {
@@ -271,37 +272,6 @@ Promise.all([
]),
};
class MenuItem {
/**
* @param {string} text
* @param {() => void} action
* @param {boolean} [removeMenu]
* @param {boolean} [isDebug]
*/
constructor(text, action, removeMenu = true, isDebug = false) {
this.text = text;
this.action = action;
this.removeMenu = removeMenu;
this.isDebug = isDebug;
}
}
class DebugMenuItem extends MenuItem {
/**
* @param {string} text
* @param {() => void} action
*/
constructor(text, action, removeMenu = true) {
super(text, action, removeMenu, true);
}
}
class Separator extends MenuItem {
constructor() {
super("", () => { });
}
}
const menuItems = [
new MenuItem(`Pet ${birdBirb()}`, pet),
new MenuItem("Field Guide", insertFieldGuide),
@@ -320,11 +290,11 @@ Promise.all([
debugMode = false;
}),
new Separator(),
new MenuItem("Settings", () => switchMenuItems(settingsItems), false),
new MenuItem("Settings", () => switchMenuItems(MENU_ID, settingsItems, debugMode, updateMenuLocation), false),
];
const settingsItems = [
new MenuItem("Go Back", () => switchMenuItems(menuItems), false),
new MenuItem("Go Back", () => switchMenuItems(MENU_ID, menuItems, debugMode, updateMenuLocation), false),
new Separator(),
new MenuItem("Toggle Birb Mode", () => {
userSettings.birbMode = !userSettings.birbMode;
@@ -529,7 +499,7 @@ Promise.all([
onClick(document, (e) => {
lastActionTimestamp = Date.now();
if (e.target instanceof Node && document.querySelector("#" + MENU_EXIT_ID)?.contains(e.target)) {
removeMenu();
removeMenu(MENU_ID, MENU_EXIT_ID);
}
});
@@ -538,7 +508,7 @@ Promise.all([
// Currently being pet, don't open menu
return;
}
insertMenu();
insertMenu(MENU_ID, MENU_EXIT_ID, menuItems, `${birdBirb().toLowerCase()}OS`, debugMode, updateMenuLocation);
});
canvas.addEventListener("mouseover", () => {
@@ -585,7 +555,7 @@ Promise.all([
// Won't be restored on fullscreen exit
}
if (currentState === States.IDLE && !frozen && !isMenuOpen()) {
if (currentState === States.IDLE && !frozen && !isMenuOpen(MENU_ID)) {
if (Math.random() < HOP_CHANCE && currentAnimation !== Animations.HEART) {
hop();
} else if (Date.now() - lastActionTimestamp > AFK_TIME) {
@@ -835,6 +805,31 @@ Promise.all([
centerElement(modal);
}
/**
* @param {HTMLElement} menu
*/
function updateMenuLocation(menu) {
let x = birdX;
let y = canvas.offsetTop + canvas.height / 2 + WINDOW_PIXEL_SIZE * 10;
const offset = 20;
if (x < window.innerWidth / 2) {
// Left side
x += offset;
} else {
// Right side
x -= (menu.offsetWidth + offset) * UI_CSS_SCALE;
}
if (y > window.innerHeight / 2) {
// Top side
y -= (menu.offsetHeight + offset + 10) * UI_CSS_SCALE;
} else {
// Bottom side
y += offset;
}
menu.style.left = `${x}px`;
menu.style.top = `${y}px`;
};
function insertFieldGuide() {
if (document.querySelector("#" + FIELD_GUIDE_ID)) {
return;
@@ -954,103 +949,6 @@ Promise.all([
save();
}
/**
* Add the menu to the page if it doesn't already exist
*/
function insertMenu() {
if (document.querySelector("#" + MENU_ID)) {
return;
}
let menu = makeElement("birb-window", undefined, MENU_ID);
let header = makeElement("birb-window-header");
header.innerHTML = `<div class="birb-window-title">${birdBirb().toLowerCase()}OS</div>`;
let content = makeElement("birb-window-content");
for (const item of menuItems) {
if (!item.isDebug || debugMode) {
content.appendChild(makeMenuItem(item));
}
}
menu.appendChild(header);
menu.appendChild(content);
document.body.appendChild(menu);
makeDraggable(document.querySelector(".birb-window-header"));
let menuExit = makeElement("birb-window-exit", undefined, MENU_EXIT_ID);
onClick(menuExit, () => {
removeMenu();
});
document.body.appendChild(menuExit);
makeClosable(removeMenu);
updateMenuLocation(menu);
}
/**
* Update the menu's location based on the bird's position
* @param {HTMLElement} menu
*/
function updateMenuLocation(menu) {
let x = birdX;
let y = canvas.offsetTop + canvas.height / 2 + WINDOW_PIXEL_SIZE * 10;
const offset = 20;
if (x < window.innerWidth / 2) {
// Left side
x += offset;
} else {
// Right side
x -= (menu.offsetWidth + offset) * UI_CSS_SCALE;
}
if (y > window.innerHeight / 2) {
// Top side
y -= (menu.offsetHeight + offset + 10) * UI_CSS_SCALE;
} else {
// Bottom side
y += offset;
}
menu.style.left = `${x}px`;
menu.style.top = `${y}px`;
}
/**
* @param {MenuItem[]} menuItems
*/
function switchMenuItems(menuItems) {
const menu = document.querySelector("#" + MENU_ID);
if (!menu || !(menu instanceof HTMLElement)) {
return;
}
const content = menu.querySelector(".birb-window-content");
if (!content) {
error("Content not found");
return;
}
content.innerHTML = "";
for (const item of menuItems) {
if (!item.isDebug || debugMode) {
content.appendChild(makeMenuItem(item));
}
}
updateMenuLocation(menu);
}
/**
* @param {MenuItem} item
* @returns {HTMLElement}
*/
function makeMenuItem(item) {
if (item instanceof Separator) {
return makeElement("birb-window-separator");
}
let menuItem = makeElement("birb-menu-item", item.text);
onClick(menuItem, () => {
if (item.removeMenu) {
removeMenu();
}
item.action();
});
return menuItem;
}
/**
* @param {Document|Element} element
* @param {(e: Event) => void} action
@@ -1076,27 +974,6 @@ Promise.all([
});
}
/**
* Remove the menu from the page
*/
function removeMenu() {
const menu = document.querySelector("#" + MENU_ID);
if (menu) {
menu.remove();
}
const exitMenu = document.querySelector("#" + MENU_EXIT_ID);
if (exitMenu) {
exitMenu.remove();
}
}
/**
* @returns {boolean} Whether the menu element is on the page
*/
function isMenuOpen() {
return document.querySelector("#" + MENU_ID) !== null;
}
/**
* @param {HTMLElement|null} element The element to detect drag events on
* @param {boolean} [parent] Whether to move the parent element when the child is dragged

263
src/menu.js Normal file
View File

@@ -0,0 +1,263 @@
export class MenuItem {
/**
* @param {string} text
* @param {() => void} action
* @param {boolean} [removeMenu]
* @param {boolean} [isDebug]
*/
constructor(text, action, removeMenu = true, isDebug = false) {
this.text = text;
this.action = action;
this.removeMenu = removeMenu;
this.isDebug = isDebug;
}
}
export class DebugMenuItem extends MenuItem {
/**
* @param {string} text
* @param {() => void} action
*/
constructor(text, action, removeMenu = true) {
super(text, action, removeMenu, true);
}
}
export class Separator extends MenuItem {
constructor() {
super("", () => { });
}
}
/**
* Create an HTML element with the specified parameters
* @param {string} className
* @param {string} [textContent]
* @param {string} [id]
* @returns {HTMLElement}
*/
function makeElement(className, textContent, id) {
const element = document.createElement("div");
element.classList.add(className);
if (textContent) {
element.textContent = textContent;
}
if (id) {
element.id = id;
}
return element;
}
/**
* @param {Document|Element} element
* @param {(e: Event) => void} action
*/
function onClick(element, action) {
element.addEventListener("click", (e) => action(e));
element.addEventListener("touchend", (e) => {
if (e instanceof TouchEvent === false) {
return;
} else if (element instanceof HTMLElement === false) {
return;
}
const touch = e.changedTouches[0];
const rect = element.getBoundingClientRect();
if (
touch.clientX >= rect.left &&
touch.clientX <= rect.right &&
touch.clientY >= rect.top &&
touch.clientY <= rect.bottom
) {
action(e);
}
});
}
/**
* @param {HTMLElement|null} element The element to detect drag events on
* @param {boolean} [parent] Whether to move the parent element when the child is dragged
* @param {(top: number, left: number) => void} [callback] Callback for when element is moved
*/
function makeDraggable(element, parent = true, callback = () => { }) {
if (!element) {
return;
}
let isMouseDown = false;
let offsetX = 0;
let offsetY = 0;
let elementToMove = parent ? element.parentElement : element;
if (!elementToMove) {
console.error("Birb: Parent element not found");
return;
}
element.addEventListener("mousedown", (e) => {
isMouseDown = true;
offsetX = e.clientX - elementToMove.offsetLeft;
offsetY = e.clientY - elementToMove.offsetTop;
});
element.addEventListener("touchstart", (e) => {
isMouseDown = true;
const touch = e.touches[0];
offsetX = touch.clientX - elementToMove.offsetLeft;
offsetY = touch.clientY - elementToMove.offsetTop;
e.preventDefault();
});
document.addEventListener("mouseup", (e) => {
if (isMouseDown) {
callback(elementToMove.offsetTop, elementToMove.offsetLeft);
e.preventDefault();
}
isMouseDown = false;
});
document.addEventListener("touchend", (e) => {
if (isMouseDown) {
callback(elementToMove.offsetTop, elementToMove.offsetLeft);
e.preventDefault();
}
isMouseDown = false;
});
document.addEventListener("mousemove", (e) => {
if (isMouseDown) {
elementToMove.style.left = `${Math.max(0, e.clientX - offsetX)}px`;
elementToMove.style.top = `${Math.max(0, e.clientY - offsetY)}px`;
}
});
document.addEventListener("touchmove", (e) => {
if (isMouseDown) {
const touch = e.touches[0];
elementToMove.style.left = `${Math.max(0, touch.clientX - offsetX)}px`;
elementToMove.style.top = `${Math.max(0, touch.clientY - offsetY)}px`;
}
});
}
/**
* @param {() => void} func
* @param {Element} [closeButton]
*/
function makeClosable(func, closeButton) {
if (closeButton) {
onClick(closeButton, func);
}
document.addEventListener("keydown", (e) => {
if (closeButton && !document.body.contains(closeButton)) {
return;
}
if (e.key === "Escape") {
func();
}
});
}
/**
* @param {MenuItem} item
* @param {() => void} removeMenuCallback
* @returns {HTMLElement}
*/
function makeMenuItem(item, removeMenuCallback) {
if (item instanceof Separator) {
return makeElement("birb-window-separator");
}
let menuItem = makeElement("birb-menu-item", item.text);
onClick(menuItem, () => {
if (item.removeMenu) {
removeMenuCallback();
}
item.action();
});
return menuItem;
}
/**
* Add the menu to the page if it doesn't already exist
* @param {string} menuId
* @param {string} menuExitId
* @param {MenuItem[]} menuItems
* @param {string} title
* @param {boolean} debugMode
* @param {(menu: HTMLElement) => void} updateLocationCallback
*/
export function insertMenu(menuId, menuExitId, menuItems, title, debugMode, updateLocationCallback) {
if (document.querySelector("#" + menuId)) {
return;
}
let menu = makeElement("birb-window", undefined, menuId);
let header = makeElement("birb-window-header");
header.innerHTML = `<div class="birb-window-title">${title}</div>`;
let content = makeElement("birb-window-content");
const removeCallback = () => removeMenu(menuId, menuExitId);
for (const item of menuItems) {
if (!item.isDebug || debugMode) {
content.appendChild(makeMenuItem(item, removeCallback));
}
}
menu.appendChild(header);
menu.appendChild(content);
document.body.appendChild(menu);
makeDraggable(document.querySelector(".birb-window-header"));
let menuExit = makeElement("birb-window-exit", undefined, menuExitId);
onClick(menuExit, removeCallback);
document.body.appendChild(menuExit);
makeClosable(removeCallback);
updateLocationCallback(menu);
}
/**
* Remove the menu from the page
* @param {string} menuId
* @param {string} menuExitId
*/
export function removeMenu(menuId, menuExitId) {
const menu = document.querySelector("#" + menuId);
if (menu) {
menu.remove();
}
const exitMenu = document.querySelector("#" + menuExitId);
if (exitMenu) {
exitMenu.remove();
}
}
/**
* @param {string} menuId
* @returns {boolean} Whether the menu element is on the page
*/
export function isMenuOpen(menuId) {
return document.querySelector("#" + menuId) !== null;
}
/**
* @param {string} menuId
* @param {MenuItem[]} menuItems
* @param {boolean} debugMode
* @param {(menu: HTMLElement) => void} updateLocationCallback
*/
export function switchMenuItems(menuId, menuItems, debugMode, updateLocationCallback) {
const menu = document.querySelector("#" + menuId);
if (!menu || !(menu instanceof HTMLElement)) {
return;
}
const content = menu.querySelector(".birb-window-content");
if (!content) {
console.error("Birb: Content not found");
return;
}
content.innerHTML = "";
const removeCallback = () => removeMenu(menuId, menuId + "-exit");
for (const item of menuItems) {
if (!item.isDebug || debugMode) {
content.appendChild(makeMenuItem(item, removeCallback));
}
}
updateLocationCallback(menu);
}