function esc(s) { const d = document.createElement('div'); d.textContent = s ?? ''; return d.innerHTML; } function formatMessageHTML(message) { return String(message) .split(/\n\n+/) .map((block) => `

${esc(block).replace(/\n/g, '
')}

`) .join(''); } function renderChoicesHTML(choices) { const name = 'confirm_modal_choice'; return `
${choices.map((choice, i) => ` `).join('')}
`; } /** @type {((value: boolean | string | null) => void) | null} */ let resolveConfirm = null; function ensureConfirmModal() { let modal = document.getElementById('confirmModal'); if (modal) return modal; modal = document.createElement('div'); modal.id = 'confirmModal'; modal.className = 'modal-overlay'; modal.hidden = true; modal.setAttribute('role', 'dialog'); modal.setAttribute('aria-modal', 'true'); modal.setAttribute('aria-labelledby', 'confirmModalTitle'); modal.innerHTML = ` `; document.body.appendChild(modal); modal.addEventListener('click', (e) => { if (e.target === modal) finishConfirm(null); }); modal.querySelector('[data-confirm-cancel]').addEventListener('click', () => finishConfirm(null)); modal.querySelector('[data-confirm-submit]').addEventListener('click', () => { const choicesEl = modal.querySelector('#confirmModalChoices'); const hasChoices = choicesEl.childElementCount > 0; if (hasChoices) { const selected = modal.querySelector('input[name="confirm_modal_choice"]:checked'); finishConfirm(selected ? selected.value : null); } else { finishConfirm(true); } }); document.addEventListener('keydown', (e) => { const el = document.getElementById('confirmModal'); if (!el || el.hidden) return; if (e.key === 'Escape') finishConfirm(null); }); return modal; } function finishConfirm(value) { const modal = document.getElementById('confirmModal'); if (!modal || modal.hidden) return; modal.hidden = true; document.body.classList.remove('modal-open'); const resolve = resolveConfirm; resolveConfirm = null; if (resolve) resolve(value); } /** * Show a confirmation dialog. * * Without `choices`: resolves to `true` (confirmed) or `false` (cancelled). * With `choices`: resolves to the selected choice value (string) or `null` (cancelled). * * @param {Object} options * @param {string} options.title * @param {string} options.message * @param {string} [options.confirmLabel='Confirm'] * @param {string} [options.cancelLabel='Cancel'] * @param {'default'|'danger'} [options.variant='default'] * @param {{ value: string, label: string, description?: string }[]} [options.choices] * @returns {Promise} */ export function confirmAction(options) { return new Promise((resolve) => { if (resolveConfirm) { resolve(false); return; } const modal = ensureConfirmModal(); const hasChoices = Array.isArray(options.choices) && options.choices.length > 0; const variant = options.variant === 'danger' ? 'danger' : 'default'; modal.querySelector('#confirmModalTitle').textContent = options.title || 'Confirm'; modal.querySelector('#confirmModalMessage').innerHTML = formatMessageHTML(options.message || ''); const choicesEl = modal.querySelector('#confirmModalChoices'); choicesEl.innerHTML = hasChoices ? renderChoicesHTML(options.choices) : ''; const cancelBtn = modal.querySelector('[data-confirm-cancel]'); const submitBtn = modal.querySelector('[data-confirm-submit]'); cancelBtn.textContent = options.cancelLabel || 'Cancel'; submitBtn.textContent = options.confirmLabel || (hasChoices ? 'Continue' : 'Confirm'); submitBtn.className = `btn btn-sm ${variant === 'danger' ? 'btn-danger' : 'btn-primary'}`; modal.classList.toggle('confirm-modal--danger', variant === 'danger'); resolveConfirm = (value) => { if (hasChoices) { resolve(value); } else { resolve(value === true); } }; modal.hidden = false; document.body.classList.add('modal-open'); (hasChoices ? modal.querySelector('input[name="confirm_modal_choice"]') : submitBtn )?.focus(); }); }