/** =============================================================================
* Formeleon - Multi-tenant Form Generator
* Author: Björn Runden
* Script: embed.js
* Description: Client-side embed script for injecting forms into tenant websites.
* Handles handshake, UI rendering, and form submission via fetch API.
* =============================================================================
*/
(function() {
// Current script tag to extract data attributes
const scripts = document.getElementsByTagName('script');
const currentScript = scripts[scripts.length - 1];
// Configuration
const formId = currentScript.getAttribute('data-form-id');
const publicKey = currentScript.getAttribute('data-public-key');
// We assume backend API is hosted at the same origin as this script
const apiBaseUrl = new URL(currentScript.src).origin + '/api/v1';
const containerId = 'formeleon-form';
// Wait for DOM
document.addEventListener("DOMContentLoaded", init);
function init() {
const container = document.getElementById(containerId);
if (!container) {
console.error(`[Formeleon] Container
[Formeleon] Missing form ID or public key configuration.
'; return; } // Show loading state container.innerHTML = 'Loading form...
'; // Step 1: Handshake (Fetch form config & reCAPTCHA config) fetch(`${apiBaseUrl}/forms/${formId}`, { method: 'GET', headers: { 'Authorization': `Bearer ${publicKey}`, 'Accept': 'application/json' } }) .then(response => { if (!response.ok) { throw new Error(`Failed to load form (Status: ${response.status})`); } return response.json(); }) .then(data => { renderForm(container, data); }) .catch(err => { console.error('[Formeleon] Handshake Error:', err); container.innerHTML = `[Formeleon Error] ${err.message}
`; }); } /** * Helper to extract localized text based on the document lang */ function getLocalizedString(fieldProperty, fallback) { if (!fieldProperty) return fallback || ''; // If it's a simple string, return it directly if (typeof fieldProperty === 'string') return fieldProperty; // If it's an object with language keys (e.g., {"en": "Name", "de": "Name"}) if (typeof fieldProperty === 'object') { const pageLang = document.documentElement.lang || 'en'; // Try exact match (e.g. 'en-US') if (fieldProperty[pageLang]) return fieldProperty[pageLang]; // Try base language match (e.g. 'en') const baseLang = pageLang.split('-')[0]; if (fieldProperty[baseLang]) return fieldProperty[baseLang]; // Fallback to English if available, or just take the first available value if (fieldProperty['en']) return fieldProperty['en']; return Object.values(fieldProperty)[0] || fallback; } return fallback || ''; } /** * Renders the HTML form based on JSON structure */ function renderForm(container, formData) { if (!formData || !formData.form_structure) { container.innerHTML = 'Invalid form configuration received.
'; return; } // Load reCAPTCHA script if enabled if (formData.recaptcha && formData.recaptcha.enabled && formData.recaptcha.site_key) { window._formeleonRecaptchaSiteKey = formData.recaptcha.site_key; const rcScript = document.createElement('script'); rcScript.src = `https://www.google.com/recaptcha/api.js?render=${formData.recaptcha.site_key}`; document.head.appendChild(rcScript); } const struct = formData.form_structure; const fields = struct.fields || []; let html = ``; container.innerHTML = html; // Attach Submit Handler const formEl = document.getElementById('formeleon-submit-form'); formEl.addEventListener('submit', handleFormSubmit); } /** * Handles the form submission process */ function handleFormSubmit(e) { e.preventDefault(); const formEl = e.target; const btn = document.getElementById('formeleon-submit-btn'); const alertBox = document.getElementById('formeleon-alert'); // Hide previous alerts alertBox.style.display = 'none'; // Extract Data const formData = new FormData(formEl); const payload = {}; formData.forEach((value, key) => { payload[key] = value; }); // Update UI State btn.disabled = true; btn.innerText = 'Submitting...'; // Helper to send data const sendPayload = (recaptchaToken) => { fetch(`${apiBaseUrl}/submissions`, { method: 'POST', headers: { 'Authorization': `Bearer ${publicKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ form_id: formId, payload: payload, recaptcha_token: recaptchaToken }) }) .then(response => { return response.json().then(data => ({status: response.status, body: data})); }) .then(res => { if (res.status >= 200 && res.status < 300) { // Success alertBox.style.display = 'block'; alertBox.style.backgroundColor = '#d4edda'; alertBox.style.color = '#155724'; alertBox.style.border = '1px solid #c3e6cb'; alertBox.innerHTML = res.body.message || 'Thank you! Your submission has been received.'; formEl.reset(); } else { // Error throw new Error(res.body.error || 'Submission failed.'); } }) .catch(err => { // Error Display alertBox.style.display = 'block'; alertBox.style.backgroundColor = '#f8d7da'; alertBox.style.color = '#721c24'; alertBox.style.border = '1px solid #f5c6cb'; alertBox.innerHTML = err.message; }) .finally(() => { btn.disabled = false; btn.innerText = 'Submit'; }); }; // Execute reCAPTCHA if enabled if (window._formeleonRecaptchaSiteKey && typeof grecaptcha !== 'undefined') { grecaptcha.ready(function() { grecaptcha.execute(window._formeleonRecaptchaSiteKey, {action: 'submit'}).then(function(token) { sendPayload(token); }).catch(function(err) { console.error('[Formeleon] reCAPTCHA execution failed', err); sendPayload(null); // Send anyway, backend will evaluate }); }); } else { // Dev mode or not configured sendPayload('mock_token_for_dev'); } } })();