Files
rea/website/public/assets/home.js
T
Kaoru0822andClaude Opus 5.5 dabcd783d5 refactor(website): keep page text when a script message is missing
A page that omits a message attribute (a translation that drops one, or a
page without the shared footer) must not show empty or "undefined" text:

- Copy buttons still copy without #copy-status or its messages, keep their
  own label and leave the status empty.
- The star label is skipped when the header has no label for the count.
- The percent demo, dinosaur game and speed check leave the current text in
  place for a missing message; an unknown {placeholder} stays literal.

scripts/test-website-scripts.mjs runs main.js, home.js and dino-demo.js
against a small document model with node:test. `website.py check` runs it,
so the website CI job and the publisher's preparation job now set up the
repository's Node.js version.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 07:09:16 +00:00

150 lines
5.1 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.
function initializePercentDemo() {
const demo = document.querySelector("[data-percent-demo]");
if (demo === null) return;
const display = demo.querySelector("[data-percent-display]");
const expression = demo.querySelector("[data-percent-expression]");
const status = demo.querySelector("[data-percent-status]");
const equals = demo.querySelector('[data-percent-key="equals"]');
// The page owns the localized messages; {name} marks an inserted value.
// A missing message leaves the current text in place.
const message = (name, values = {}) =>
demo.dataset[name]?.replace(/\{(\w+)\}/g, (match, key) =>
String(values[key] ?? match),
);
const show = (element, text) => {
if (text) element.textContent = text;
};
const operations = demo.querySelectorAll("[data-percent-operation]");
let operation = "add";
let stage = "input";
const render = () => {
const adding = operation === "add";
const previous = 200;
const current = 10;
const percent = adding ? (current * previous) / 100 : current / 100;
const result = adding ? previous + percent : previous * percent;
const operator = adding ? "+" : "×";
expression.textContent =
stage === "result" ? `200 ${operator} ${percent} =` : `200 ${operator}`;
display.textContent = String(
stage === "input" ? current : stage === "percent" ? percent : result,
);
equals.disabled = stage !== "percent";
show(
status,
stage === "input"
? message("msgInput")
: stage === "percent"
? message(adding ? "msgPercentAdd" : "msgPercentMultiply")
: message("msgResult", { operator, percent, result }),
);
};
operations.forEach((button) => {
button.addEventListener("click", () => {
operation = button.getAttribute("data-percent-operation");
stage = "input";
operations.forEach((candidate) => {
candidate.setAttribute("aria-pressed", String(candidate === button));
});
render();
});
});
demo.querySelectorAll("[data-percent-key]").forEach((button) => {
button.addEventListener("click", () => {
const key = button.getAttribute("data-percent-key");
stage =
key === "reset" ? "input" : key === "percent" ? "percent" : "result";
render();
});
});
demo.querySelector(".percent-operation").hidden = false;
demo.querySelector(".percent-controls").hidden = false;
render();
}
function initializeExpertAssembly() {
const excerpt = document.querySelector("[data-expert-assembly]");
if (excerpt && window.matchMedia("(max-width: 640px)").matches) {
excerpt.open = false;
}
}
function initializeHomeNavigation() {
const menu = document.querySelector("[data-home-nav]");
if (menu === null) return;
const currentLabel = menu.querySelector("[data-home-nav-current]");
if (currentLabel === null) return;
const wideScreen = window.matchMedia("(min-width: 1440px)");
const entries = Array.from(menu.querySelectorAll('a[href^="#"]'))
.map((link) => ({
link,
target: document.getElementById(link.getAttribute("href").slice(1)),
}))
.filter((entry) => entry.target !== null);
if (entries.length === 0) return;
const updatePosition = () => {
const offset = wideScreen.matches ? 40 : 88;
let current = entries[0];
entries.forEach((entry) => {
if (entry.target.getBoundingClientRect().top <= offset) current = entry;
});
if (
window.scrollY + window.innerHeight >=
document.documentElement.scrollHeight - 2
) {
const selected = entries.find(
(entry) => entry.link.hash === window.location.hash,
);
const selectedTop = selected?.target.getBoundingClientRect().top;
current =
selectedTop !== undefined &&
selectedTop >= 0 &&
selectedTop < window.innerHeight
? selected
: entries[entries.length - 1];
}
entries.forEach((entry) => {
if (entry === current)
entry.link.setAttribute("aria-current", "location");
else entry.link.removeAttribute("aria-current");
});
currentLabel.textContent = current.link.textContent;
};
const updateLayout = () => {
menu.open = wideScreen.matches;
updatePosition();
};
let scheduled = false;
const schedulePosition = () => {
if (scheduled) return;
scheduled = true;
window.requestAnimationFrame(() => {
scheduled = false;
updatePosition();
});
};
entries.forEach(({ link, target }) => {
link.addEventListener("click", () => {
if (!wideScreen.matches) menu.open = false;
const heading = target.querySelector("h2") ?? target;
heading.setAttribute("tabindex", "-1");
window.requestAnimationFrame(() =>
heading.focus({ preventScroll: true }),
);
});
});
wideScreen.addEventListener("change", updateLayout);
window.addEventListener("scroll", schedulePosition, { passive: true });
window.addEventListener("resize", schedulePosition, { passive: true });
window.addEventListener("hashchange", schedulePosition);
window.addEventListener("load", schedulePosition);
updateLayout();
}
initializeExpertAssembly();
initializePercentDemo();
initializeHomeNavigation();