1/** 2 * SEJ â hide promo elements when a HubSpot CTA (popup) is shown, 3 * + a universal "user is filling a form" detector. 4 * 5 * Targets hidden on CTA: .sej-hello-inner, .sej-sticky-form, .popup-lg 6 * 7 * PART 1 â Universal form-fill detector 8 * Sets a global flag window.is_in_form_fill_state = true while the user is 9 * typing in ANY form field, then clears it after a grace period that GROWS 10 * with sustained typing (â3s after a short burst, â6s, then â9s). Works for 11 * forms on the main page AND inside same-origin iframes (the .popup-lg webinar 12 * form runs in a same-domain iframe). The detector is attached from the main 13 * page to each same-origin iframe's document, so the flag always lives on the 14 * top window where the CTA logic can read it. 15 * 16 * PART 2 â CTA promo-hider 17 * The HubSpot CTA SDK (window.HubSpotCallsToActions) broadcasts its lifecycle 18 * via window.postMessage ({type:'hsCallsToActionCallback', eventName, id}); 19 * that is the reliable signal on SEJ (the hsCallsToActionReady queue is not 20 * drained here). On the first "shown" event we hide the promo elements with an 21 * injected display:none !important stylesheet keyed on a class on <html>, so 22 * elements injected later (they load on scroll) are covered automatically. 23 * 24 * Exception: .popup-lg is NOT hidden if the user is mid-form-fill at that 25 * moment (window.is_in_form_fill_state), so we never yank away a popup whose 26 * form someone is filling. Decision is made once; hide-only, never restored. 27 * 28 * Docs: https://developers.hubspot.com/docs/api-reference/latest/marketing/ctas-sdk/guide 29 */ 30(function () { 31 'use strict'; 32 33 // Flip to false once confirmed working in production. 34 var DEBUG = true; 35 36 function log() { 37 if (DEBUG && window.console && console.log) { 38 console.log.apply(console, ['[sej-hs-cta]'].concat([].slice.call(arguments))); 39 } 40 } 41 42 /* ===================================================================== 43 * PART 1 â Universal form-fill detector -> window.is_in_form_fill_state 44 * ===================================================================== */ 45 (function formFillDetector() { 46 if (window.__sejFillInit) { return; } // guard against double-load 47 window.__sejFillInit = true; 48 window.is_in_form_fill_state = false; 49 50 var burst = 0; // input events in the current typing run 51 var timer = null; 52 53 // Grace window grows with sustained typing. 54 function graceMs(n) { return n <= 2 ? 3000 : (n <= 4 ? 6000 : 9000); } 55 56 function mark() { 57 burst++; 58 window.is_in_form_fill_state = true; 59 if (timer) { clearTimeout(timer); } 60 timer = setTimeout(function () { 61 window.is_in_form_fill_state = false; 62 burst = 0; 63 timer = null; 64 log('form fill state -> false'); 65 }, graceMs(burst)); 66 if (burst === 1) { log('form fill state -> true'); } 67 } 68 69 // 'input' fires only on genuine user entry (typing, pasting, selecting) â 70 // not on programmatic .val() â so it is a clean "is filling" signal. 71 try { document.addEventListener('input', mark, true); } catch (e) {} 72 73 // Same-origin iframes: attach the same listener to their document. 74 function attachToIframe(iframe) { 75 function attach() { 76 var doc; 77 try { doc = iframe.contentDocument; } catch (e) { return; } // cross-origin -> skip 78 if (!doc || doc.__sejFillAttached) { return; } 79 doc.__sejFillAttached = true; 80 try { doc.addEventListener('input', mark, true); log('fill detector attached to iframe doc'); } catch (e) {} 81 } 82 attach(); // in case it is already loaded 83 // Add the load listener once per iframe element (re-attaches after the 84 // iframe (re)navigates); the guard avoids stacking duplicate listeners. 85 if (!iframe.__sejFillWatched) { 86 iframe.__sejFillWatched = true; 87 try { iframe.addEventListener('load', attach); } catch (e) {} 88 } 89 } 90 91 function scanIframes(root) { 92 var frames = (root || document).querySelectorAll('iframe'); 93 for (var i = 0; i < frames.length; i++) { attachToIframe(frames[i]); } 94 } 95 96 // Iframes (e.g. the popup form) are injected on scroll â watch for them. 97 function watchForIframes() { 98 scanIframes(document); 99 if (typeof MutationObserver === 'undefined') { return; } 100 try { 101 new MutationObserver(function (muts) { 102 for (var m = 0; m < muts.length; m++) { 103 var added = muts[m].addedNodes; 104 for (var n = 0; n < added.length; n++) { 105 var node = added[n]; 106 if (!node || node.nodeType !== 1) { continue; } 107 if (node.tagName === 'IFRAME') { attachToIframe(node); } 108 else if (node.querySelectorAll) { scanIframes(node); } 109 } 110 } 111 }).observe(document.documentElement, { childList: true, subtree: true }); 112 } catch (e) { log('iframe watcher failed', e); } 113 } 114 115 if (document.readyState === 'loading') { 116 document.addEventListener('DOMContentLoaded', watchForIframes); 117 } else { 118 watchForIframes(); 119 } 120 })(); 121 122 /* =====================================================================
123 * PART 2 â Hide promo elements when a HubSpot CTA is shown 124 * ===================================================================== */ 125 var ACTIVE_CLASS = 'sej-hs-cta-active'; // always-hide elements 126 var POPUP_HIDE_CLASS = 'sej-hs-cta-hide-popup'; // .popup-lg (only when not filling) 127 var STYLE_ID = 'sej-hs-cta-hide-style'; 128 var ALWAYS_HIDE = ['.sej-hello-inner', '.sej-sticky-form']; 129 var POPUP_SEL = '.popup-lg'; 130 var SHOWN_EVENTS = ['onCallToActionReady', 'onCallToActionViewed']; 131 132 var ctaShown = false; 133 var observer = null; 134 135 function ensureStyle() { 136 if (document.getElementById(STYLE_ID)) { return; } 137 try { 138 var rules = 139 ALWAYS_HIDE.map(function (s) { return 'html.' + ACTIVE_CLASS + ' ' + s; }).join(',') + 140 '{display:none !important;}' + 141 'html.' + POPUP_HIDE_CLASS + ' ' + POPUP_SEL + '{display:none !important;}'; 142 var style = document.createElement('style'); 143 style.id = STYLE_ID; 144 style.textContent = rules; 145 (document.head || document.documentElement).appendChild(style); 146 log('hide stylesheet injected'); 147 } catch (e) { 148 log('failed to inject stylesheet', e); 149 } 150 } 151 152 function hideElement(el, sel) { 153 try { el.style.setProperty('display', 'none', 'important'); log('element appeared -> hidden:', sel); } 154 catch (e) { /* never break the page */ } 155 } 156 157 // Backstop: re-assert hiding on elements added after the CTA fired. 158 function startObserver() { 159 if (observer || typeof MutationObserver === 'undefined' || !document.body) { return; } 160 try { 161 observer = new MutationObserver(function (mutations) { 162 var hidePopup = document.documentElement.classList.contains(POPUP_HIDE_CLASS); 163 var selectors = hidePopup ? ALWAYS_HIDE.concat(POPUP_SEL) : ALWAYS_HIDE; 164 for (var m = 0; m < mutations.length; m++) { 165 var added = mutations[m].addedNodes; 166 for (var n = 0; n < added.length; n++) { 167 var node = added[n]; 168 if (!node || node.nodeType !== 1) { continue; } 169 for (var s = 0; s < selectors.length; s++) { 170 var sel = selectors[s]; 171 if (node.matches && node.matches(sel)) { hideElement(node, sel); } 172 if (node.querySelectorAll) { 173 var found = node.querySelectorAll(sel); 174 for (var k = 0; k < found.length; k++) { hideElement(found[k], sel); } 175 } 176 } 177 } 178 } 179 }); 180 observer.observe(document.body, { childList: true, subtree: true }); 181 log('MutationObserver started'); 182 } catch (e) { 183 log('observer failed to start', e); 184 } 185 } 186 187 function onCtaShown(reason) { 188 if (ctaShown) { return; } 189 ctaShown = true; 190 ensureStyle(); 191 192 // Always hide the hello bar + sticky form. 193 try { document.documentElement.classList.add(ACTIVE_CLASS); } catch (e) {} 194 195 // .popup-lg: keep it visible if the user is filling a form right now, 196 // otherwise hide it (and any popup injected later). 197 if (window.is_in_form_fill_state) { 198 log('popup KEPT â user is filling a form (' + reason + ')'); 199 } else { 200 try { document.documentElement.classList.add(POPUP_HIDE_CLASS); } catch (e) {} 201 log('popup hidden (' + reason + ')'); 202 } 203 204 if (DEBUG) {
205 ALWAYS_HIDE.concat(POPUP_SEL).forEach(function (s) { 206 log('matches present now', s, document.querySelectorAll(s).length); 207 }); 208 } 209 210 startObserver(); 211 log('CTA shown (' + reason + ') -> <html>.' + ACTIVE_CLASS + ' applied'); 212 } 213 214 function onCtaEvent(eventName, id) { 215 log('CTA event:', eventName, id ? '#' + id : ''); 216 if (SHOWN_EVENTS.indexOf(eventName) !== -1) { 217 onCtaShown(eventName + (id ? ' #' + id : '')); 218 } 219 } 220 221 // PRIMARY signal: HubSpot broadcasts the CTA lifecycle via postMessage. 222 window.addEventListener('message', function (ev) { 223 var d = ev && ev.data; 224 if (d && d.type === 'hsCallsToActionCallback') { 225 onCtaEvent(d.eventName, d.id); 226 } 227 }); 228 229 // FALLBACK: bind directly if the SDK object is present, else defer. 230 function bindDirect() { 231 var sdk = window.HubSpotCallsToActions; 232 if (!sdk || typeof sdk.on !== 'function') { return false; } 233 SHOWN_EVENTS.forEach(function (name) { 234 try { sdk.on(name, function (e) { onCtaEvent(name, e && e.id); }); } 235 catch (err) { log('on() failed for', name, err); } 236 }); 237 log('bound directly via HubSpotCallsToActions.on'); 238 return true; 239 } 240 if (!bindDirect()) { 241 window.hsCallsToActionReady = window.hsCallsToActionReady || []; 242 window.hsCallsToActionReady.push(bindDirect); 243 } 244 245 log('initialised; DEBUG =', DEBUG); 246})();
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.