Files
Country-Game---The-Desk/web/js/ui/decision.js
T

149 lines
5.8 KiB
JavaScript

// Centre folder: the in-tray. The oldest waiting proposal is the document on top, answered with
// two rubber stamps; the others show as a pile of sheets under it. Also shows the idle and
// end-of-term messages.
import { t, tx, dateParts } from "../i18n/i18n.js";
import { dateOfDay } from "../engine/index.js";
import { $, esc } from "./desk.js";
const STAMP_MS = 600; // how long the stamp animation plays before the paper leaves the tray
const LEAVE_MS = 320; // and how long the paper takes to slide away
export function createDecision(root, { cardsById, characters, inboxMax = 10, onAnswer, onStamp = () => {}, onProfile, keysEnabled = () => true }) {
root.innerHTML = `
<h2 class="inbox__tab">${t("inbox.title")} <span class="inbox__count" data-count></span></h2>
<p class="inbox__full" role="status" hidden>${t("inbox.full")}</p>
<div class="inbox__body"></div>`;
const body = $(root, ".inbox__body");
const countEl = $(root, "[data-count]");
const fullEl = $(root, ".inbox__full");
let shown = "";
let locked = false;
let latest = null;
let lastCount = 0;
let timers = [];
function choose(choice) {
if (locked) return;
const card = $(body, ".card");
const id = latest?.inbox[0]?.id;
if (!card || !id) return;
locked = true;
card.dataset.stamped = choice;
for (const b of card.querySelectorAll("button")) b.disabled = true;
const stamp = document.createElement("div");
stamp.className = `card__stamp card__stamp--${choice}`;
stamp.textContent = t(choice === "yes" ? "card.approved" : "card.rejected");
card.append(stamp);
onStamp(choice);
timers.push(setTimeout(() => card.classList.add("card--leave"), STAMP_MS));
timers.push(setTimeout(() => onAnswer(choice, id), STAMP_MS + LEAVE_MS));
}
body.addEventListener("click", (e) => {
const b = e.target.closest("button[data-choice]");
if (b) choose(b.dataset.choice);
const p = e.target.closest("button[data-pronouns]");
if (p) onProfile(p.dataset.pronouns);
});
// The panel is rebuilt on language changes and new games, so its key listener must be removable.
const onKey = (e) => {
if (!keysEnabled() || !latest?.inbox.length) return;
if (latest.pending.review || latest.pending.profile || latest.pending.alert) return;
if (e.ctrlKey || e.metaKey || e.altKey) return;
if (e.target.closest?.("input, textarea, select")) return;
const k = e.key.toLowerCase();
if (k === "y") choose("yes");
else if (k === "n") choose("no");
};
document.addEventListener("keydown", onKey);
function option(card, side) {
const label = side === "yes" ? t("card.approved") : t("card.rejected");
return `
<button class="stamp-btn stamp-btn--${side}" data-choice="${side}">
<span class="stamp-btn__label">${esc(tx(card[side].label))}</span>
<span class="stamp-btn__ink">${esc(label)}</span>
</button>`;
}
function cardHtml(card, state) {
const from = characters[card.sender];
const date = dateParts(dateOfDay(state.startDate, state.inbox[0].day).iso);
const paragraphs = tx(card.body).split("\n\n").map((p) => `<p>${esc(p)}</p>`).join("");
return `
<article class="card card--enter card--${esc(card.doc)}" data-pile="${Math.min(3, state.inbox.length - 1)}">
<header class="card__head">
<span class="card__kind">${t("kind." + card.doc)}</span>
<span class="card__date">${esc(date.day)} ${esc(date.month)} ${esc(date.year)}</span>
</header>
<div class="card__from">
<span class="seal">${esc(from.seal)}</span>
<span><b>${esc(tx(from.name))}</b><small>${esc(tx(from.role))}</small></span>
</div>
<h3 class="card__title">${esc(tx(card.title))}</h3>
<div class="card__body">${paragraphs}</div>
<div class="card__choices">${option(card, "yes")}${option(card, "no")}</div>
<p class="card__keys">${t("card.keys")}</p>
</article>`;
}
// The tab counts the proposals waiting; the pile under the top paper grows with them.
function updateTray(state) {
const n = state.inbox.length;
countEl.textContent = n ? `${n}/${inboxMax}` : "";
countEl.classList.toggle("is-warn", n >= inboxMax - 2);
if (n > lastCount) {
countEl.classList.remove("is-bump");
void countEl.offsetWidth; // restart the animation
countEl.classList.add("is-bump");
}
lastCount = n;
fullEl.hidden = !(state.pending.inboxFull && !state.over);
const card = $(body, ".card");
if (card) card.dataset.pile = Math.min(3, Math.max(0, n - 1));
}
function update(state) {
latest = state;
updateTray(state);
let kind = "idle";
if (state.over) kind = "over";
else if (state.pending.profile) kind = "profile";
else if (state.inbox.length) kind = `card:${state.inbox[0].id}`;
if (kind === shown) return;
shown = kind;
locked = false;
timers.forEach(clearTimeout);
timers = [];
body.classList.toggle("is-card", kind.startsWith("card:"));
if (kind === "profile") {
body.innerHTML = `
<h3>${t("profile.title")}</h3>
<p>${t("profile.lead")}</p>
<p class="profile__ask">${t("profile.body")}</p>
<div class="profile__choices">
${["he", "she", "they"].map((p) => `<button class="brass brass--wide" data-pronouns="${p}">${t("profile." + p)}</button>`).join("")}
</div>
<p class="profile__note">${t("profile.note")}</p>`;
} else if (kind === "idle") {
body.innerHTML = `<p class="inbox__idle">${t("inbox.idle")}</p>`;
} else if (kind === "over") {
body.innerHTML = `<h3>${t("inbox.overTitle")}</h3><p>${t("inbox.overBody")}</p>`;
} else {
body.innerHTML = cardHtml(cardsById.get(state.inbox[0].id), state);
}
}
return {
update,
destroy: () => {
timers.forEach(clearTimeout);
document.removeEventListener("keydown", onKey);
},
};
}