dashboard: extract themed dialog into a reusable modal.js component
Move the themed dialog out of common.js into its own modal.js module: a general openDialog(title/message/content/buttons) primitive with themedConfirm as a thin cancel/confirm wrapper on top. tabs.js imports it from there. No behaviour change to the stop-confirm flow; the dialog is now a standalone reusable component other surfaces can open.
This commit is contained in:
parent
03b10d22cd
commit
0407e7da62
3 changed files with 117 additions and 67 deletions
|
|
@ -46,72 +46,6 @@ export const form = (action, btnClass, btnLabel, confirmMsg, extra = {}, opts =
|
|||
return f;
|
||||
};
|
||||
|
||||
// ─── themed confirm modal ───────────────────────────────────────────────
|
||||
// In-theme replacement for the browser's native `confirm()` so destructive
|
||||
// actions match the dashboard chrome instead of a jarring OS dialog. Returns
|
||||
// a Promise resolving to `null` when cancelled, or an object of the checkbox
|
||||
// states (keyed by each checkbox's `name`) when confirmed — `{}` when there
|
||||
// are no checkboxes. Escape, a backdrop click, or Cancel all reject; for a
|
||||
// `danger` action the Cancel button takes initial focus (so a stray Enter
|
||||
// doesn't fire the destructive path). Example:
|
||||
// const r = await themedConfirm({ message: `stop ${n}?`, danger: true,
|
||||
// confirmLabel: '■ stop', checkboxes: [{ name: 'graceful', label: '…' }] });
|
||||
// if (!r) return; // cancelled
|
||||
// doStop(r.graceful);
|
||||
export function themedConfirm(opts = {}) {
|
||||
return new Promise((resolve) => {
|
||||
const {
|
||||
title = '', message = '', danger = false,
|
||||
confirmLabel = 'confirm', cancelLabel = 'cancel', checkboxes = [],
|
||||
} = opts;
|
||||
|
||||
const boxes = checkboxes.map((cb) => {
|
||||
const input = el('input', { type: 'checkbox', class: 'tc-check', name: cb.name });
|
||||
if (cb.checked) input.checked = true;
|
||||
const row = el('label', { class: 'tc-checkrow' },
|
||||
input, el('span', { class: 'tc-check-label' }, cb.label || cb.name));
|
||||
return { name: cb.name, input, row };
|
||||
});
|
||||
|
||||
let settled = false;
|
||||
function done(result) {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
document.removeEventListener('keydown', onKey, true);
|
||||
backdrop.remove();
|
||||
resolve(result);
|
||||
}
|
||||
const cancel = () => done(null);
|
||||
const confirmAction = () => {
|
||||
const out = {};
|
||||
for (const b of boxes) out[b.name] = b.input.checked;
|
||||
done(out);
|
||||
};
|
||||
function onKey(e) {
|
||||
if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); cancel(); }
|
||||
}
|
||||
|
||||
const cancelBtn = el('button', { type: 'button', class: 'btn tc-btn tc-cancel' }, cancelLabel);
|
||||
const okBtn = el('button', {
|
||||
type: 'button', class: 'btn tc-btn tc-confirm' + (danger ? ' tc-danger' : ''),
|
||||
}, confirmLabel);
|
||||
cancelBtn.addEventListener('click', cancel);
|
||||
okBtn.addEventListener('click', confirmAction);
|
||||
|
||||
const box = el('div', { class: 'tc-box', role: 'dialog', 'aria-modal': 'true' },
|
||||
title ? el('div', { class: 'tc-title' }, title) : null,
|
||||
el('div', { class: 'tc-message' }, message),
|
||||
boxes.length ? el('div', { class: 'tc-checks' }, ...boxes.map((b) => b.row)) : null,
|
||||
el('div', { class: 'tc-actions' }, cancelBtn, okBtn));
|
||||
|
||||
const backdrop = el('div', { class: 'tc-backdrop' }, box);
|
||||
backdrop.addEventListener('click', (e) => { if (e.target === backdrop) cancel(); });
|
||||
document.addEventListener('keydown', onKey, true);
|
||||
document.body.append(backdrop);
|
||||
(danger ? cancelBtn : okBtn).focus();
|
||||
});
|
||||
}
|
||||
|
||||
// Page-level submit interceptor for `data-async` forms — the pattern every
|
||||
// dashboard action button uses (meta-update, spawn, cancel, purge, …).
|
||||
// Without this a `data-async` form POSTs natively and the browser navigates
|
||||
|
|
|
|||
Loading…
Reference in a new issue