04552aa8f6
The Game status dropdown is now a <game-status-selector> light-DOM custom
element: the Python builder emits the tag + kebab attrs htpy-style, behavior
lives in ts/elements/{dropdown,game-status-selector}.ts wired by the native
connectedCallback, and GameStatusSelectorProps is the codegen'd contract. The
~70-line inline-Alpine f-string is gone.
Also fix SimpleTable to collect and re-attach the media of its row/header
nodes: it stringifies cells into the table markup, which silently dropped each
cell component's declared Media — so a <game-status-selector> in a cell never
got its <script> emitted. Now Page() emits it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
51 lines
1.8 KiB
TypeScript
51 lines
1.8 KiB
TypeScript
export interface DropdownConfig {
|
|
patchUrl: string;
|
|
bodyKey: string; // server field name, e.g. "status" or "device_id"
|
|
event: string; // dispatched on document.body after a successful PATCH
|
|
csrf: string;
|
|
numericValue?: boolean; // parse the option value as a number
|
|
}
|
|
|
|
// Wires a light-DOM value-selector dropdown that lives inside `host`.
|
|
// Markup hooks (rendered server-side): [data-toggle], [data-menu],
|
|
// [data-label], and one or more [data-option][data-value].
|
|
export function initDropdown(host: HTMLElement, config: DropdownConfig): void {
|
|
const toggle = host.querySelector<HTMLElement>("[data-toggle]");
|
|
const menu = host.querySelector<HTMLElement>("[data-menu]");
|
|
const label = host.querySelector<HTMLElement>("[data-label]");
|
|
if (!toggle || !menu || !label) return;
|
|
|
|
const close = () => {
|
|
menu.hidden = true;
|
|
};
|
|
|
|
toggle.addEventListener("click", (event) => {
|
|
event.stopPropagation();
|
|
menu.hidden = !menu.hidden;
|
|
});
|
|
document.addEventListener("click", (event) => {
|
|
if (!host.contains(event.target as Node)) close();
|
|
});
|
|
|
|
host.querySelectorAll<HTMLElement>("[data-option]").forEach((option) => {
|
|
option.addEventListener("click", (event) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
const raw = option.dataset.value ?? "";
|
|
label.innerHTML = option.innerHTML;
|
|
close();
|
|
const body: Record<string, unknown> = {
|
|
[config.bodyKey]: config.numericValue ? Number(raw) : raw,
|
|
};
|
|
window
|
|
.fetchWithHtmxTriggers(config.patchUrl, {
|
|
method: "PATCH",
|
|
headers: { "Content-Type": "application/json", "X-CSRFToken": config.csrf },
|
|
body: JSON.stringify(body),
|
|
})
|
|
.then(() => document.body.dispatchEvent(new CustomEvent(config.event)))
|
|
.catch(() => console.error("Failed to update", config.patchUrl));
|
|
});
|
|
});
|
|
}
|