221 lines
6.3 KiB
JavaScript
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;
|
|
}
|