Files
breadcrumb-the-shire/web/js/components/app-confirm-actions.js

126 lines
4.1 KiB
JavaScript
Raw Normal View History

/**
* Declarative confirm-before-action intercepts clicks and submits on [data-confirm-message] elements.
*/
2026-03-06 00:44:52 +01:00
import { confirmDialog, requestSubmitWithFallback } from '../core/app-confirm-dialog.js';
import { isSubmitControl, belongsToForm } from '../core/app-form-utils.js';
2026-03-06 00:44:52 +01:00
2026-03-04 15:56:58 +01:00
const CONFIRM_ATTR = 'data-confirm-message';
const BOUND_KEY = '_confirmActionsBound';
2026-03-06 00:44:52 +01:00
const confirmedSubmitterByForm = new WeakMap();
const confirmedClickElements = new WeakSet();
const trimAttr = (element, attr) => String(element?.getAttribute(attr) || '').trim();
2026-03-04 15:56:58 +01:00
const submitterByForm = new Map();
2026-03-06 00:44:52 +01:00
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')
});
2026-03-04 15:56:58 +01:00
const onClickCapture = (event) => {
const trigger = event.target instanceof HTMLElement ? event.target.closest('a, button, input') : null;
if (!(trigger instanceof HTMLElement)) {
return;
}
2026-03-06 00:44:52 +01:00
if (confirmedClickElements.has(trigger)) {
confirmedClickElements.delete(trigger);
return;
}
2026-03-04 15:56:58 +01:00
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;
}
2026-03-06 00:44:52 +01:00
event.preventDefault();
event.stopPropagation();
void confirmDialog.confirm(buildConfirmOptions(confirmMessage, trigger, null))
.then((approved) => {
if (!approved || !document.contains(trigger)) {
return;
}
confirmedClickElements.add(trigger);
trigger.click();
});
2026-03-04 15:56:58 +01:00
};
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);
2026-03-06 00:44:52 +01:00
const confirmedSubmitter = confirmedSubmitterByForm.get(form);
if (confirmedSubmitter && (confirmedSubmitter === true || confirmedSubmitter === submitter || confirmedSubmitter === null)) {
confirmedSubmitterByForm.delete(form);
return;
}
2026-03-04 15:56:58 +01:00
const submitterMessage = String(submitter?.getAttribute(CONFIRM_ATTR) || '').trim();
const formMessage = String(form.getAttribute(CONFIRM_ATTR) || '').trim();
const confirmMessage = submitterMessage !== '' ? submitterMessage : formMessage;
if (confirmMessage === '') {
return;
}
2026-03-06 00:44:52 +01:00
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);
});
2026-03-04 15:56:58 +01:00
};
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 };
}