ldc-legaulois/resources/ts/bo/utilities/submit-form.ts
2026-06-04 14:20:18 +02:00

184 lines
No EOL
7 KiB
TypeScript

// Helpers
import { initPictureSizeButton } from "./picto-picture-size";
function clearFormErrors(form: HTMLFormElement) {
form.querySelectorAll('.is-invalid').forEach(el => el.classList.remove('is-invalid'));
form.querySelectorAll('.invalid-feedback').forEach(el => el.remove());
}
function displayFormErrors(form: HTMLFormElement, errors: Record<string, string[]>) {
clearFormErrors(form);
Object.entries(errors).forEach(([field, messages]) => {
const inputName = field.replace(/\.(\w+)/g, '[$1]');
const input = form.querySelector(`[name="${inputName}"]`) as HTMLElement;
if (!input) return;
input.classList.add('is-invalid');
const feedback = document.createElement('div');
feedback.classList.add('invalid-feedback');
feedback.textContent = messages[0];
input.after(feedback);
});
}
function getFlashContainer(form: HTMLFormElement): HTMLElement {
let container = form.previousElementSibling as HTMLElement;
if (!container || !container.classList.contains('flash-container')) {
container = document.createElement('div');
container.classList.add('flash-container');
form.parentElement!.insertBefore(container, form);
}
return container;
}
async function fetchFlash(container: HTMLElement) {
const res = await fetch('/bo/flash', {
headers: { 'Accept': 'application/json' }
});
const data = await res.json();
if (data.html) {
container.innerHTML = data.html;
}
}
function showSuccess(form: HTMLFormElement, message: string) {
const container = getFlashContainer(form);
container.innerHTML = `
<div class="alert alert-success alert-dismissible fade show m-0 mb-3" role="alert">
<i class="fa-solid fa-circle-check"></i>
<strong>${message}</strong>
<button class="btn-close" data-bs-dismiss="alert" type="button" aria-label="Fermer"></button>
</div>`;
container.scrollIntoView({ behavior: 'smooth', block: 'center' });
setTimeout(() => container.innerHTML = '', 5000);
}
function showError(form: HTMLFormElement, message: string) {
const container = getFlashContainer(form);
container.innerHTML = `
<div class="alert alert-danger alert-dismissible fade show m-0 mb-3" role="alert">
<i class="fa-solid fa-circle-xmark"></i>
<strong>${message}</strong>
<button class="btn-close" data-bs-dismiss="alert" type="button" aria-label="Fermer"></button>
</div>`;
container.scrollIntoView({ behavior: 'smooth', block: 'center' });
setTimeout(() => container.innerHTML = '', 5000);
}
export function showFlashGlobal(message: string, type: 'success' | 'danger' = 'success') {
let container = document.querySelector<HTMLElement>('.flash-container');
if (!container) {
container = document.createElement('div');
container.classList.add('flash-container');
document.body.prepend(container);
}
const icon = type === 'success' ? 'fa-circle-check' : 'fa-circle-xmark';
container.innerHTML = `
<div class="alert alert-${type} alert-dismissible fade show m-0 mb-3" role="alert">
<i class="fa-solid ${icon}"></i>
<strong>${message}</strong>
<button class="btn-close" data-bs-dismiss="alert" type="button" aria-label="Fermer"></button>
</div>`;
container.scrollIntoView({ behavior: 'smooth', block: 'center' });
setTimeout(() => { if (container) container.innerHTML = ''; }, 5000);
}
export async function handleSubmit(form: HTMLFormElement, action: (form: HTMLFormElement) => Promise<any>) {
clearFormErrors(form);
try {
const data = await action(form);
showSuccess(form, data.message ?? 'Enregistré avec succès.');
} catch (e: any) {
if (e.errors) displayFormErrors(form, e.errors);
if (e.message && !e.errors) showError(form, e.message);
console.error(e);
const container = getFlashContainer(form);
container.scrollIntoView({ behavior: 'smooth', block: 'center' });
await fetchFlash(container);
}
}
export function initPictureSizeButtons(container: HTMLElement = document.body): void {
container.querySelectorAll<HTMLButtonElement>('.submit-picture-size')
.forEach(initPictureSizeButton);
}
// API
export async function apiFetch(url: string, method: string, form: HTMLFormElement, override?: string) {
const token = (form.querySelector('input[name="_token"]') as HTMLInputElement)?.value;
const headers: Record<string, string> = {
'X-CSRF-TOKEN': token,
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
};
const isDelete = method === 'DELETE';
const body = isDelete ? undefined : new FormData(form);
if (!isDelete && override) body!.append('_method', override);
const response = await fetch(url, {
method,
headers,
body
});
const data = await response.json();
if (!response.ok) throw data;
return data;
}
// Handlers
document.addEventListener('submit', async (e) => {
const form = (e.target as HTMLElement).closest<HTMLFormElement>('.section-form');
if (!form) return;
e.preventDefault();
await handleSubmit(form, f => apiFetch(`/bo/sections/${f.dataset.id}`, 'POST', f, 'PATCH'));
});
document.addEventListener('submit', async (e) => {
const form = (e.target as HTMLElement).closest<HTMLFormElement>('.block-form');
if (!form) return;
e.preventDefault();
await handleSubmit(form, f => apiFetch(`/bo/blocks/${f.dataset.id}`, 'POST', f, 'PUT'));
});
document.addEventListener('submit', async (e) => {
const form = (e.target as HTMLElement).closest<HTMLFormElement>('.picto-form');
if (!form) return;
e.preventDefault();
await handleSubmit(form, async f => {
const data = f.dataset.id
? await apiFetch(`/bo/pictos/${f.dataset.id}`, 'POST', f, 'PUT')
: await apiFetch('/bo/pictos', 'POST', f);
const sectionId = (f.querySelector('input[name="section_id"]') as HTMLInputElement)?.value;
if (sectionId && !f.dataset.id) {
const listContainer = document.querySelector<HTMLDivElement>(`.picto-list[data-section="${sectionId}"]`);
const wrapper = document.querySelector<HTMLDivElement>(`.picto-form-wrapper[data-section="${sectionId}"]`);
const btnCreate = document.querySelector<HTMLElement>(`.btn-create-picto[data-section="${sectionId}"]`);
if (listContainer) {
const res = await fetch(`/bo/pictos?section_id=${sectionId}`, {
headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
if (res.ok) listContainer.innerHTML = await res.text();
}
if (wrapper) wrapper.style.display = 'none';
if (listContainer) listContainer.style.display = 'block';
if (btnCreate) btnCreate.style.display = '';
}
return data;
});
});
document.addEventListener('DOMContentLoaded', () => {
initPictureSizeButtons();
});