Files
Reservair/assets/js/forms/RsvFormSender.js
T
Martin Slachta 8e264b8892 (#3) - templating
2026-06-14 07:21:07 +02:00

171 lines
5.5 KiB
JavaScript

export const RsvFormSender = {
get_form_url(form_id) {
return ReservairServiceAPI.restUrl + '/form/' + form_id;
},
clear_feedback(form) {
form.querySelectorAll('.rsv-field-error').forEach(el => el.remove());
form.querySelectorAll('.rsv-invalid').forEach(el => el.classList.remove('rsv-invalid'));
form.querySelector('.rsv-error-summary')?.remove();
},
show_errors(form, errors) {
this.clear_feedback(form);
const ul = document.createElement('ul');
for (const err of errors) {
const li = document.createElement('li');
li.textContent = err.message;
ul.appendChild(li);
if (err.element) {
const field = form.querySelector(`[name="${err.element}"]`);
if (field) {
field.classList.add('rsv-invalid');
const msg = document.createElement('span');
msg.classList.add('rsv-field-error');
msg.textContent = err.message;
field.insertAdjacentElement('afterend', msg);
}
}
}
const summary = document.createElement('div');
summary.classList.add('rsv-error-summary');
summary.appendChild(ul);
form.prepend(summary);
},
show_success(form, _data) {
const s = ReservairStrings.form;
const wrapper = form.parentElement;
const existing = Array.from(wrapper.children);
const svgNS = 'http://www.w3.org/2000/svg';
const path = document.createElementNS(svgNS, 'path');
path.setAttribute('d', 'M6 14l6 6L22 8');
path.setAttribute('stroke', '#16a34a');
path.setAttribute('stroke-width', '2.5');
path.setAttribute('stroke-linecap', 'round');
path.setAttribute('stroke-linejoin', 'round');
const svg = document.createElementNS(svgNS, 'svg');
svg.setAttribute('width', '28');
svg.setAttribute('height', '28');
svg.setAttribute('viewBox', '0 0 28 28');
svg.setAttribute('fill', 'none');
svg.appendChild(path);
const icon = document.createElement('div');
icon.className = 'rsv-success-icon';
icon.appendChild(svg);
const body = this.build_success_body(form, s);
const state = document.createElement('div');
state.className = 'rsv-success-state';
state.append(icon, body);
const msg = document.createElement('div');
msg.appendChild(state);
existing.forEach(child => child.style.display = 'none');
wrapper.appendChild(msg);
// The form catches every data-rsv-reset button in the card and links it to
// its cleanup — so new buttons just need the marker, no wiring here.
const reset = () => {
msg.remove();
form.reset();
// Native reset leaves custom controls untouched; reset the reservation
// selectors so their slots, hidden inputs and the summary clear, the date
// stays selected and availability reloads with the just-booked slots.
form.querySelectorAll('rsv-reservation-selector').forEach(sel => sel.reset());
this.clear_feedback(form);
existing.forEach(child => child.style.display = '');
};
state.querySelectorAll('[data-rsv-reset]').forEach(btn => btn.addEventListener('click', reset));
},
// Body of the success card. Uses the admin-configured template when the form
// ships one, filling the .rsv-success-summary placeholder (expanded server-side
// from <reservation-summary>) with a snapshot of the selected slots; otherwise
// falls back to the default text.
build_success_body(form, strings) {
const tpl = form.parentElement?.querySelector('template.rsv-form-success');
if (!tpl) {
const subtitle = document.createElement('p');
subtitle.className = 'rsv-success-msg';
subtitle.textContent = strings.success_subtitle;
return subtitle;
}
const body = document.createElement('div');
body.className = 'rsv-success-msg';
body.appendChild(tpl.content.cloneNode(true));
const placeholder = body.querySelector('.rsv-success-summary');
if (placeholder) {
const summary = form.querySelector('rsv-reservation-summary');
if (summary && typeof summary.snapshot === 'function') {
placeholder.replaceWith(summary.snapshot());
} else {
placeholder.remove();
}
}
return body;
},
set_loading(form, is_loading) {
const btn = form.querySelector('button[type="submit"], button:not([type])');
if (!btn) return;
btn.disabled = is_loading;
btn.classList.toggle('rsv-loading', is_loading);
},
encode_to_json(form) {
const fields = form.querySelectorAll('.rsv-form-field');
const body = {};
fields.forEach(field => {
const name = field.name ?? field.getAttribute('name');
try {
body[name] = JSON.parse(field.value);
} catch {
body[name] = field.value;
}
});
return body;
},
send_form(event) {
event.preventDefault();
const form = event.target;
this.clear_feedback(form);
this.set_loading(form, true);
const body = this.encode_to_json(form);
fetch(this.get_form_url(form.id), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
.then(async response => {
const data = await response.json().catch(() => null);
if (!response.ok) throw { status: response.status, body: data };
return data;
})
.then(data => {
this.show_success(form, data);
})
.catch(error => {
const errors = error?.body?.errors
?? [{ element: '', message: ReservairStrings.form.error_generic }];
this.show_errors(form, errors);
})
.finally(() => {
this.set_loading(form, false);
});
},
};