/** * Declarative confirm-before-action — intercepts clicks and submits on [data-confirm-message] elements. */ import { confirmDialog, requestSubmitWithFallback } from '../core/app-confirm-dialog.js'; import { isSubmitControl, belongsToForm } from '../core/app-form-utils.js'; const CONFIRM_ATTR = 'data-confirm-message'; const BOUND_KEY = '_confirmActionsBound'; const confirmedSubmitterByForm = new WeakMap(); const confirmedClickElements = new WeakSet(); const trimAttr = (element, attr) => String(element?.getAttribute(attr) || '').trim(); const submitterByForm = new Map(); const readConfirmOption = (primary, fallback, suffix) => { const attr = `data-confirm-${suffix}`; return trimAttr(primary, attr) || trimAttr(fallback, attr); }; const buildConfirmOptions = (message, primary, fallback) => ({ message, title: readConfirmOption(primary, fallback, 'title'), confirmLabel: readConfirmOption(primary, fallback, 'confirm-label'), cancelLabel: readConfirmOption(primary, fallback, 'cancel-label'), variant: readConfirmOption(primary, fallback, 'variant'), focus: readConfirmOption(primary, fallback, 'focus'), describedBy: readConfirmOption(primary, fallback, 'describedby') || readConfirmOption(primary, fallback, 'described-by'), actionKind: readConfirmOption(primary, fallback, 'action-kind') }); const onClickCapture = (event) => { const trigger = event.target instanceof HTMLElement ? event.target.closest('a, button, input') : null; if (!(trigger instanceof HTMLElement)) { return; } if (confirmedClickElements.has(trigger)) { confirmedClickElements.delete(trigger); return; } if (isSubmitControl(trigger)) { const form = trigger.form instanceof HTMLFormElement ? trigger.form : null; if (form) { submitterByForm.set(form, trigger); } return; } const confirmMessage = String(trigger.getAttribute(CONFIRM_ATTR) || '').trim(); if (confirmMessage === '') { return; } event.preventDefault(); event.stopPropagation(); void confirmDialog.confirm(buildConfirmOptions(confirmMessage, trigger, null)) .then((approved) => { if (!approved || !document.contains(trigger)) { return; } confirmedClickElements.add(trigger); trigger.click(); }); }; const onSubmitCapture = (event) => { const form = event.target instanceof HTMLFormElement ? event.target : null; if (!(form instanceof HTMLFormElement)) { return; } const submitter = event.submitter instanceof HTMLElement ? event.submitter : (submitterByForm.get(form) || null); submitterByForm.delete(form); const confirmedSubmitter = confirmedSubmitterByForm.get(form); if (confirmedSubmitter && (confirmedSubmitter === true || confirmedSubmitter === submitter || confirmedSubmitter === null)) { confirmedSubmitterByForm.delete(form); return; } const submitterMessage = String(submitter?.getAttribute(CONFIRM_ATTR) || '').trim(); const formMessage = String(form.getAttribute(CONFIRM_ATTR) || '').trim(); const confirmMessage = submitterMessage !== '' ? submitterMessage : formMessage; if (confirmMessage === '') { return; } event.preventDefault(); event.stopPropagation(); void confirmDialog.confirm(buildConfirmOptions(confirmMessage, submitter, form)) .then((approved) => { if (!approved || !document.contains(form)) { return; } const submitterForSubmit = submitter && belongsToForm(submitter, form) ? submitter : null; confirmedSubmitterByForm.set(form, submitterForSubmit || true); requestSubmitWithFallback(form, submitterForSubmit); }); }; export function initConfirmActions(root = document) { const host = root && typeof root.addEventListener === 'function' ? root : document; if (host[BOUND_KEY]) { return { destroy: () => {} }; } host[BOUND_KEY] = true; host.addEventListener('click', onClickCapture, true); host.addEventListener('submit', onSubmitCapture, true); const destroy = () => { host.removeEventListener('click', onClickCapture, true); host.removeEventListener('submit', onSubmitCapture, true); delete host[BOUND_KEY]; }; return { destroy }; }