Files
breadcrumb-the-shire/web/js/core/app-details-action-policy.js
2026-03-04 15:56:58 +01:00

221 lines
6.3 KiB
JavaScript

const escapeAttr = (value) => {
const raw = String(value || '');
if (typeof CSS !== 'undefined' && CSS && typeof CSS.escape === 'function') {
return CSS.escape(raw);
}
return raw.replace(/["\\]/g, '\\$&');
};
const toContainerElement = (value) => {
if (!value) {
return document.querySelector('.app-details-container') || document.body || document.documentElement;
}
if (typeof value === 'string') {
return document.querySelector(value);
}
if (value instanceof HTMLElement) {
return value;
}
if (value === document) {
return document.documentElement;
}
return null;
};
const toFormElement = (value) => {
if (!value) {
return null;
}
if (value instanceof HTMLFormElement) {
return value;
}
if (typeof value === 'string') {
const element = document.querySelector(value);
return element instanceof HTMLFormElement ? element : null;
}
return null;
};
const normalizeSubmitLock = (value) => {
const normalized = String(value || '').trim().toLowerCase();
return normalized === 'none' ? 'none' : 'form';
};
const belongsToForm = (control, form) => {
if (!(control instanceof HTMLElement) || !(form instanceof HTMLFormElement)) {
return false;
}
const owner = control.form;
if (owner instanceof HTMLFormElement) {
return owner === form;
}
const controlFormAttr = String(control.getAttribute('form') || '').trim();
const formId = String(form.id || '').trim();
if (controlFormAttr !== '' && formId !== '') {
return controlFormAttr === formId;
}
return form.contains(control);
};
const isSubmitControl = (control) => {
if (!(control instanceof HTMLElement)) {
return false;
}
if (control instanceof HTMLButtonElement) {
const type = String(control.getAttribute('type') || 'submit').trim().toLowerCase();
return type === '' || type === 'submit';
}
if (control instanceof HTMLInputElement) {
const type = String(control.type || '').trim().toLowerCase();
return type === 'submit' || type === 'image';
}
return false;
};
const findSubmitControlsForForm = (form) => {
if (!(form instanceof HTMLFormElement)) {
return [];
}
const controls = [];
controls.push(
...Array.from(form.querySelectorAll('button, input')).filter(isSubmitControl)
);
const formId = String(form.id || '').trim();
if (formId !== '') {
const escapedId = escapeAttr(formId);
controls.push(
...Array.from(document.querySelectorAll(`button[form="${escapedId}"], input[form="${escapedId}"]`))
.filter(isSubmitControl)
);
}
return Array.from(new Set(controls)).filter((control) => belongsToForm(control, form));
};
const pickSubmitter = (event, form, submitterByForm) => {
const eventSubmitter = event.submitter instanceof HTMLElement ? event.submitter : null;
if (eventSubmitter && belongsToForm(eventSubmitter, form)) {
return eventSubmitter;
}
const fallback = submitterByForm.get(form);
if (fallback && belongsToForm(fallback, form)) {
return fallback;
}
return null;
};
const readConfirmMessage = (submitter, form) => {
const fromSubmitter = String(submitter?.getAttribute('data-detail-confirm-message') || '').trim();
if (fromSubmitter !== '') {
return fromSubmitter;
}
return String(form.getAttribute('data-detail-confirm-message') || '').trim();
};
const readSubmitLockMode = (submitter, form) => normalizeSubmitLock(
submitter?.getAttribute('data-detail-submit-lock')
|| form.getAttribute('data-detail-submit-lock')
|| 'form'
);
const disableSubmitControlsForForm = (form, submitter, options = {}) => {
const controls = findSubmitControlsForForm(form);
controls.forEach((control) => {
control.disabled = true;
});
if (submitter instanceof HTMLElement) {
submitter.setAttribute('aria-busy', 'true');
}
if (options.markFormBusy === true) {
form.setAttribute('aria-busy', 'true');
}
};
export function initDetailActionPolicy(options = {}) {
const enabled = options.enabled !== false;
if (!enabled) {
return {
isSubmitting: () => false,
destroy: () => {}
};
}
const container = toContainerElement(options.container);
if (!(container instanceof HTMLElement)) {
return {
isSubmitting: () => false,
destroy: () => {}
};
}
if (container._detailActionPolicyApi) {
return container._detailActionPolicyApi;
}
const submittingForms = new Set();
const submitterByForm = new Map();
const onClickCapture = (event) => {
const target = event.target instanceof HTMLElement
? event.target.closest('button, input')
: null;
if (!(target instanceof HTMLElement) || !isSubmitControl(target) || !container.contains(target)) {
return;
}
const form = target.form instanceof HTMLFormElement ? target.form : null;
if (!(form instanceof HTMLFormElement)) {
return;
}
submitterByForm.set(form, target);
};
const onSubmitCapture = (event) => {
const form = event.target instanceof HTMLFormElement ? event.target : null;
if (!(form instanceof HTMLFormElement) || !container.contains(form)) {
return;
}
const submitter = pickSubmitter(event, form, submitterByForm);
const lockMode = readSubmitLockMode(submitter, form);
if (lockMode !== 'none' && submittingForms.has(form)) {
event.preventDefault();
return;
}
const confirmMessage = readConfirmMessage(submitter, form);
if (confirmMessage !== '' && !window.confirm(confirmMessage)) {
event.preventDefault();
return;
}
if (lockMode === 'none') {
return;
}
submittingForms.add(form);
disableSubmitControlsForForm(form, submitter, { markFormBusy: options.markFormBusy === true });
};
container.addEventListener('click', onClickCapture, true);
container.addEventListener('submit', onSubmitCapture, true);
const api = {
isSubmitting: (formRef = null) => {
const form = toFormElement(formRef);
if (form) {
return submittingForms.has(form);
}
return submittingForms.size > 0;
},
destroy: () => {
container.removeEventListener('click', onClickCapture, true);
container.removeEventListener('submit', onSubmitCapture, true);
submittingForms.clear();
submitterByForm.clear();
delete container._detailActionPolicyApi;
}
};
container._detailActionPolicyApi = api;
return api;
}