184 lines
No EOL
7 KiB
TypeScript
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();
|
|
}); |