refactored confirmation dialogs to use a unified confirmAction modal
Some checks failed
PHPUnit / test (push) Has been cancelled
Some checks failed
PHPUnit / test (push) Has been cancelled
This commit is contained in:
148
website/js/confirm-modal.js
Normal file
148
website/js/confirm-modal.js
Normal file
@ -0,0 +1,148 @@
|
||||
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();
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user