|
| 1 | +/** |
| 2 | + * Django Forms Workflows — Embeddable Form Loader |
| 3 | + * |
| 4 | + * Drop this script tag on any webpage to embed a form: |
| 5 | + * |
| 6 | + * <script src="https://server/static/django_forms_workflows/js/dfw-embed.js" |
| 7 | + * data-form="contact-us" |
| 8 | + * data-server="https://server" |
| 9 | + * ></script> |
| 10 | + * |
| 11 | + * Attributes: |
| 12 | + * data-form (required) Form slug |
| 13 | + * data-server (required) Base URL of the DFW server |
| 14 | + * data-target CSS selector of container (default: insert after script tag) |
| 15 | + * data-on-submit Global function name called on successful submission |
| 16 | + * data-on-load Global function name called when form has loaded |
| 17 | + * data-theme "light" (default) or "dark" |
| 18 | + * data-accent-color Hex color for primary buttons (e.g., "#ff6600") |
| 19 | + * data-min-height Minimum iframe height in px (default: 300) |
| 20 | + * data-loading-text Text shown while loading (default: "Loading form...") |
| 21 | + */ |
| 22 | +(function () { |
| 23 | + 'use strict'; |
| 24 | + |
| 25 | + // Find our own script tag |
| 26 | + var script = document.currentScript; |
| 27 | + if (!script) { |
| 28 | + // Fallback for older browsers |
| 29 | + var scripts = document.getElementsByTagName('script'); |
| 30 | + script = scripts[scripts.length - 1]; |
| 31 | + } |
| 32 | + |
| 33 | + var formSlug = script.getAttribute('data-form'); |
| 34 | + var server = script.getAttribute('data-server'); |
| 35 | + |
| 36 | + if (!formSlug || !server) { |
| 37 | + console.error('[dfw-embed] data-form and data-server attributes are required.'); |
| 38 | + return; |
| 39 | + } |
| 40 | + |
| 41 | + // Read optional attributes |
| 42 | + var targetSelector = script.getAttribute('data-target'); |
| 43 | + var onSubmitFn = script.getAttribute('data-on-submit'); |
| 44 | + var onLoadFn = script.getAttribute('data-on-load'); |
| 45 | + var theme = script.getAttribute('data-theme') || ''; |
| 46 | + var accentColor = script.getAttribute('data-accent-color') || ''; |
| 47 | + var minHeight = parseInt(script.getAttribute('data-min-height')) || 300; |
| 48 | + var loadingText = script.getAttribute('data-loading-text') || 'Loading form...'; |
| 49 | + |
| 50 | + // Normalize server URL (remove trailing slash) |
| 51 | + server = server.replace(/\/+$/, ''); |
| 52 | + |
| 53 | + // Build embed URL |
| 54 | + var embedUrl = server + '/forms/' + encodeURIComponent(formSlug) + '/embed/'; |
| 55 | + var params = []; |
| 56 | + if (theme) params.push('theme=' + encodeURIComponent(theme)); |
| 57 | + if (accentColor) params.push('accent_color=' + encodeURIComponent(accentColor)); |
| 58 | + if (params.length) embedUrl += '?' + params.join('&'); |
| 59 | + |
| 60 | + // Create container |
| 61 | + var container = document.createElement('div'); |
| 62 | + container.className = 'dfw-embed-container'; |
| 63 | + container.style.cssText = 'width:100%;position:relative;'; |
| 64 | + |
| 65 | + // Loading indicator |
| 66 | + var loading = document.createElement('div'); |
| 67 | + loading.textContent = loadingText; |
| 68 | + loading.style.cssText = 'text-align:center;padding:2rem;color:#6c757d;font-family:sans-serif;font-size:0.9rem;'; |
| 69 | + container.appendChild(loading); |
| 70 | + |
| 71 | + // Create iframe |
| 72 | + var iframe = document.createElement('iframe'); |
| 73 | + iframe.src = embedUrl; |
| 74 | + iframe.style.cssText = 'width:100%;border:none;overflow:hidden;display:none;min-height:' + minHeight + 'px;'; |
| 75 | + iframe.setAttribute('scrolling', 'no'); |
| 76 | + iframe.setAttribute('allowtransparency', 'true'); |
| 77 | + iframe.setAttribute('title', 'Form: ' + formSlug); |
| 78 | + container.appendChild(iframe); |
| 79 | + |
| 80 | + // Insert into DOM |
| 81 | + if (targetSelector) { |
| 82 | + var target = document.querySelector(targetSelector); |
| 83 | + if (target) { |
| 84 | + target.appendChild(container); |
| 85 | + } else { |
| 86 | + console.error('[dfw-embed] Target element not found:', targetSelector); |
| 87 | + script.parentNode.insertBefore(container, script.nextSibling); |
| 88 | + } |
| 89 | + } else { |
| 90 | + script.parentNode.insertBefore(container, script.nextSibling); |
| 91 | + } |
| 92 | + |
| 93 | + // Parse the server origin for message validation |
| 94 | + var serverOrigin; |
| 95 | + try { |
| 96 | + var a = document.createElement('a'); |
| 97 | + a.href = server; |
| 98 | + serverOrigin = a.protocol + '//' + a.host; |
| 99 | + } catch (e) { |
| 100 | + serverOrigin = server; |
| 101 | + } |
| 102 | + |
| 103 | + // Listen for postMessage events from the iframe |
| 104 | + window.addEventListener('message', function (event) { |
| 105 | + // Validate origin |
| 106 | + if (event.origin !== serverOrigin) return; |
| 107 | + |
| 108 | + var data = event.data; |
| 109 | + if (!data || !data.type || data.formSlug !== formSlug) return; |
| 110 | + |
| 111 | + switch (data.type) { |
| 112 | + case 'dfw:loaded': |
| 113 | + loading.style.display = 'none'; |
| 114 | + iframe.style.display = 'block'; |
| 115 | + if (onLoadFn && typeof window[onLoadFn] === 'function') { |
| 116 | + window[onLoadFn](data); |
| 117 | + } |
| 118 | + break; |
| 119 | + |
| 120 | + case 'dfw:resize': |
| 121 | + if (data.height) { |
| 122 | + iframe.style.height = Math.max(data.height + 20, minHeight) + 'px'; |
| 123 | + } |
| 124 | + break; |
| 125 | + |
| 126 | + case 'dfw:submitted': |
| 127 | + if (onSubmitFn && typeof window[onSubmitFn] === 'function') { |
| 128 | + window[onSubmitFn](data); |
| 129 | + } |
| 130 | + break; |
| 131 | + } |
| 132 | + }); |
| 133 | +})(); |
0 commit comments