(function () { function byId(id) { return document.getElementById(id); } const menuBtn = byId("menu-toggle"); const nav = byId("site-nav"); const langToggle = byId("lang-toggle"); const langMenu = byId("lang-options"); const currentLang = byId("current-lang"); let openDropdown = null; function closeMenu() { if (!menuBtn || !nav) return; menuBtn.classList.remove("active"); menuBtn.setAttribute("aria-expanded", "false"); nav.hidden = true; if (openDropdown === "menu") openDropdown = null; } function closeLang() { if (!langToggle || !langMenu) return; langToggle.setAttribute("aria-expanded", "false"); langMenu.hidden = true; if (openDropdown === "lang") openDropdown = null; } function toggleMenu(e) { if (!menuBtn || !nav) return; e.stopPropagation(); if (openDropdown === "lang") closeLang(); const open = menuBtn.getAttribute("aria-expanded") === "true"; if (open) { closeMenu(); return; } menuBtn.classList.add("active"); menuBtn.setAttribute("aria-expanded", "true"); nav.hidden = false; openDropdown = "menu"; } function toggleLang(e) { if (!langToggle || !langMenu) return; e.stopPropagation(); if (openDropdown === "menu") closeMenu(); const open = langToggle.getAttribute("aria-expanded") === "true"; if (open) { closeLang(); return; } langToggle.setAttribute("aria-expanded", "true"); langMenu.hidden = false; openDropdown = "lang"; } if (nav) nav.hidden = true; if (langMenu) langMenu.hidden = true; if (menuBtn && nav) { menuBtn.addEventListener("click", toggleMenu); nav.addEventListener("click", (e) => e.stopPropagation()); } if (langToggle && langMenu) { langToggle.addEventListener("click", toggleLang); langMenu.addEventListener("click", (e) => e.stopPropagation()); } if ((menuBtn && nav) || (langToggle && langMenu)) { document.addEventListener("click", function (e) { if ( menuBtn && nav && (menuBtn.contains(e.target) || nav.contains(e.target)) ) { return; } if ( langToggle && langMenu && (langToggle.contains(e.target) || langMenu.contains(e.target)) ) { return; } closeMenu(); closeLang(); }); document.addEventListener("keydown", function (e) { if (e.key === "Escape") { closeMenu(); closeLang(); } }); } if (currentLang) { const docLang = (document.documentElement.lang || "en").toLowerCase(); currentLang.textContent = docLang === "zh" ? "ZH" : docLang === "ja" ? "JA" : "EN"; } if (langMenu) { langMenu.querySelectorAll("a[data-lang]").forEach((a) => { a.addEventListener("click", function () { localStorage.setItem("preferredLang", this.getAttribute("data-lang") || "en"); }); }); } // Language menu hrefs: convert current page to *_zh.html / *_jp.html. // Needs to work for both: // - directory URLs: /foo/ (serve /foo/index.html) // - file URLs: /foo/page.html function updateLangMenuHrefs() { const links = document.querySelectorAll("a[data-lang-href][data-lang]"); if (!links.length) return; let path = window.location.pathname || "/"; if (!path.startsWith("/")) path = "/" + path; const isDir = path.endsWith("/"); const file = isDir ? "index.html" : (path.split("/").pop() || "index.html"); const base = isDir ? path : path.slice(0, -file.length); const baseName = file.replace(/_(zh|jp)\.html$/i, ".html"); const map = { en: baseName, zh: baseName.replace(/(_(zh|jp))?\.html$/i, "_zh.html"), ja: baseName.replace(/(_(zh|jp))?\.html$/i, "_jp.html"), }; links.forEach((a) => { const lang = (a.getAttribute("data-lang") || "en").toLowerCase(); const targetFile = map[lang] || map.en; a.href = base + targetFile; }); } function updateAriaCurrent() { const path = window.location.pathname || "/"; const normalized = path.startsWith("/") ? path : "/" + path; const isDir = normalized.endsWith("/"); const file = isDir ? "index.html" : (normalized.split("/").pop() || "index.html"); const base = isDir ? normalized : normalized.slice(0, -file.length); const indexPath = base + "index.html"; const dirPath = base || "/"; const candidates = new Set([normalized, indexPath, dirPath, dirPath.replace(/\/$/, "")]); document.querySelectorAll("#site-nav a[href]").forEach((a) => { const href = a.getAttribute("href") || ""; const hrefUrl = new URL(href, window.location.origin); if (hrefUrl.origin !== window.location.origin) { a.removeAttribute("aria-current"); return; } const hrefPath = hrefUrl.pathname; if (candidates.has(hrefPath)) { a.setAttribute("aria-current", "page"); } else { a.removeAttribute("aria-current"); } }); const docLang = (document.documentElement.lang || "en").toLowerCase(); document.querySelectorAll("#lang-options a[data-lang]").forEach((a) => { const lang = (a.getAttribute("data-lang") || "en").toLowerCase(); if (lang === docLang) { a.setAttribute("aria-current", "page"); } else { a.removeAttribute("aria-current"); } }); } // Run once now... updateLangMenuHrefs(); updateAriaCurrent(); // ...and again after layout/injection settles. setTimeout(updateLangMenuHrefs, 0); setTimeout(updateAriaCurrent, 0); // Home page blog list (optional) const blogList = byId("blog-list"); if (blogList) { const blogHost = "https://blog.sillylaird.ca"; const ctrl = new AbortController(); const timeout = setTimeout(() => ctrl.abort(), 3500); fetch(blogHost + "/api/posts.php", { signal: ctrl.signal }) .then((r) => r.json()) .then((posts) => { blogList.textContent = ""; const top = document.createElement("div"); const strong = document.createElement("strong"); strong.textContent = "Posts"; top.appendChild(strong); top.appendChild(document.createTextNode(" (")); const allLink = document.createElement("a"); allLink.href = blogHost; allLink.textContent = "all"; top.appendChild(allLink); top.appendChild(document.createTextNode(")")); blogList.appendChild(top); const list = document.createElement("ol"); (posts || []).forEach((post) => { const li = document.createElement("li"); const a = document.createElement("a"); a.href = blogHost + "/" + String(post.path || "").replace(/^\/+/, ""); a.textContent = String(post.title || "Untitled"); const date = document.createElement("div"); date.className = "muted"; const y = String(post.year || ""); const m = String(post.month || "").padStart(2, "0"); const d = String(post.day || "").padStart(2, "0"); const iso = y && m && d ? `${y}-${m}-${d}` : ""; const time = document.createElement("time"); if (iso) time.setAttribute("datetime", iso); time.textContent = iso || ""; li.appendChild(a); li.appendChild(document.createElement("br")); date.appendChild(time); li.appendChild(date); list.appendChild(li); }); blogList.appendChild(list); if (!list.children.length) { const empty = document.createElement("div"); empty.className = "muted"; empty.textContent = "No posts yet."; blogList.appendChild(empty); } }) .catch((err) => { blogList.textContent = err && err.name === "AbortError" ? "Blog list timed out." : "Cannot load posts right now."; }) .finally(() => clearTimeout(timeout)); } const changelogList = byId("changelog-list"); if (changelogList) { const changelogPage = "/changelog/"; const ctrl = new AbortController(); const timeout = setTimeout(() => ctrl.abort(), 3500); fetch(changelogPage, { signal: ctrl.signal }) .then((r) => r.text()) .then((html) => { changelogList.textContent = ""; const doc = new DOMParser().parseFromString(html, "text/html"); const entries = Array.from(doc.querySelectorAll(".changelog-entry")); const top = document.createElement("div"); const strong = document.createElement("strong"); strong.textContent = "Entries"; top.appendChild(strong); top.appendChild(document.createTextNode(" (")); const allLink = document.createElement("a"); allLink.href = "/changelog/"; allLink.textContent = "all"; top.appendChild(allLink); top.appendChild(document.createTextNode(")")); changelogList.appendChild(top); const list = document.createElement("ol"); entries.forEach((entry) => { const li = document.createElement("li"); const titleText = (entry.querySelector("strong, b")?.textContent || "").trim(); const timeEl = entry.querySelector("time"); const dateText = (timeEl?.textContent || "").trim(); const dateIso = (timeEl?.getAttribute("datetime") || "").trim(); const bodyEl = entry.querySelector(".text"); const bodyText = (bodyEl?.textContent || "").trim(); const title = document.createElement("strong"); if (titleText) { title.textContent = titleText; li.appendChild(title); if (dateText) { const date = document.createElement("div"); date.className = "muted"; const time = document.createElement("time"); if (dateIso) time.setAttribute("datetime", dateIso); time.textContent = dateText; date.appendChild(time); li.appendChild(date); } } else if (dateText) { title.textContent = dateText; li.appendChild(title); } if (bodyText) { const body = document.createElement("div"); body.className = "changelog-body"; body.textContent = bodyText; li.appendChild(body); } list.appendChild(li); }); changelogList.appendChild(list); if (!list.children.length) { const empty = document.createElement("div"); empty.className = "muted"; empty.textContent = "No updates yet."; changelogList.appendChild(empty); } }) .catch((err) => { changelogList.textContent = err && err.name === "AbortError" ? "Changelog list timed out." : "Cannot load changelog right now."; }) .finally(() => clearTimeout(timeout)); } const resetBtn = byId("guestbook-reset"); if (resetBtn) { resetBtn.addEventListener("click", () => { const frame = byId("guestbook-form-iframe"); if (!frame) return; setIframeLoading(frame); const src = frame.getAttribute("src") || frame.src; if (src) { // Reset by reloading the iframe URL (works cross-origin too). frame.setAttribute("src", src); return; } if (frame.contentWindow) frame.contentWindow.location.reload(); }); } function setIframeLoading(frame) { if (!frame) return; const clip = frame.closest(".iframe-clip"); if (!clip) return; clip.classList.add("is-loading"); const onLoad = () => { clip.classList.remove("is-loading"); }; frame.addEventListener("load", onLoad, { once: true }); } function resetIframe(frame) { if (!frame) return; const src = frame.getAttribute("src") || frame.src; if (src) { frame.setAttribute("src", src); return; } if (frame.contentWindow) frame.contentWindow.location.reload(); } document.querySelectorAll("button[data-reset-iframe]").forEach((btn) => { btn.addEventListener("click", () => { const key = btn.getAttribute("data-reset-iframe"); if (!key) return; const scope = btn.closest(".iframe-clip") || btn.parentElement || document; const frame = scope.querySelector(`iframe[data-iframe=\"${key}\"]`) || document.querySelector(`iframe[data-iframe=\"${key}\"]`); setIframeLoading(frame); resetIframe(frame); }); }); // Footer year: local time only. (function () { const el = byId("copyright-year"); if (!el) return; el.textContent = String(new Date().getFullYear()); })(); // Footer last-updated: based on document.lastModified. (function () { const el = byId("last-updated"); if (!el) return; const raw = document.lastModified; const date = raw ? new Date(raw) : null; if (!date || Number.isNaN(date.getTime())) return; const iso = date.toISOString().slice(0, 10); el.setAttribute("datetime", iso); el.textContent = iso; })(); })();