docs(design): 新增 UI demo 页面,供 UI 精修评审
- docs/design/ui-demo/:控件总览、表单、卡片、列表与表格,以及工作台、读标两个组合页 - 只用设计规范的变量和组件;新增组件样式放在 demo.css,清单见 index - index 列出 UI 精修待定问题 Q1–Q12 - design/README 与 AGENTS.md 进度同步 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
275
docs/design/ui-demo/demo.js
Normal file
275
docs/design/ui-demo/demo.js
Normal file
@@ -0,0 +1,275 @@
|
||||
/* 标博士 · UI demo 公共脚本:图标、页面框架、轻提示、对话框、下拉、日期、开关、标签页、排序。
|
||||
* 只演示交互,不连后端。正式实现里这些交互件由 Reka UI 打底(AR10)。 */
|
||||
(function () {
|
||||
const $ = (s, el = document) => el.querySelector(s);
|
||||
const $$ = (s, el = document) => [...el.querySelectorAll(s)];
|
||||
const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
|
||||
|
||||
/* Lucide 图标(ISC 许可)路径 */
|
||||
const ICONS = {
|
||||
grid: '<rect x="3" y="3" width="7" height="9" rx="1"/><rect x="14" y="3" width="7" height="5" rx="1"/><rect x="14" y="12" width="7" height="9" rx="1"/><rect x="3" y="16" width="7" height="5" rx="1"/>',
|
||||
folder: '<path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/>',
|
||||
cert: '<rect x="3" y="4" width="18" height="16" rx="2"/><path d="M7 9h10M7 13h6"/><circle cx="16" cy="15" r="2"/>',
|
||||
gauge: '<path d="m12 14 4-4"/><path d="M3.34 19a10 10 0 1 1 17.32 0"/>',
|
||||
bell: '<path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/>',
|
||||
help: '<circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/>',
|
||||
x: '<path d="M18 6 6 18"/><path d="m6 6 12 12"/>',
|
||||
plus: '<path d="M5 12h14"/><path d="M12 5v14"/>',
|
||||
upload: '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m17 8-5-5-5 5"/><path d="M12 3v12"/>',
|
||||
download: '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m7 10 5 5 5-5"/><path d="M12 15V3"/>',
|
||||
file: '<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M10 9H8M16 13H8M16 17H8"/>',
|
||||
search: '<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>',
|
||||
"chev-down": '<path d="m6 9 6 6 6-6"/>',
|
||||
"chev-left": '<path d="m15 18-6-6 6-6"/>',
|
||||
"chev-right": '<path d="m9 18 6-6-6-6"/>',
|
||||
sort: '<path d="m7 15 5 5 5-5"/><path d="m7 9 5-5 5 5"/>',
|
||||
"arrow-up": '<path d="m5 12 7-7 7 7"/><path d="M12 19V5"/>',
|
||||
"arrow-down": '<path d="M12 5v14"/><path d="m19 12-7 7-7-7"/>',
|
||||
calendar: '<rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/>',
|
||||
check: '<path d="M20 6 9 17l-5-5"/>',
|
||||
more: '<circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/>',
|
||||
filter: '<path d="M22 3H2l8 9.46V19l4 2v-8.54L22 3z"/>',
|
||||
inbox: '<path d="M22 12h-6l-2 3h-4l-2-3H2"/><path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/>',
|
||||
clock: '<circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/>',
|
||||
trash: '<path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>',
|
||||
pencil: '<path d="M21.17 6.81a1 1 0 0 0-3.98-3.98L3.84 16.17a2 2 0 0 0-.5.83l-1.32 4.35a.5.5 0 0 0 .62.62l4.35-1.32a2 2 0 0 0 .83-.5z"/>',
|
||||
history: '<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M12 7v5l4 2"/>',
|
||||
keyboard: '<rect x="2" y="4" width="20" height="16" rx="2"/><path d="M6 8h.01M10 8h.01M14 8h.01M18 8h.01M8 12h.01M12 12h.01M16 12h.01M7 16h10"/>',
|
||||
refresh: '<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M8 16H3v5"/>',
|
||||
send: '<path d="M14.54 21.69a.5.5 0 0 0 .94-.03l6.5-19a.5.5 0 0 0-.64-.64l-19 6.5a.5.5 0 0 0-.03.94l7.93 3.18a2 2 0 0 1 1.11 1.11z"/><path d="m21.85 2.15-10.94 10.94"/>',
|
||||
list: '<path d="m3 17 2 2 4-4"/><path d="m3 7 2 2 4-4"/><path d="M13 6h8M13 12h8M13 18h8"/>',
|
||||
paperclip: '<path d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 18 8.84l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48"/>',
|
||||
book: '<path d="M12 7v14"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/>',
|
||||
archive: '<rect x="2" y="3" width="20" height="5" rx="1"/><path d="M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8"/><path d="M10 12h4"/>',
|
||||
layers: '<path d="m12.83 2.18 8.58 3.91a1 1 0 0 1 0 1.83l-8.58 3.9a2 2 0 0 1-1.66 0L2.6 7.92a1 1 0 0 1 0-1.83l8.58-3.9a2 2 0 0 1 1.66 0Z"/><path d="m22 12-9.17 4.17a2 2 0 0 1-1.66 0L2 12"/><path d="m22 17-9.17 4.17a2 2 0 0 1-1.66 0L2 17"/>'
|
||||
};
|
||||
const icon = (n, cls = "") => `<svg class="ic ${cls}" viewBox="0 0 24 24" aria-hidden="true">${ICONS[n] || ""}</svg>`;
|
||||
function hydrate(root = document) {
|
||||
$$("i[data-ic]", root).forEach((el) => { el.outerHTML = icon(el.dataset.ic, el.className); });
|
||||
}
|
||||
|
||||
/* ---------- demo 顶部导航(控件页) ---------- */
|
||||
const PAGES = [
|
||||
["index.html", "目录"], ["controls.html", "① 控件"], ["forms.html", "② 表单"], ["cards.html", "③ 卡片"],
|
||||
["lists.html", "④ 列表与表格"], ["page-workbench.html", "⑤ 工作台"], ["page-reading.html", "⑥ 读标"]
|
||||
];
|
||||
function demoTop(cur) {
|
||||
const h = $("[data-demo-top]");
|
||||
if (!h) return;
|
||||
h.className = "d-top";
|
||||
h.innerHTML = `<a class="brand" href="index.html"><img src="../logo/svg/logo-horizontal.svg" alt="标博士"></a><span class="tag">UI demo</span>
|
||||
<nav class="d-nav" aria-label="demo 页面">${PAGES.map(([u, t]) => `<a href="${u}" class="${u === cur ? "on" : ""}">${t}</a>`).join("")}</nav>`;
|
||||
}
|
||||
|
||||
/* ---------- 产品页面框架(组合页) ---------- */
|
||||
function shell(opt) {
|
||||
const nav = [["page-workbench.html", "grid", "工作台", "workbench"], ["#", "cert", "资质库", "quals"], ["#", "gauge", "额度", "quota"]]
|
||||
.map(([h, i, t, k]) => `<a href="${h}" class="${opt.nav === k ? "on" : ""}" ${opt.nav === k ? 'aria-current="page"' : ""}>${icon(i, "lg")}${t}</a>`).join("");
|
||||
const crumbs = (opt.crumbs || []).map((c, i, a) => i < a.length - 1 ? `<a href="${c[1]}">${c[0]}</a><span>/</span>` : `<b>${c[0]}</b>`).join("");
|
||||
document.body.classList.add("app");
|
||||
document.body.insertAdjacentHTML("afterbegin", `<aside class="sidebar">
|
||||
<a class="brand" href="page-workbench.html"><img src="../logo/svg/logo-horizontal.svg" alt="标博士"></a>
|
||||
<nav class="nav" aria-label="主导航">${nav}<div class="sub">UI demo</div><a href="index.html">${icon("book", "lg")}demo 目录</a></nav>
|
||||
<div class="sidebar-foot"><b>王敏</b>南京某信息技术有限公司<br>专业版 · 至 2027-10-31</div></aside>`);
|
||||
const main = $("main");
|
||||
main.classList.add("main");
|
||||
main.insertAdjacentHTML("afterbegin", `<header class="top"><div class="crumbs">${crumbs}</div>
|
||||
<div class="top-right">
|
||||
<a class="quota-chip tip-below" href="#" data-tip="本期剩余额度">解析<span class="fig">37</span>检查<span class="fig">12</span></a>
|
||||
<button class="icon-btn tip-below" aria-label="通知" data-tip="通知" data-notice>${icon("bell", "lg")}<span class="dot"></span></button>
|
||||
<button class="icon-btn tip-below tip-left" aria-label="快捷键说明" data-tip="快捷键说明(?)" data-keys>${icon("keyboard", "lg")}</button>
|
||||
</div></header>`);
|
||||
$("[data-notice]").onclick = () => toast("解析完成:补遗 1 已并入清单(新增 2 条,更新 1 条)。");
|
||||
$("[data-keys]").onclick = keysDialog;
|
||||
}
|
||||
function keysDialog() {
|
||||
dialog({ title: "快捷键", cancel: null, ok: "知道了",
|
||||
body: `<table class="data"><tbody>
|
||||
<tr><td><span class="kbd">J</span> / <span class="kbd">K</span></td><td>下一条 / 上一条</td></tr>
|
||||
<tr><td><span class="kbd">O</span> 或 <span class="kbd">Enter</span></td><td>在原文查看器里打开当前出处</td></tr>
|
||||
<tr><td><span class="kbd">1</span> <span class="kbd">2</span> <span class="kbd">3</span></td><td>确认无误 / 已修改 / 忽略(3 会打开原因输入框)</td></tr>
|
||||
<tr><td><span class="kbd">U</span></td><td>撤销当前条的处理</td></tr>
|
||||
<tr><td><span class="kbd">?</span></td><td>显示本说明</td></tr></tbody></table>` });
|
||||
}
|
||||
|
||||
/* ---------- 轻提示:4 秒消失,悬停暂停 ---------- */
|
||||
function toast(msg, opt = {}) {
|
||||
let box = $(".toasts");
|
||||
if (!box) { box = document.createElement("div"); box.className = "toasts"; box.setAttribute("role", "status"); box.setAttribute("aria-live", "polite"); document.body.appendChild(box); }
|
||||
const t = document.createElement("div");
|
||||
t.className = "toast";
|
||||
t.innerHTML = `${opt.ok === false ? "" : '<span class="ok" aria-hidden="true">✓</span>'}<span>${msg}</span>${opt.undo ? '<span class="sep"></span><button class="lnk" data-undo>撤销</button>' : ""}`;
|
||||
box.appendChild(t);
|
||||
let left = 4000, start = Date.now(), timer = setTimeout(() => t.remove(), left);
|
||||
t.addEventListener("mouseenter", () => { clearTimeout(timer); left -= Date.now() - start; });
|
||||
t.addEventListener("mouseleave", () => { start = Date.now(); timer = setTimeout(() => t.remove(), Math.max(left, 1200)); });
|
||||
if (opt.undo) $("[data-undo]", t).onclick = () => { t.remove(); opt.undo(); };
|
||||
}
|
||||
|
||||
/* ---------- 对话框:Esc 关闭、焦点回到触发处 ---------- */
|
||||
function dialog({ title, body, ok = "确定", cancel = "取消", wide = false, onOk, onOpen, footLeft = "" }) {
|
||||
const back = document.activeElement;
|
||||
const s = document.createElement("div");
|
||||
s.className = "scrim";
|
||||
s.innerHTML = `<div class="dialog ${wide ? "wide" : ""}" role="dialog" aria-modal="true" aria-labelledby="dlg-t">
|
||||
<h2 id="dlg-t">${title}</h2><button class="icon-btn sm x" aria-label="关闭" data-x>${icon("x")}</button>
|
||||
<div class="body">${body}</div>
|
||||
<div class="foot">${footLeft ? `<span class="left">${footLeft}</span>` : ""}${cancel ? `<button class="btn" data-cancel>${cancel}</button>` : ""}<button class="btn primary" data-ok>${ok}</button></div></div>`;
|
||||
document.body.appendChild(s);
|
||||
document.body.style.overflow = "hidden";
|
||||
const close = () => { s.remove(); document.body.style.overflow = ""; document.removeEventListener("keydown", onKey, true); if (back && back.focus) back.focus(); };
|
||||
const onKey = (e) => {
|
||||
if (e.key === "Escape") { e.stopPropagation(); close(); }
|
||||
if (e.key === "Tab") { // 焦点锁定在对话框内
|
||||
const f = $$("button:not([disabled]), input:not([disabled]), textarea, select, a[href]", s);
|
||||
if (!f.length) return;
|
||||
if (e.shiftKey && document.activeElement === f[0]) { e.preventDefault(); f[f.length - 1].focus(); }
|
||||
else if (!e.shiftKey && document.activeElement === f[f.length - 1]) { e.preventDefault(); f[0].focus(); }
|
||||
}
|
||||
};
|
||||
document.addEventListener("keydown", onKey, true);
|
||||
s.addEventListener("mousedown", (e) => { if (e.target === s) close(); });
|
||||
$("[data-x]", s).onclick = close;
|
||||
if (cancel) $("[data-cancel]", s).onclick = close;
|
||||
$("[data-ok]", s).onclick = () => { if (!onOk || onOk(s) !== false) close(); };
|
||||
hydrate(s); init(s);
|
||||
if (onOpen) onOpen(s);
|
||||
const f = $("textarea, input:not([type=checkbox]):not([type=radio]), [data-ok]", s);
|
||||
if (f) f.focus();
|
||||
return s;
|
||||
}
|
||||
|
||||
/* ---------- 下拉选择(listbox)与下拉菜单 ---------- */
|
||||
function closePops(except) {
|
||||
$$(".pop.open").forEach((p) => { if (p !== except) { p.classList.remove("open"); const t = $("[aria-expanded]", p); if (t) t.setAttribute("aria-expanded", "false"); const m = $(".menu,.cal", p); if (m) m.hidden = true; } });
|
||||
}
|
||||
function initSelect(p) {
|
||||
const trg = $(".select, [data-menu-trigger]", p), menu = $(".menu", p);
|
||||
if (!trg || !menu || p.dataset.ready) return;
|
||||
p.dataset.ready = 1;
|
||||
menu.hidden = true;
|
||||
const opts = () => $$('[role=option]:not([aria-disabled="true"]), [role=menuitem]:not([aria-disabled="true"])', menu);
|
||||
let ai = -1;
|
||||
const mark = (i) => { opts().forEach((o, k) => o.classList.toggle("is-active", k === i)); ai = i; const o = opts()[i]; if (o) o.scrollIntoView({ block: "nearest" }); };
|
||||
const open = () => { closePops(p); p.classList.add("open"); menu.hidden = false; trg.setAttribute("aria-expanded", "true"); const sel = opts().findIndex((o) => o.getAttribute("aria-selected") === "true"); mark(sel < 0 ? 0 : sel); };
|
||||
const close = () => { p.classList.remove("open"); menu.hidden = true; trg.setAttribute("aria-expanded", "false"); };
|
||||
const pick = (o) => {
|
||||
if (o.getAttribute("role") === "option") {
|
||||
$$("[role=option]", menu).forEach((x) => x.setAttribute("aria-selected", x === o ? "true" : "false"));
|
||||
const v = $(".val", trg); if (v) v.innerHTML = o.dataset.label || o.firstChild.textContent;
|
||||
p.closest(".field")?.classList.remove("err");
|
||||
p.dispatchEvent(new CustomEvent("change", { detail: o.dataset.value || o.textContent }));
|
||||
} else if (o.dataset.toast) toast(o.dataset.toast);
|
||||
close(); trg.focus();
|
||||
};
|
||||
trg.addEventListener("click", (e) => { e.stopPropagation(); p.classList.contains("open") ? close() : open(); });
|
||||
trg.addEventListener("keydown", (e) => {
|
||||
const n = opts().length;
|
||||
if (["ArrowDown", "ArrowUp"].includes(e.key)) { e.preventDefault(); if (!p.classList.contains("open")) return open(); mark((ai + (e.key === "ArrowDown" ? 1 : n - 1)) % n); }
|
||||
else if (e.key === "Enter" || e.key === " ") { if (p.classList.contains("open") && ai >= 0) { e.preventDefault(); pick(opts()[ai]); } }
|
||||
else if (e.key === "Escape" && p.classList.contains("open")) { e.stopPropagation(); close(); }
|
||||
});
|
||||
menu.addEventListener("click", (e) => { const o = e.target.closest('[role=option], [role=menuitem]'); if (o && o.getAttribute("aria-disabled") !== "true") { e.stopPropagation(); pick(o); } });
|
||||
}
|
||||
|
||||
/* ---------- 日期选择 ---------- */
|
||||
const WD = ["一", "二", "三", "四", "五", "六", "日"];
|
||||
function calHTML(y, m, sel, opt = {}) {
|
||||
const first = new Date(y, m, 1), start = (first.getDay() + 6) % 7, days = new Date(y, m + 1, 0).getDate();
|
||||
const prevDays = new Date(y, m, 0).getDate();
|
||||
let cells = "";
|
||||
for (let i = 0; i < 42; i++) {
|
||||
const d = i - start + 1;
|
||||
let dd, out = false, mm = m;
|
||||
if (d < 1) { dd = prevDays + d; out = true; mm = m - 1; } else if (d > days) { dd = d - days; out = true; mm = m + 1; } else dd = d;
|
||||
const date = new Date(y, mm, dd), key = `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`;
|
||||
const cls = [out ? "out" : "", key === opt.today ? "today" : "", (opt.marks || []).includes(key) ? "mark" : ""].join(" ");
|
||||
cells += `<button type="button" class="${cls}" data-d="${key}" aria-selected="${key === sel}" ${opt.min && date < opt.min ? "disabled" : ""} tabindex="-1">${dd}</button>`;
|
||||
if (i === 34 && d >= days) break;
|
||||
}
|
||||
return `<div class="cal-h"><button type="button" class="icon-btn sm" data-cm="-1" aria-label="上个月">${icon("chev-left")}</button><b><span class="fig">${y}</span> 年 <span class="fig">${m + 1}</span> 月</b><button type="button" class="icon-btn sm" data-cm="1" aria-label="下个月">${icon("chev-right")}</button></div>
|
||||
<div class="cal-grid" role="grid">${WD.map((w) => `<span class="wd">${w}</span>`).join("")}${cells}</div>
|
||||
<div class="cal-f">${opt.time ? `时间 <input class="input time" value="${opt.time}" aria-label="时间">` : '<span>点圆点的日期是本项目的关键日期</span>'}<button type="button" class="btn sm text" data-today>今天</button></div>`;
|
||||
}
|
||||
function initDate(p) {
|
||||
if (p.dataset.ready) return;
|
||||
p.dataset.ready = 1;
|
||||
const trg = $(".select", p), cal = $(".cal", p);
|
||||
let [y, m] = (p.dataset.month || "2026-11").split("-").map(Number); m -= 1;
|
||||
let sel = p.dataset.value || "";
|
||||
const opt = { today: "2026-11-3", marks: (p.dataset.marks || "").split(",").filter(Boolean), time: p.dataset.time, min: p.dataset.min ? new Date(...p.dataset.min.split("-").map((v, i) => i === 1 ? v - 1 : +v)) : null };
|
||||
const render = () => { cal.innerHTML = calHTML(y, m, sel, opt); };
|
||||
const fmt = (k) => { const [a, b, c] = k.split("-"); return `${a}-${String(b).padStart(2, "0")}-${String(c).padStart(2, "0")}`; };
|
||||
render();
|
||||
if (!p.classList.contains("d-open")) cal.hidden = true;
|
||||
trg?.addEventListener("click", (e) => { e.stopPropagation(); const o = !p.classList.contains("open"); closePops(p); p.classList.toggle("open", o); cal.hidden = !o; trg.setAttribute("aria-expanded", o); });
|
||||
cal.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
const b = e.target.closest("button"); if (!b) return;
|
||||
if (b.dataset.cm) { m += +b.dataset.cm; if (m < 0) { m = 11; y--; } if (m > 11) { m = 0; y++; } render(); return; }
|
||||
if (b.dataset.today) { y = 2026; m = 10; sel = opt.today; }
|
||||
else if (b.dataset.d) sel = b.dataset.d;
|
||||
else return;
|
||||
render();
|
||||
if (trg) { $(".val", trg).innerHTML = fmt(sel); p.closest(".field")?.classList.remove("err"); if (!p.classList.contains("d-open")) { p.classList.remove("open"); cal.hidden = true; trg.setAttribute("aria-expanded", "false"); trg.focus(); } }
|
||||
p.dispatchEvent(new CustomEvent("change", { detail: fmt(sel) }));
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------- 开关、分段、标签页、半选、排序、加载按钮 ---------- */
|
||||
function init(root = document) {
|
||||
$$(".pop[data-select], .pop[data-menu]", root).forEach(initSelect);
|
||||
$$(".pop[data-date]", root).forEach(initDate);
|
||||
$$(".switch:not([data-ready])", root).forEach((s) => {
|
||||
s.dataset.ready = 1;
|
||||
const upd = () => { const st = $(".st", s); if (st) st.textContent = s.getAttribute("aria-checked") === "true" ? "开" : "关"; };
|
||||
upd();
|
||||
s.addEventListener("click", () => { if (s.disabled) return; s.setAttribute("aria-checked", s.getAttribute("aria-checked") === "true" ? "false" : "true"); upd(); });
|
||||
});
|
||||
$$(".seg:not([data-ready])", root).forEach((g) => {
|
||||
g.dataset.ready = 1;
|
||||
g.addEventListener("click", (e) => { const b = e.target.closest("button"); if (!b) return; $$("button", g).forEach((x) => x.setAttribute("aria-pressed", x === b)); g.dispatchEvent(new CustomEvent("change", { detail: b.dataset.v })); });
|
||||
});
|
||||
$$("[role=tablist]:not([data-ready])", root).forEach((tl) => {
|
||||
tl.dataset.ready = 1;
|
||||
const tabs = $$("[role=tab]", tl);
|
||||
const sel = (t) => { tabs.forEach((x) => { const on = x === t; x.setAttribute("aria-selected", on); x.tabIndex = on ? 0 : -1; const pn = x.getAttribute("aria-controls"); if (pn) document.getElementById(pn).hidden = !on; }); };
|
||||
tabs.forEach((t) => t.addEventListener("click", () => { if (t.getAttribute("aria-disabled") !== "true") sel(t); }));
|
||||
tl.addEventListener("keydown", (e) => {
|
||||
const i = tabs.indexOf(document.activeElement); if (i < 0) return;
|
||||
const d = e.key === "ArrowRight" ? 1 : e.key === "ArrowLeft" ? -1 : 0; if (!d) return;
|
||||
e.preventDefault(); let j = i; do { j = (j + d + tabs.length) % tabs.length; } while (tabs[j].getAttribute("aria-disabled") === "true");
|
||||
tabs[j].focus(); sel(tabs[j]);
|
||||
});
|
||||
});
|
||||
$$("input[data-indeterminate]", root).forEach((c) => { c.indeterminate = true; });
|
||||
$$("[data-loading]:not([data-ready])", root).forEach((b) => {
|
||||
b.dataset.ready = 1;
|
||||
b.addEventListener("click", () => {
|
||||
if (b.classList.contains("is-loading")) return;
|
||||
b.classList.add("is-loading"); b.setAttribute("aria-busy", "true");
|
||||
setTimeout(() => { b.classList.remove("is-loading"); b.removeAttribute("aria-busy"); if (b.dataset.loading) toast(b.dataset.loading); }, 2200);
|
||||
});
|
||||
});
|
||||
}
|
||||
document.addEventListener("click", () => closePops());
|
||||
document.addEventListener("keydown", (e) => { if (e.key === "Escape") closePops(); });
|
||||
|
||||
/* 可排序表格:th[data-sort] 里放 button.th-sort;按 data-sort 指定的列取 td 的 data-v 或文字 */
|
||||
function sortable(table, onSort) {
|
||||
$$("th[data-sort]", table).forEach((th) => {
|
||||
const b = $(".th-sort", th);
|
||||
b.insertAdjacentHTML("beforeend", icon("sort"));
|
||||
b.addEventListener("click", () => {
|
||||
const dir = th.getAttribute("aria-sort") === "ascending" ? "descending" : "ascending";
|
||||
$$("th[aria-sort]", table).forEach((x) => { x.removeAttribute("aria-sort"); $("svg", x).outerHTML = icon("sort"); });
|
||||
th.setAttribute("aria-sort", dir);
|
||||
$("svg", th).outerHTML = icon(dir === "ascending" ? "arrow-up" : "arrow-down");
|
||||
onSort(th.dataset.sort, dir);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
window.Demo = { $, $$, esc, icon, hydrate, demoTop, shell, toast, dialog, init, sortable, keysDialog, calHTML };
|
||||
document.addEventListener("DOMContentLoaded", () => { hydrate(); init(); });
|
||||
})();
|
||||
Reference in New Issue
Block a user