/** ============================================================================= * 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
not found.`); return; } if (!formId || !publicKey) { container.innerHTML = '

[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 = `
`; // Render Title/Desc if (formData.name) html += `

${formData.name}

`; if (formData.description) html += `

${formData.description}

`; html += ``; // Render Fields fields.forEach(field => { const required = field.required ? 'required' : ''; const reqStar = field.required ? '*' : ''; // i18n resolution for label and placeholder const labelText = getLocalizedString(field.label, field.name); const placeholderText = getLocalizedString(field.placeholder, ''); html += `
`; html += ``; // Extract custom CSS classes const customCss = field.css_classes ? `class="${field.css_classes}"` : ''; if (field.type === 'textarea') { html += ``; } else if (field.type === 'select') { html += ``; } else { html += ``; } html += `
`; }); // Submit Button const btnText = (struct.submit_button && struct.submit_button.text) ? getLocalizedString(struct.submit_button.text, 'Submit') : 'Submit'; const btnCss = (struct.submit_button && struct.submit_button.css_classes) ? `class="${struct.submit_button.css_classes}"` : 'style="padding:10px 20px; cursor:pointer;"'; html += ``; 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'); } } })();