zcbot/web/static/js/dialog.js

116 lines
4.3 KiB
JavaScript

// 页内弹框原语:替代原生 alert/confirm/prompt。分两类(对标 Element 的 dialog / message):
// - dialogConfirm / dialogPrompt:需要交互(确认 / 输入)→ 遮罩 + 卡片 modal,复用 .modal/.card。
// - message:单向提示(成功 / 报错 / 信息)→ 顶部居中轻量 toast,自动消失。
// 复用改密码 modal 那套头/体/脚布局;单例容器 #app-dialog 每次调用重建内容,同一时刻只开一个。
import { $ } from "./dom.js";
import { escapeHtml } from "./format.js";
// ───── 交互式 modal(confirm / prompt)─────
let _resolve = null; // 当前打开弹框的 resolve
let _onKey = null; // 当前绑定的 keydown 处理器(关闭时解绑)
function closeDialog(result) {
const wrap = $("app-dialog");
wrap.classList.remove("show");
wrap.innerHTML = "";
if (_onKey) { document.removeEventListener("keydown", _onKey, true); _onKey = null; }
const r = _resolve; _resolve = null;
if (r) r(result);
}
// 内部通用打开:kind = "confirm" | "prompt"
function openDialog(kind, opts) {
const {
title = "",
message = "",
label = "",
value = "",
placeholder = "",
okText = "确认",
cancelText = "取消",
danger = false,
} = opts || {};
// 若已有弹框未关(极少见:非阻塞下重复触发),先把旧的按取消收掉
if (_resolve) closeDialog(kind === "prompt" ? null : false);
const wrap = $("app-dialog");
const isPrompt = kind === "prompt";
const bodyHtml = isPrompt
? `${label ? `<label for="app-dialog-input">${escapeHtml(label)}</label>` : ""}
<input id="app-dialog-input" type="text" autocomplete="off" />`
: `<div class="msg">${escapeHtml(message)}</div>`;
wrap.innerHTML = `
<div class="card" role="dialog" aria-modal="true">
${title ? `<h3>${escapeHtml(title)}</h3>` : ""}
<div class="body">${bodyHtml}</div>
<div class="actions">
<button id="app-dialog-cancel">${escapeHtml(cancelText)}</button>
<button id="app-dialog-ok" class="primary${danger ? " danger" : ""}">${escapeHtml(okText)}</button>
</div>
</div>`;
wrap.classList.add("show");
const inp = isPrompt ? $("app-dialog-input") : null;
if (inp) { inp.value = value; inp.placeholder = placeholder; }
const ok = () => closeDialog(isPrompt ? (inp.value) : true);
const cancel = () => closeDialog(isPrompt ? null : false);
$("app-dialog-ok").onclick = ok;
$("app-dialog-cancel").onclick = cancel;
wrap.onclick = (e) => { if (e.target === wrap) cancel(); }; // 点遮罩 = 取消
_onKey = (e) => {
if (e.key === "Escape") { e.stopPropagation(); cancel(); }
else if (e.key === "Enter" && (!isPrompt || e.target === inp)) { e.preventDefault(); ok(); }
};
document.addEventListener("keydown", _onKey, true);
// 聚焦:输入框选中已有文字(方便重命名整段改),否则聚焦确认键
if (inp) { inp.focus(); inp.select(); }
else $("app-dialog-ok").focus();
return new Promise((res) => { _resolve = res; });
}
// 确认框 → Promise<boolean>
export function dialogConfirm(opts) {
return openDialog("confirm", typeof opts === "string" ? { message: opts } : opts);
}
// 输入框 → Promise<string|null>(取消返回 null;确认返回原始输入,调用方自行 trim)
export function dialogPrompt(opts) {
return openDialog("prompt", opts);
}
// 供 main.js Esc 栈查询 / 关闭(与其它 modal 统一)
export function isDialogOpen() { return !!_resolve; }
export function closeTopDialog() { if (_resolve) closeDialog(false); }
// ───── 单向提示 toast(message)─────
// type: "info" | "success" | "error";顶部居中,自动消失。
export function message(text, type = "info", duration = 3200) {
let wrap = $("app-messages");
if (!wrap) {
wrap = document.createElement("div");
wrap.id = "app-messages";
document.body.appendChild(wrap);
}
const el = document.createElement("div");
el.className = "app-msg " + type;
el.textContent = text;
wrap.appendChild(el);
// 触发进入动画
requestAnimationFrame(() => el.classList.add("in"));
const kill = () => {
el.classList.remove("in");
el.addEventListener("transitionend", () => el.remove(), { once: true });
setTimeout(() => el.remove(), 400); // 兜底(transition 未触发时)
};
setTimeout(kill, duration);
el.onclick = kill; // 点一下立刻消
}