PageSourceSearch

https://event.cts.com.tw/2026asiangames/js/medallists.a9b24c1.js

js cts.com.tw collected 2026-09-24 08:41:02 UTC 4,955 bytes, 124 lines download raw bytes

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.