1(() => { 2 const trigger = document.querySelector(".medal"); 3 if (!trigger) return; 4 5 // JSON: { gold: [], silver: [], bronze: [] } 6 // Each entry: { medalists: ["鏿å§å"], event: "女ååé«è³½", sport: "è»ç¶²" } 7 // Entries and names are displayed in their JSON array order. 8 const dialog = document.createElement("dialog"); 9 dialog.id = "medallists-dialog"; 10 dialog.className = "medallists-dialog"; 11 dialog.setAttribute("aria-labelledby", "medallists-title"); 12 dialog.innerHTML = ` 13 <div class="medallists-header"> 14 <h2 id="medallists-title">ä¸è¯éççæ¦</h2> 15 <button class="medallists-close" type="button" aria-label="ééççæ¦" autofocus>Ã</button> 16 </div> 17 <div class="medallists-content" aria-live="polite" aria-busy="false"></div> 18 `; 19 document.body.append(dialog); 20 const content = dialog.querySelector(".medallists-content"); 21 let request; 22 let previousOverflow; 23 let refreshTimer; 24 25 function render(data) { 26 const fragment = document.createDocumentFragment(); 27 for (const [key, label] of [["gold", "éç"], ["silver", "éç"], ["bronze", "é ç"]]) { 28 if (!Array.isArray(data[key])) throw new Error(`Invalid medallists: ${key}`); 29 const section = document.createElement("section"); 30 const heading = document.createElement("h3"); 31 heading.textContent = label; 32 section.append(heading); 33 const table = document.createElement("table"); 34 table.setAttribute("aria-label", `${label}å¾çåå®`); 35 table.innerHTML = "<thead><tr><th scope=\"col\">鏿</th><th scope=\"col\">è³½äº</th><th scope=\"col\">é ç®</th></tr></thead>"; 36 const body = document.createElement("tbody"); 37 const entries = data[key]; 38 for (const entry of entries) { 39 const row = document.createElement("tr"); 40 const names = document.createElement("td"); 41 const athletes = Array.isArray(entry.medalists) ? entry.medalists : [entry.medalists]; 42 for (const athlete of athletes) { 43 const name = document.createElement("span"); 44 name.textContent = athlete || "â"; 45 names.append(name); 46 } 47 row.append(names); 48 for (const value of [entry.event, entry.sport]) { 49 const cell = document.createElement("td"); 50 cell.textContent = value || "â"; 51 row.append(cell); 52 } 53 body.append(row); 54 } 55 if (!entries.length) { 56 const row = document.createElement("tr"); 57 const cell = document.createElement("td"); 58 cell.colSpan = 3; 59 cell.className = "medallists-empty"; 60 cell.textContent = "ç®åå°ç¡å¾çåå®"; 61 row.append(cell); 62 body.append(row); 63 } 64 table.append(body); 65 section.append(table); 66 fragment.append(section); 67 } 68 content.replaceChildren(fragment); 69 } 70 71 async function refresh() { 72 request?.abort(); 73 content.setAttribute("aria-busy", "true"); 74 const controller = new AbortController(); 75 request = controller; 76 try { 77 const apiBase = (window.MEDALLISTS_API_BASE || "").replace(/\/$/, ""); 78 const dataBase = (window.SITE_DATA_BASE || "").replace(/\/$/, ""); 79 const response = await fetch(dataBase ? `${dataBase}/medallists.json` : apiBase ? `${apiBase}/api/medallists` : "./json/medallists.json", { 80 cache: "no-store", 81 signal: AbortSignal.any([controller.signal, AbortSignal.timeout(15000)]), 82 }); 83 if (!response.ok) throw new Error(`Medallists request failed: ${response.status}`); 84 const data = await response.json(); 85 if (request === controller) render(data); 86 } catch (error) { 87 if (error.name !== "AbortError" && request === controller) { 88 content.textContent = "æ«æç¡æ³è¼å ¥ææ°å¾çåå®ï¼è«ééå¾éæ°éåï¼æç¨åèªåé試ã"; 89 console.error(error); 90 } 91 } finally { 92 if (request === controller) content.setAttribute("aria-busy", "false"); 93 } 94 } 95 96 async function open() { 97 if (dialog.open) return; 98 previousOverflow = document.body.style.overflow; 99 document.body.style.overflow = "hidden"; 100 dialog.showModal(); 101 content.textContent = "åå®è¼å ¥ä¸â¦"; 102 content.scrollTop = 0; 103 if (window.SITE_DATA_BASE || window.MEDALLISTS_API_BASE) refreshTimer = setInterval(() => { 104 if (!document.hidden && dialog.open) refresh(); 105 }, 30000); 106 await refresh(); 107 } 108 109 trigger.addEventListener("click", open); 110 trigger.addEventListener("keydown", (event) => { 111 if (event.key === "Enter" || event.key === " ") { 112 event.preventDefault(); 113 open(); 114 } 115 }); 116 dialog.querySelector(".medallists-close").addEventListener("click", () => dialog.close()); 117 dialog.addEventListener("close", () => { 118 clearInterval(refreshTimer); 119 request?.abort(); 120 request = null; 121 document.body.style.overflow = previousOverflow; 122 trigger.focus({ preventScroll: true }); 123 }); 124})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.