149 lines
5.5 KiB
JavaScript
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();
|
|
});
|
|
}
|