PageSourceSearch

https://cdn.searchenginejournal.com/wp-content/themes/sej/js/sej-hubspot-cta.js?ver=1786713541

js searchenginejournal.com collected 2026-09-24 06:23:34 UTC 10,967 bytes, 246 lines download raw bytes

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.