Files
nat-as-server/website/js/confirm-modal.js
Tom Hempel b06c4bf3ee
Some checks failed
PHPUnit / test (push) Has been cancelled
refactored confirmation dialogs to use a unified confirmAction modal
2026-06-14 18:02:03 +02:00

149 lines
5.5 KiB
JavaScript

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) => `<p>${esc(block).replace(/\n/g, '<br>')}</p>`)
.join('');
}
function renderChoicesHTML(choices) {
const name = 'confirm_modal_choice';
return `
<fieldset class="password-mode-fieldset confirm-modal-choices">
<div class="password-mode-options">
${choices.map((choice, i) => `
<label class="password-mode-option">
<input type="radio" name="${name}" value="${esc(choice.value)}"${i === 0 ? ' checked' : ''}>
<span class="password-mode-option-title">${esc(choice.label)}</span>
${choice.description
? `<span class="password-mode-option-desc">${esc(choice.description)}</span>`
: ''}
</label>
`).join('')}
</div>
</fieldset>`;
}
/** @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 = `
<div class="modal-dialog confirm-modal-dialog">
<h2 id="confirmModalTitle"></h2>
<div class="confirm-modal-message modal-subtitle" id="confirmModalMessage"></div>
<div id="confirmModalChoices"></div>
<div class="modal-actions">
<button type="button" class="btn btn-sm" data-confirm-cancel>Cancel</button>
<button type="button" class="btn btn-sm btn-primary" data-confirm-submit>Confirm</button>
</div>
</div>
`;
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<boolean|string|null>}
*/
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();
});
}