Files
FSE-Ticket.sys/web/custom-dialog.js
T
2026-06-21 10:00:13 +08:00

201 lines
7.5 KiB
JavaScript

(function () {
if (window.appDialog) return;
const state = {
root: null,
panel: null,
title: null,
message: null,
field: null,
input: null,
cancel: null,
confirm: null,
lastFocused: null,
queue: Promise.resolve()
};
function ensureRoot() {
if (state.root) return;
const root = document.createElement('div');
root.className = 'tm-dialog-root';
root.hidden = true;
root.innerHTML = [
'<div class="tm-dialog-backdrop" data-dialog-close="cancel"></div>',
'<div class="tm-dialog-panel" role="dialog" aria-modal="true" aria-labelledby="tmDialogTitle">',
' <div class="tm-dialog-kicker">FSE RAILWAY</div>',
' <h3 class="tm-dialog-title" id="tmDialogTitle">系统提示</h3>',
' <div class="tm-dialog-message"></div>',
' <label class="tm-dialog-field" hidden>',
' <span class="tm-dialog-field-label">输入内容</span>',
' <input class="tm-dialog-input" type="text" />',
' </label>',
' <div class="tm-dialog-actions">',
' <button type="button" class="btn tm-dialog-cancel">取消</button>',
' <button type="button" class="btn primary tm-dialog-confirm">确定</button>',
' </div>',
'</div>'
].join('');
document.body.appendChild(root);
state.root = root;
state.panel = root.querySelector('.tm-dialog-panel');
state.title = root.querySelector('.tm-dialog-title');
state.message = root.querySelector('.tm-dialog-message');
state.field = root.querySelector('.tm-dialog-field');
state.input = root.querySelector('.tm-dialog-input');
state.cancel = root.querySelector('.tm-dialog-cancel');
state.confirm = root.querySelector('.tm-dialog-confirm');
}
function whenReady() {
if (document.body) return Promise.resolve();
return new Promise((resolve) => {
document.addEventListener('DOMContentLoaded', resolve, { once: true });
});
}
function normalizeOptions(type, value, fallbackValue) {
if (value && typeof value === 'object' && !Array.isArray(value)) {
return {
type,
title: value.title || (type === 'confirm' ? '请确认' : type === 'prompt' ? '请输入内容' : '系统提示'),
message: value.message || '',
confirmText: value.confirmText || (type === 'alert' ? '知道了' : '确定'),
cancelText: value.cancelText || '取消',
defaultValue: value.defaultValue == null ? '' : String(value.defaultValue),
placeholder: value.placeholder || ''
};
}
return {
type,
title: type === 'confirm' ? '请确认' : type === 'prompt' ? '请输入内容' : '系统提示',
message: value == null ? '' : String(value),
confirmText: type === 'alert' ? '知道了' : '确定',
cancelText: '取消',
defaultValue: fallbackValue == null ? '' : String(fallbackValue),
placeholder: ''
};
}
function setOpen(open) {
ensureRoot();
state.root.hidden = !open;
state.root.classList.toggle('is-open', open);
}
async function showDialog(options) {
await whenReady();
ensureRoot();
return new Promise((resolve) => {
setOpen(true);
state.lastFocused = document.activeElement instanceof HTMLElement ? document.activeElement : null;
state.title.textContent = options.title;
state.message.textContent = options.message;
state.confirm.textContent = options.confirmText;
state.cancel.textContent = options.cancelText;
const isPrompt = options.type === 'prompt';
const showCancel = options.type !== 'alert';
state.field.hidden = !isPrompt;
state.input.value = options.defaultValue || '';
state.input.placeholder = options.placeholder || '';
state.cancel.hidden = !showCancel;
let settled = false;
const close = (result, shouldRestoreFocus = true) => {
if (settled) return;
settled = true;
document.removeEventListener('keydown', onKeydown, true);
state.root.removeEventListener('click', onRootClick, true);
setOpen(false);
if (shouldRestoreFocus && state.lastFocused && typeof state.lastFocused.focus === 'function') {
window.setTimeout(() => state.lastFocused.focus(), 0);
}
resolve(result);
};
const onKeydown = (event) => {
if (event.key === 'Escape') {
event.preventDefault();
if (options.type === 'alert') close(undefined);
else close(null);
return;
}
if (event.key === 'Enter') {
const target = event.target;
if (target === state.cancel) return;
event.preventDefault();
if (isPrompt) close(state.input.value);
else if (options.type === 'confirm') close(true);
else close(undefined);
}
};
const onRootClick = (event) => {
const action = event.target && event.target.getAttribute && event.target.getAttribute('data-dialog-close');
if (action === 'cancel') {
if (options.type === 'alert') close(undefined);
else close(null);
return;
}
if (event.target === state.cancel) {
close(null);
return;
}
if (event.target === state.confirm) {
if (isPrompt) close(state.input.value);
else if (options.type === 'confirm') close(true);
else close(undefined);
}
};
document.addEventListener('keydown', onKeydown, true);
state.root.addEventListener('click', onRootClick, true);
window.setTimeout(() => {
if (isPrompt) state.input.focus();
else state.confirm.focus();
}, 0);
}).then((result) => {
if (options.type === 'confirm') return result === true;
if (options.type === 'prompt') return result == null ? null : String(result);
return undefined;
});
}
function enqueue(task) {
state.queue = state.queue.then(task, task);
return state.queue;
}
const api = {
alert(message) {
return enqueue(() => showDialog(normalizeOptions('alert', message)));
},
confirm(message) {
return enqueue(() => showDialog(normalizeOptions('confirm', message)));
},
prompt(message, defaultValue) {
return enqueue(() => showDialog(normalizeOptions('prompt', message, defaultValue)));
}
};
window.appDialog = api;
window.alert = function (message) {
return api.alert(message);
};
window.confirm = function (message) {
return api.confirm(message);
};
window.prompt = function (message, defaultValue) {
return api.prompt(message, defaultValue);
};
})();