summaryrefslogtreecommitdiff
path: root/assets/js/site.js
diff options
context:
space:
mode:
Diffstat (limited to 'assets/js/site.js')
-rw-r--r--assets/js/site.js415
1 files changed, 415 insertions, 0 deletions
diff --git a/assets/js/site.js b/assets/js/site.js
new file mode 100644
index 0000000..39891b7
--- /dev/null
+++ b/assets/js/site.js
@@ -0,0 +1,415 @@
+(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;
+ })();
+})();