tarinai/js/ui_layout_dialogs.js
2026-07-29 20:52:11 +09:00

292 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use strict";
const managedDialogOrigins = new WeakMap();
function managedDialogFocusable(dialog) {
if (!dialog) return [];
return [...dialog.querySelectorAll('button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])')]
.filter(el => !el.hidden && el.getClientRects().length > 0);
}
function openManagedDialog(dialog, opener = document.activeElement) {
if (!dialog) return;
if (opener instanceof HTMLElement && !dialog.contains(opener)) managedDialogOrigins.set(dialog, opener);
if (!dialog.hasAttribute("tabindex")) dialog.tabIndex = -1;
dialog.classList.remove("hidden");
dialog.setAttribute("aria-hidden", "false");
requestAnimationFrame(() => {
const focusables = managedDialogFocusable(dialog);
(focusables[0] || dialog).focus?.({ preventScroll: true });
});
}
function closeManagedDialog(dialog) {
if (!dialog) return;
dialog.classList.add("hidden");
dialog.setAttribute("aria-hidden", "true");
const origin = managedDialogOrigins.get(dialog);
managedDialogOrigins.delete(dialog);
if (origin?.isConnected) requestAnimationFrame(() => origin.focus?.({ preventScroll: true }));
}
function managedDialogVisible(dialog) {
if (!dialog || dialog.hidden || dialog.getAttribute("aria-hidden") === "true") return false;
if (dialog.classList.contains("hidden") || dialog.closest(".hidden")) return false;
return dialog.getClientRects().length > 0;
}
function topVisibleDialog() {
const dialogs = [...document.querySelectorAll('[role="dialog"], [role="alertdialog"]')].filter(managedDialogVisible);
return dialogs[dialogs.length - 1] || null;
}
document.addEventListener("keydown", (event) => {
const dialog = topVisibleDialog();
if (!dialog) return;
if (event.key === "Escape") {
event.preventDefault();
event.stopPropagation();
dialog.dispatchEvent(new CustomEvent("tarinai:dialog-close-request"));
return;
}
if (event.key !== "Tab") return;
const focusables = managedDialogFocusable(dialog);
if (!focusables.length) {
event.preventDefault();
dialog.focus?.();
return;
}
const first = focusables[0];
const last = focusables[focusables.length - 1];
if (event.shiftKey && document.activeElement === first) {
event.preventDefault();
last.focus();
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault();
first.focus();
} else if (!dialog.contains(document.activeElement)) {
event.preventDefault();
first.focus();
}
}, true);
window.TarinaiManagedDialogs = Object.freeze({ open: openManagedDialog, close: closeManagedDialog });
function resizeCanvas() {
const rect = canvas.getBoundingClientRect();
const perfScale = window.TarinaiPerf.dprScale();
const dpr = Math.max(0.5, Math.min((window.devicePixelRatio || 1) * perfScale, 2));
const nextW = Math.max(1, Math.floor(rect.width * dpr));
const nextH = Math.max(1, Math.floor(rect.height * dpr));
if (canvas.width === nextW && canvas.height === nextH && uiCache.canvasDpr === dpr) return;
uiCache.canvasDpr = dpr;
canvas.width = nextW;
canvas.height = nextH;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.fillStyle = "#cfe3d3";
ctx.fillRect(0, 0, Math.max(1, rect.width), Math.max(1, rect.height));
world.setViewportSize(Math.max(1, rect.width), Math.max(1, rect.height), { resizeWorld: true, scaleContents: true });
uiCache.lastChartDraw = "";
if (typeof render === "function") render();
}
function applyFieldLayout(fieldType = world.fieldType || "garden") {
const field = FIELD_TYPES?.[fieldType] || FIELD_TYPES.garden;
const layout = FIELD_LAYOUT;
const root = document.documentElement;
root.style.setProperty("--field-height", layout.height);
root.style.setProperty("--field-min-height", layout.minHeight);
root.style.setProperty("--field-mobile-height", layout.mobileHeight);
root.style.setProperty("--field-mobile-min-height", layout.mobileMinHeight);
world.fieldType = field.id;
const rect = canvas.getBoundingClientRect();
world.setViewportSize(rect.width, rect.height, { resizeWorld: true, scaleContents: true });
resizeCanvas();
}
function normalizeResetPresetId(presetId = "default") {
if (window.TarinaiResetPresets?.normalize) return window.TarinaiResetPresets.normalize(presetId);
return new Set(["empty", "default", "athletic", "crowded", "family", "war", "happy", "hair_trigger"]).has(String(presetId || "")) ? String(presetId) : "default";
}
function syncFieldDialogChoices() {
const fieldType = FIELD_TYPES?.[uiCache.resetFieldType]?.id || world?.fieldType || "garden";
const presetId = normalizeResetPresetId(uiCache.resetPreset || "default");
uiCache.resetFieldType = fieldType;
uiCache.resetPreset = presetId;
ui.fieldDialog?.querySelectorAll("[data-field-type]").forEach((btn) => {
const active = btn.dataset.fieldType === fieldType;
btn.classList.toggle("active", active);
btn.setAttribute("aria-pressed", active ? "true" : "false");
});
ui.fieldDialog?.querySelectorAll("[data-reset-preset]").forEach((btn) => {
const active = btn.dataset.resetPreset === presetId;
btn.classList.toggle("active", active);
btn.setAttribute("aria-pressed", active ? "true" : "false");
});
}
function selectResetFieldType(fieldType = "garden") {
uiCache.resetFieldType = FIELD_TYPES?.[fieldType]?.id || "garden";
syncFieldDialogChoices();
}
function selectResetPreset(presetId = "default") {
uiCache.resetPreset = normalizeResetPresetId(presetId);
syncFieldDialogChoices();
}
function openFieldDialog() {
uiCache.resetFieldType = FIELD_TYPES?.[world?.fieldType]?.id || uiCache.resetFieldType || "garden";
uiCache.resetPreset = normalizeResetPresetId(uiCache.resetPreset || "default");
syncFieldDialogChoices();
openManagedDialog(ui.fieldDialog);
}
function closeFieldDialog() {
closeManagedDialog(ui.fieldDialog);
}
function hydrateLazyImages(root) {
const scope = root || document;
for (const img of scope.querySelectorAll("img[data-src]")) {
if (!img.getAttribute("src")) img.setAttribute("src", img.dataset.src || "");
}
}
function renderEcologyCards() {
if (!ui.ecologyGrid || ui.ecologyGrid.dataset.ready === "1") return;
const cards = window.TEXT_CATALOG?.ecologyCards || [];
ui.ecologyGrid.innerHTML = cards.map(card => `
<section class="ecology-card">
<img data-src="${window.TarinaiUIHelpers.htmlEscape(card.image || "assets/sprites/tarinai_01_smile.webp")}" loading="lazy" decoding="async" alt="">
<div><strong>${window.TarinaiUIHelpers.htmlEscape(card.title || "")}</strong><p>${window.TarinaiUIHelpers.htmlEscape(card.text || "")}</p></div>
</section>`).join("");
ui.ecologyGrid.dataset.ready = "1";
}
function openEcologyDialog() {
renderEcologyCards();
hydrateLazyImages(ui.ecologyDialog);
openManagedDialog(ui.ecologyDialog);
}
function closeEcologyDialog() {
closeManagedDialog(ui.ecologyDialog);
}
function openArchiveResetDialog() {
openManagedDialog(ui.archiveResetDialog);
}
function closeArchiveResetDialog() {
closeManagedDialog(ui.archiveResetDialog);
}
function resetWithField(fieldType, presetId = uiCache.resetPreset || "default") {
const field = FIELD_TYPES?.[fieldType] || FIELD_TYPES.garden;
const preset = normalizeResetPresetId(presetId);
uiCache.resetFieldType = field.id;
uiCache.resetPreset = preset;
closeFieldDialog();
applyFieldLayout(field.id);
world.reset(null, field.id, preset);
uiCache.archiveVersion = "";
renderLog(world.logs);
renderArchive();
renderStats();
render();
}
function resizeFieldFromWheel(e) {
const itemType = toolItemType(world?.tool || "");
if ((e.shiftKey || e.altKey) && itemType && isRotatableItemType(itemType) && world?.rotateToolAngle) {
e.preventDefault();
const step = (e.altKey ? 1 : 5) * Math.PI / 180;
world.rotateToolAngle(itemType, (e.deltaY > 0 ? 1 : -1) * step);
render();
return;
}
if (!world.setFieldZoom) return;
e.preventDefault();
const rect = canvas.getBoundingClientRect();
const sx = e.clientX - rect.left;
const sy = e.clientY - rect.top;
const focus = world.screenToWorld ? world.screenToWorld(sx, sy) : { x: sx, y: sy };
const factor = e.deltaY > 0 ? 1 / 1.08 : 1.08;
if (!world.setFieldZoom((world.fieldZoom || 1) * factor)) return;
const off = world.fieldScreenOffset ? world.fieldScreenOffset() : { x: 0, y: 0 };
const scale = world.viewScale ? world.viewScale() : 1;
world.cameraX = focus.x - (sx - off.x) / scale;
world.cameraY = focus.y - (sy - off.y) / scale;
world.clampCamera?.();
render();
}
function selectTool(tool) {
const wasCopyArmed = world.tool === "copy" && tool === "copy" && Boolean(world.copyBuffer);
const result = window.TarinaiCommands.dispatch(world, { type: "tool.select", toolId: tool });
const activeTool = result?.toolId || world.tool || tool;
if (!["rope", "rod", "spring", "wire", "insulated_wire"].includes(activeTool)) world.pendingLinkEndpoint = null;
if (activeTool !== "copy") world.copyBuffer = null;
else if (wasCopyArmed) {
world.copyBuffer = null;
showToast("\u30b3\u30d4\u30fc\u3092\u89e3\u9664\u3057\u307e\u3057\u305f\u3002");
}
for (const btn of uiCache.toolButtons) {
btn.classList.toggle("selected", btn.dataset.tool === activeTool);
}
syncToolSizeBadges();
canvas.style.cursor = window.TarinaiUIInputShared.cursorForTool(activeTool) || (activeTool === "observe" ? "default" : (activeTool === "pinch" ? "grab" : (activeTool === "poke" || activeTool === "delete" ? "pointer" : "crosshair")));
}
function showToast(text) {
ui.toast.textContent = text;
ui.toast.classList.remove("hidden");
clearTimeout(showToast._timer);
showToast._timer = setTimeout(() => ui.toast.classList.add("hidden"), 1400);
}
function syncTopButtons() {
ui.pauseBtn.textContent = world.paused ? "\u518d\u958b" : "\u4e00\u6642\u505c\u6b62";
if (ui.speedSelect) ui.speedSelect.value = String(world.speed);
if (ui.speedMenuBtn) ui.speedMenuBtn.textContent = `速度 ×${world.speed}`;
for (const option of ui.speedMenuPanel?.querySelectorAll?.("[data-speed-value]") || []) {
const active = Number(option.dataset.speedValue) === Number(world.speed);
option.classList.toggle("active", active);
option.setAttribute("aria-checked", active ? "true" : "false");
}
syncAudioControls();
syncArchiveUpdateButton();
}
function archiveAutoUpdateEnabled() {
return Boolean(uiCache.archiveUpdateEnabled);
}
function syncArchiveUpdateButton() {
const btn = ui.archiveUpdateToggleBtn;
if (!btn) return;
const on = archiveAutoUpdateEnabled();
btn.textContent = on ? "\u66f4\u65b0 ON" : "\u66f4\u65b0 OFF";
btn.classList.toggle("active", on);
btn.setAttribute("aria-pressed", on ? "true" : "false");
}
function setArchiveUpdateEnabled(on) {
uiCache.archiveUpdateEnabled = Boolean(on);
syncArchiveUpdateButton();
if (uiCache.archiveUpdateEnabled) {
uiCache.archiveVersion = "";
uiCache.archiveFamilyVersion = null;
uiCache.archiveViewActivated = true;
world.familyTreeDirty = true;
renderArchive?.({ userRequested: true });
showToast("\u5bb6\u7cfb\u56f3\u306e\u66f4\u65b0\u3092ON\u306b\u3057\u307e\u3057\u305f\u3002");
} else {
if (uiCache.archiveRenderTimer) { clearTimeout(uiCache.archiveRenderTimer); uiCache.archiveRenderTimer = 0; }
uiCache.archiveScheduled = false;
showToast("\u5bb6\u7cfb\u56f3\u306e\u66f4\u65b0\u3092OFF\u306b\u3057\u307e\u3057\u305f\u3002");
}
}