1/** 2 * Dapta Forms embed helper: auto-resizes embedded form iframes, carries their 3 * end-of-form redirect up to the host page, and tells a form which page it is 4 * answered on. 5 * 6 * Drop next to any iframe carrying the `data-dapta-forms` attribute: 7 * 8 * <iframe data-dapta-forms src="https://your-host/acct/f/slug?embed=1" 9 * title="My form" loading="lazy" 10 * style="width:100%;border:0;min-height:480px;"></iframe> 11 * <script src="https://your-host/embed.js" async></script> 12 * 13 * Four messages cross the boundary, all matched to a frame by `event.source` 14 * and never by URL, so several forms can share one page and a message can only 15 * ever act on the frame it came from: 16 * 17 * dapta-forms:resize {height}: the embedded page (opened with ?embed=1) 18 * posts its content height whenever it changes; this script sets the 19 * matching iframe's height so the form never shows an inner scrollbar. 20 * Only a number crosses: no answers, no identifiers. 21 * 22 * dapta-forms:redirect {url}: the form finished and its configured 23 * destination has to be opened at the TOP level, not inside the frame. 24 * Checkout pages (Stripe Payment Links above all) refuse to run framed by 25 * another site and hang on their own skeleton, so a form that redirects to 26 * one submits into nothing unless the host page navigates itself. The URL 27 * is the form's published `redirectUrl`, a static string, never built from 28 * what the visitor typed. It is allowlisted to http(s) before use, exactly 29 * as the renderer does before assigning `window.location`. This script 30 * replies `dapta-forms:redirect-ack` so the frame knows not to navigate 31 * the top as well: two navigations of one document race, and WebKit 32 * resolves that race by cancelling both. 33 * 34 * dapta-forms:scroll-into-view: the form swapped its whole screen (the 35 * ending, the submitting screen) and the new screen starts at the top of 36 * the frame. On a long one-page form the visitor is scrolled to the button 37 * they just pressed, far below it, and would be left looking at an empty 38 * block. This script scrolls the frame's top into view, and ONLY when that 39 * top is out of the viewport: someone already looking at it is not moved. 40 * Smooth unless the visitor asked for reduced motion. The form never sends 41 * it for the screen it loads with, so loading a page never scrolls it. 42 * 43 * dapta-forms:context-request {id, v: 1}: the form is about to save its 44 * answers and asks which page it is on. This script answers 45 * `dapta-forms:context {id, v: 1, pageUri, pageName, pageId?, 46 * hsPortalId?, hutk?}`: this page's URL and title, its HubSpot page and 47 * portal ids when it runs HubSpot, and HubSpot's visitor cookie 48 * (`hubspotutk`), which is what lets HubSpot join the new contact to the 49 * visits it made before converting. The iframe cannot read any of it: it 50 * lives on another origin. The answer goes ONLY to one of our frames: its 51 * `src` must be on the host this script was loaded from, so an iframe 52 * someone marked with the attribute but that is not our form gets 53 * nothing, and it must still show the origin of that `src`, so a frame 54 * that navigated elsewhere is never handed the cookie. (A copy of the 55 * script with no src of its own, pasted inline, cannot know its host and 56 * keeps only the second check.) The cookie is left 57 * out when the visitor opted out of HubSpot tracking (`__hs_opt_out=yes`, 58 * or a `__hs_do_not_track` cookie) and when it is not HubSpot shaped. This 59 * script reads cookies and never writes one. Put 60 * `data-dapta-forms-context="off"` on an iframe to keep all of it from 61 * that form: the answer is then `{id, v: 1, off: true}` and nothing else. 62 * 63 * Anything that is not one of those messages, in that shape, is ignored. A 64 * copy of this script cached from before a message existed simply ignores it. 65 * 66 * WHY THE REDIRECT HANDLER EXISTS: a plain cross-origin iframe can navigate 67 * its own top, so most embeds never need this message. An iframe given a 68 * `sandbox` attribute that omits `allow-top-navigation` cannot: the browser 69 * refuses it with nothing but a console warning, and the visitor is stranded 70 * on a form that went nowhere. This script runs in the host page, outside that 71 * sandbox, so it can still navigate. Keep it on the page. 72 */ 73(function () { 74 'use strict'; 75 76 var MAX_HEIGHT = 20000; // sanity cap â a runaway report can't blow up the host page 77 var HTTP_ONLY = /^https?:\/\//i; // same allowlist the renderer applies 78 var leaving = false; // one redirect per page â a second message is a no-op 79 80 var HUTK = /^[0-9a-f]{32}$/i; // HubSpot's visitor cookie: 32 hex characters 81 var DIGITS = /^\d{1,20}$/; // a HubSpot page or portal id 82 83 // The forms host this script was loaded from, read once: `currentScript` is 84 // only set while the script first runs. Null when it cannot be known (an 85 // inline copy, an old browser), and then only the frame's own src is checked. 86 var SCRIPT_ORIGIN = (function () { 87 try { 88 var script = document.currentScript; 89 if (!script || !script.src) return null; 90 var origin = new URL(script.src, window.location.href).origin; 91 return origin && origin !== 'null' ? origin : null; 92 } catch (_) { 93 return null; 94 } 95 })(); 96 97 /** True when `source` is the contentWindow of one of OUR embedded frames. */
98 function isOurFrame(source) { 99 var frames = document.querySelectorAll('iframe[data-dapta-forms]'); 100 for (var i = 0; i < frames.length; i++) { 101 try { 102 if (source === frames[i].contentWindow) return frames[i]; 103 } catch (_) { 104 /* cross-origin contentWindow access can throw â skip that frame */ 105 } 106 } 107 return null; 108 } 109 110 /** The origin of a frame's `src`, or null when it has none worth answering. */ 111 function srcOrigin(frame) { 112 // No src (a srcdoc frame, say) is no form of ours: it would resolve to this page. 113 if (!frame.src) return null; 114 try { 115 var origin = new URL(frame.src, window.location.href).origin; 116 return origin && origin !== 'null' ? origin : null; 117 } catch (_) { 118 return null; 119 } 120 } 121 122 /** One cookie of this page, or null. Read only: this script never writes one. */ 123 function readCookie(name) { 124 var parts = String(document.cookie || '').split(';'); 125 for (var i = 0; i < parts.length; i++) { 126 var eq = parts[i].indexOf('='); 127 var key = (eq < 0 ? parts[i] : parts[i].slice(0, eq)).replace(/^\s+|\s+$/g, ''); 128 if (key === name) return eq < 0 ? '' : parts[i].slice(eq + 1).replace(/^\s+|\s+$/g, ''); 129 } 130 return null; 131 } 132 133 /** The HubSpot portal whose tracking code runs on this page, or null. */ 134 function hubspotPortal() { 135 var vars = window.hsVars; 136 if (vars && vars.portal_id != null && DIGITS.test(String(vars.portal_id))) return String(vars.portal_id); 137 var loader = document.getElementById('hs-script-loader'); 138 var match = loader && loader.src ? /\/(\d{1,20})\.js(?:[?#]|$)/.exec(loader.src) : null; 139 return match ? match[1] : null; 140 } 141 142 /** This page, as it is right now, for the frame that asked. */ 143 function pageContext(id) { 144 var reply = { 145 type: 'dapta-forms:context', 146 id: id, 147 v: 1, 148 pageUri: String(window.location.href), 149 pageName: String(document.title || ''), 150 }; 151 var vars = window.hsVars; 152 if (vars && vars.page_id != null && DIGITS.test(String(vars.page_id))) reply.pageId = String(vars.page_id); 153 var portal = hubspotPortal(); 154 if (portal) reply.hsPortalId = portal; 155 var optedOut = readCookie('__hs_opt_out') === 'yes' || readCookie('__hs_do_not_track') !== null; 156 var hutk = readCookie('hubspotutk'); 157 if (!optedOut && hutk && HUTK.test(hutk)) reply.hutk = hutk; 158 return reply; 159 } 160 161 window.addEventListener('message', function (event) { 162 var data = event && event.data; 163 if (!data) return; 164 165 if (data.type === 'dapta-forms:resize') { 166 if (typeof data.height !== 'number') return; 167 if (!isFinite(data.height) || data.height <= 0) return; 168 var frame = isOurFrame(event.source); 169 if (!frame) return; 170 frame.style.height = Math.min(Math.ceil(data.height), MAX_HEIGHT) + 'px'; 171 return; 172 } 173 174 if (data.type === 'dapta-forms:scroll-into-view') { 175 var target = isOurFrame(event.source); 176 if (!target) return; 177 var top = target.getBoundingClientRect().top; 178 var viewport = window.innerHeight || document.documentElement.clientHeight; 179 if (top >= 0 && top < viewport) return; // its top is already on screen 180 var reduce = !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); 181 try { 182 target.scrollIntoView({ behavior: reduce ? 'auto' : 'smooth', block: 'start' }); 183 } catch (_) { 184 target.scrollIntoView(true); // browsers without the options object 185 } 186 return; 187 } 188 189 if (data.type === 'dapta-forms:context-request') { 190 if (data.v !== 1 || typeof data.id !== 'string' || !data.id || data.id.length > 64) return; 191 var asking = isOurFrame(event.source); 192 if (!asking) return; 193 var origin = srcOrigin(asking); 194 // Our forms host only: an iframe marked with the attribute that loads 195 // something else is not our form, whatever it shows. 196 if (!origin || (SCRIPT_ORIGIN && origin !== SCRIPT_ORIGIN)) return; 197 // And it must still show what its own src loaded: one that navigated 198 // elsewhere is not our form any more, and gets nothing. 199 if (event.origin !== origin) return; 200 var off = asking.getAttribute('data-dapta-forms-context') === 'off';
201 try { 202 var reply = off ? { type: 'dapta-forms:context', id: data.id, v: 1, off: true } : pageContext(data.id); 203 event.source.postMessage(reply, origin); 204 } catch (_) { 205 /* the frame is gone; it saves without the page */ 206 } 207 return; 208 } 209 210 if (data.type === 'dapta-forms:redirect') { 211 if (leaving) return; 212 if (typeof data.url !== 'string' || !HTTP_ONLY.test(data.url.trim())) return; 213 if (!isOurFrame(event.source)) return; 214 leaving = true; 215 // Answer BEFORE navigating. The frame falls back to navigating the top 216 // itself when no answer arrives, and two navigations of one top-level 217 // document race â WebKit cancels both and the visitor goes nowhere. 218 try { 219 event.source.postMessage({ type: 'dapta-forms:redirect-ack' }, '*'); 220 } catch (_) { 221 /* the frame is gone; nothing left to tell */ 222 } 223 window.location.href = data.url.trim(); 224 } 225 }); 226})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.