// Live updates: the server pushes an "update" event when data or git changes. // We re-fetch the same server-rendered page and swap the sidebar menu and the // main content, so rendering stays on the server. (() => { const live = document.getElementById("live"); const setState = (state, text) => { if (!live) return; live.dataset.state = state; live.querySelector(".live-text").textContent = text; }; let busy = false; let again = false; // A review form someone is filling in must not be replaced by a live update. const editing = () => document.querySelector("#uc-editor") !== null || document.querySelector("#mock-editor") !== null || document.querySelector("#main form[data-dirty]") !== null || (document.activeElement && document.activeElement.closest && document.activeElement.closest("#main form") !== null); document.addEventListener("input", (e) => { const f = e.target.closest && e.target.closest("#main form"); if (f) f.dataset.dirty = "1"; }); // Remember the reviewer's name in this browser (a per-viewer convenience). const NAME_KEY = "tracker-reviewer"; function fillReviewer() { let name = ""; try { name = localStorage.getItem(NAME_KEY) || ""; } catch {} if (!name) return; document.querySelectorAll('input[name="reviewer"]').forEach((el) => { if (!el.value) el.value = name; }); } document.addEventListener("submit", (e) => { const el = e.target.querySelector && e.target.querySelector('input[name="reviewer"]'); if (el && el.value.trim()) { try { localStorage.setItem(NAME_KEY, el.value.trim()); } catch {} } }); fillReviewer(); async function refresh() { if (busy) { again = true; return; } busy = true; try { const res = await fetch(location.pathname + location.search, { headers: { "X-Live": "1" }, cache: "no-store" }); const html = await res.text(); const next = new DOMParser().parseFromString(html, "text/html"); swap(next, ".nav"); swap(next, "#loaded-at"); const main = document.getElementById("main"); const nextMain = next.getElementById("main"); if (editing()) { // Keep the form; the menu badges above are already updated. setState("open", "มีข้อมูลใหม่ · ฟอร์มและ diagram ยังคงอยู่"); return; } if (main && nextMain) { const open = [...main.querySelectorAll("details[open]")].map(pathOf); // Keep the scroll position of wide areas such as the timeline. const scrolls = [...main.querySelectorAll("[data-keep-scroll][id]")].map((el) => [el.id, el.scrollLeft, el.scrollTop]); const before = main.innerHTML; main.innerHTML = nextMain.innerHTML; scrolls.forEach(([id, l, t]) => { const el = document.getElementById(id); if (el) { el.scrollLeft = l; el.scrollTop = t; } }); document.dispatchEvent(new CustomEvent("tracker:updated")); open.forEach((p) => { const d = byPath(main, p); if (d) d.open = true; }); fillReviewer(); if (before !== main.innerHTML) { main.classList.remove("updated"); void main.offsetWidth; main.classList.add("updated"); } } document.title = next.title; setState("open", "อัปเดตแบบเรียลไทม์"); } catch { setState("closed", "ดึงข้อมูลไม่สำเร็จ จะลองใหม่"); } finally { busy = false; if (again) { again = false; refresh(); } } } function swap(next, selector) { const cur = document.querySelector(selector); const nxt = next.querySelector(selector); if (cur && nxt) cur.replaceWith(nxt); } // Remember which
were open by their position in the document. function pathOf(el) { return [...document.querySelectorAll("#main details")].indexOf(el); } function byPath(root, i) { return root.querySelectorAll("details")[i]; } if (!("EventSource" in window)) { setState("closed", "เบราว์เซอร์นี้ไม่รองรับการอัปเดตอัตโนมัติ"); return; } // Only a visible tab keeps the stream open. Browsers allow about 6 // connections to one site over HTTP/1.1, and every open stream holds one, so // a few background tabs would otherwise make every new page wait. let es = null; let dropped = false; function connect() { if (es) return; es = new EventSource("/events"); es.addEventListener("hello", () => { setState("open", "อัปเดตแบบเรียลไทม์"); if (dropped) { dropped = false; refresh(); } // catch up on changes missed while offline or hidden }); es.addEventListener("update", refresh); es.onerror = () => { dropped = true; setState("closed", "ขาดการเชื่อมต่อ กำลังเชื่อมใหม่"); }; } function disconnect() { if (!es) return; es.close(); es = null; dropped = true; } document.addEventListener("visibilitychange", () => (document.hidden ? disconnect() : connect())); window.addEventListener("pagehide", disconnect); window.addEventListener("pageshow", () => { if (!document.hidden) connect(); }); if (!document.hidden) connect(); })();