PageSourceSearch

https://orases.com/wp-content/themes/orases/assets/js/hubspot-modifiers.js?ver=1.6.52

js orases.com collected 2026-09-24 22:52:28 UTC 15,072 bytes, 386 lines download raw bytes

1// Store original referrer
2if (!sessionStorage.getItem("original_referrer")) {
3    sessionStorage.setItem("original_referrer", document.referrer || "direct");
4}
5
6// ── Helpers ───────────────────────────────────────────────────────────────────
7
8function getHubspotForms() {
9    return document.querySelectorAll(".hbspt-form form, .hs-form-html form");
10}
11
12function getCookie(name) {
13    const match = document.cookie.match(new RegExp("(^| )" + name + "=([^;]+)"));
14    return match ? match[2] : null;
15}
16
17// ── Geo ───────────────────────────────────────────────────────────────────────
18
19let resolvedGeo = null;
20
21function parseGeoCookie(raw) {
22    try {
23        const decoded = decodeURIComponent(raw);
24        const geoObj = JSON.parse(decoded);
25        const str = typeof geoObj === "object"
26            ? `${geoObj.city}, ${geoObj.region}, ${geoObj.country}`
27            : String(geoObj);
28        return str.includes("undefined") ? null : str;
29    } catch {
30        const cleaned = decodeURIComponent(raw).replace(/"/g, "");
31        return cleaned.includes("undefined") ? null : cleaned;
32    }
33}
34
35function waitForGeoCookie(callback, attempts = 20) {
36    const raw = getCookie("orases_geoip_data");
37    if (raw) {
38        callback(parseGeoCookie(raw));
39    } else if (attempts > 0) {
40        setTimeout(() => waitForGeoCookie(callback, attempts - 1), 500);
41    }
42}
43
44// ── reCAPTCHA v3 ──────────────────────────────────────────────────────────────
45
46const RECAPTCHA_SITE_KEY = orasesConfig.recaptchaSiteKey;
47const RECAPTCHA_VERIFY_ENDPOINT = orasesConfig.recaptchaEndpoint;
48
49// api.js is loaded once, server-side, by wp_enqueue_script() in
50// includes/hubspot-recaptcha.php (declared as this script's dependency) —
51// this file must not load it a second time.
52
53// Generates a fresh token on demand, right before it's needed, rather than
54// polling grecaptcha.execute() in the background every ~2 minutes forever
55// regardless of whether the visitor ever submits a form. Since api.js is
56// only requested when the page actually needs it (no longer preloaded
57// unconditionally), it may not have finished loading/parsing yet if the
58// visitor submits quickly — so this waits (briefly, bounded) for
59// `grecaptcha` to become available rather than failing instantly.
60function getRecaptchaToken() {
61    return new Promise(function(resolve) {
62        function execute() {
63            grecaptcha.ready(function() {
64                grecaptcha.execute(RECAPTCHA_SITE_KEY, { action: "submit" })
65                    .then(function(token) { resolve(token); })
66                    .catch(function() { resolve(null); });
67            });
68        }
69
70        if (typeof grecaptcha !== "undefined") {
71            execute();
72            return;
73        }
74
75        var waitedMs = 0;
76        var MAX_WAIT_MS = 10000;
77        var interval = setInterval(function() {
78            waitedMs += 250;
79            if (typeof grecaptcha !== "undefined") {
80                clearInterval(interval);
81                execute();
82            } else if (waitedMs >= MAX_WAIT_MS) {
83                clearInterval(interval);
84                resolve(null);
85            }
86        }, 250);
87    });
88}
89
90// Generates a fresh token and verifies it against the WordPress endpoint —
91// returns a Promise<boolean>
92function verifyRecaptchaToken() {
93    return getRecaptchaToken().then(function(token) {
94        if (!token) return false;
95
96        return fetch(RECAPTCHA_VERIFY_ENDPOINT, {
97            method:  "POST",
98            headers: { "Content-Type": "application/json" },
99            body:    JSON.stringify({ token: token }),
100        })
101        .then(function(res) { return res.json(); })
102        .then(function(data) { return data.success === true; })
103        .catch(function() { return false; });
104    });
105}
106
107// ── Block restricted countries ────────────────────────────────────────────────
108
109function blockRestrictedCountries(locationStr) {
110    const blockedCountries = ["ME","ZA","IN","NG","BG","PK","BD","TR","SG","LK","PH","VN","OM","IQ","ZM"];
111    const parts = locationStr.split(",");
112    const countryCode = parts.length ? parts[parts.length - 1].trim() : "";
113
114    if (blockedCountries.includes(countryCode)) {
115        getHubspotForms().forEach(form => {
116            const submitBtn = form.querySelector("input[type='submit'], button[type='submit']");
117            if (submitBtn) {
118                submitBtn.disabled = true;
119                submitBtn.style.opacity = "0.5";
120                submitBtn.style.cursor = "not-allowed";
121            }
122            if (!form.querySelector(".geo-block-msg")) {
123                const msg = document.createElement("p");
124                msg.className = "geo-block-msg";
125                msg.innerText = "Sorry, we do not accept submissions from your location.";
126                form.appendChild(msg);
127            }
128        });
129    }
130}
131
132// ── Phone mask ────────────────────────────────────────────────────────────────
133
134function applyPhoneMask() {
135    document.querySelectorAll("input[name$='phone']").forEach(function(phoneInput) {
136        if (phoneInput.dataset.maskApplied) return;
137        phoneInput.dataset.maskApplied = "true";
138        phoneInput.setAttribute("placeholder", "(___) ___-____");
139        phoneInput.setAttribute("maxlength", "14");
140        phoneInput.addEventListener("input", function(e) {
141            let value = e.target.value.replace(/\D/g, "").substring(0, 10);
142            let formatted = "";
143            if (value.length > 0) formatted = "(" + value.substring(0, 3);
144            if (value.length >= 3) formatted = "(" + value.substring(0, 3) + ") ";
145            if (value.length >= 4) formatted += value.substring(3, 6);
146            if (value.length >= 7) formatted += "-" + value.substring(6, 10);
147            e.target.value = formatted;
148        });
149    });
150}
151
152// ── Active fields ─────────────────────────────────────────────────────────────
153
154function markFieldsActive() {
155    const form = document.querySelector('.hs-form-html form');
156    if (!form) return;
157
158    const containers = form.querySelectorAll('.hsfc-Row > div');
159    Array.prototype.forEach.call(containers, function (container) {
160        const isPhoneField = container.classList.contains('hsfc-PhoneField');
161        if (isPhoneField) {
162            const phoneInput = container.querySelector('.hsfc-PhoneInput');
163            phoneInput.addEventListener('click', function () {
164                container.classList.add('active');
165            });
166            return;
167        }
168
169        // Find the wrapper (field) and the actual value-holding input
170        const field = container.querySelector(
171            ':scope > .hsfc-TextInput, :scope > .hsfc-DropdownInput, :scope > .hsfc-TextareaInput, :scope > .hsfc-PhoneField'
172        );
173        if (!field) return;
174
175        // The element that actually holds the value
176        const input = field.matches('.hsfc-DropdownInput')
177            ? field.querySelector('input, select')
178            : field.querySelector('input, textarea') || field;
179
180        // focusin/focusout bubble, so they catch focus on any descendant
181        // (e.g. dropdown options, combobox listbox items)
182        container.addEventListener('focusin', function () {
183            container.classList.add('active');
184        });
185
186        container.addEventListener('focusout', function (e) {
187            // If focus is moving to another element inside this container,
188            // don't deactivate — the user is still interacting with the field
189            if (container.contains(e.relatedTarget)) return;
190
191            if (!input || !input.value) {
192                container.classList.remove('active');
193            }
194        });
195
196        // Pre-filled on load
197        if (input && input.value) {
198            container.classList.add('active');
199        }
200    });
201}
202
203// ── Tracking values ───────────────────────────────────────────────────────────
204
205function getTrackingValues() {
206    return {
207        landing_page:    sessionStorage.getItem("landing_url") || window.location.pathname + window.location.search,
208        conversion_page: window.location.pathname,
209        orases_referrer: sessionStorage.getItem("original_referrer") === "direct"
210                            ? "" : (sessionStorage.getItem("original_referrer") || ""),
211        geolocated:      resolvedGeo || "",
212    };
213}
214
215// ── Stamp DOM fields (belt) ───────────────────────────────────────────────────
216
217function stampDOMFields() {
218    const tracking = getTrackingValues();
219    getHubspotForms().forEach(form => {
220        Object.entries(tracking).forEach(([name, value]) => {
221            form.querySelectorAll(`[name$="${name}"]`).forEach(el => {
222                el.value = value ?? "";
223            });
224        });
225    });
226}
227
228// ── Network interception (suspenders) ────────────────────────────────────────
229// Injects values at the wire level — HubSpot DOM resets cannot affect this
230
231function isHubSpotRequest(body) {
232    if (typeof body === "string") {
233        return body.includes("hs_context") || body.includes("submittedAt");
234    }
235    if (body instanceof FormData) {
236        return body.has("hs_context") || body.has("submittedAt");
237    }
238    return false;
239}
240
241function injectTrackingIntoBody(body) {
242    const tracking = getTrackingValues();
243
244    if (typeof body === "string") {
245        const extra = Object.entries(tracking)
246            .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
247            .join("&");
248        return body + (body ? "&" : "") + extra;
249    }
250
251    if (body instanceof FormData) {
252        Object.entries(tracking).forEach(([k, v]) => body.set(k, v));
253        return body;
254    }
255
256    return body;
257}
258
259function showRecaptchaError() {
260    getHubspotForms().forEach(function(form) {
261        if (form.querySelector(".recaptcha-error-msg")) return;
262        const msg = document.createElement("p");
263        msg.className = "recaptcha-error-msg";
264        msg.style.color = "red";
265        msg.innerText = "Your submission could not be verified. Please try again.";
266        form.appendChild(msg);
267        // Auto-remove after 5 seconds
268        setTimeout(function() { msg.remove(); }, 5000);
269    });
270}
271
272function patchXHR() {
273    const OriginalXHR = window.XMLHttpRequest;
274
275    function PatchedXHR() {
276        const xhr = new OriginalXHR();
277        const originalSend = xhr.send.bind(xhr);
278
279        xhr.send = function(body) {
280            if (!isHubSpotRequest(body)) {
281                originalSend(body);
282                return;
283            }
284
285            // Verify reCAPTCHA before allowing HubSpot submission through
286            verifyRecaptchaToken().then(function(passed) {
287                if (!passed) {
288                    showRecaptchaError();
289                    return; // block — never calls originalSend
290                }
291                originalSend(injectTrackingIntoBody(body));
292            });
293        };
294
295        return xhr;
296    }
297
298    Object.setPrototypeOf(PatchedXHR, OriginalXHR);
299    PatchedXHR.prototype = OriginalXHR.prototype;
300    window.XMLHttpRequest = PatchedXHR;
301}
302
303function patchFetch() {
304    const originalFetch = window.fetch.bind(window);
305
306    window.fetch = function(input, init = {}) {
307        if (!init.body || !isHubSpotRequest(init.body)) {
308            return originalFetch(input, init);
309        }
310
311        // Verify reCAPTCHA before allowing HubSpot submission through
312        return verifyRecaptchaToken().then(function(passed) {
313            if (!passed) {
314                showRecaptchaError();
315                return Promise.reject(new Error("reCAPTCHA verification failed"));
316            }
317            init.body = injectTrackingIntoBody(init.body);
318            return originalFetch(input, init);
319        });
320    };
321}
322
323// ── Only load the form when ready to go ──────────────────────────────────────
324
325function revealFormWhenReady() {
326    document.querySelectorAll('.hs-form-html').forEach(host => {
327        if (host.dataset.readyWatched) return;
328        host.dataset.readyWatched = 'true';
329
330        let quietTimer = null;
331        const QUIET_MS = 300;       // how long of no mutations = "done"
332        const MAX_WAIT = 5000;      // absolute failsafe
333
334        const markReady = () => {
335            if (host.classList.contains('form-ready')) return;
336            // Run all your setup once, right before revealing
337            applyPhoneMask();
338            markFieldsActive();
339            stampDOMFields();
340            host.classList.add('form-ready');
341        };
342
343        const observer = new MutationObserver(() => {
344            clearTimeout(quietTimer);
345            quietTimer = setTimeout(markReady, QUIET_MS);
346        });
347
348        observer.observe(host, { childList: true, subtree: true });
349
350        // Kick off initial timer in case nothing mutates
351        quietTimer = setTimeout(markReady, QUIET_MS);
352
353        // Failsafe — reveal no matter what after MAX_WAIT
354        setTimeout(markReady, MAX_WAIT);
355    });
356}
357
358// ── MutationObserver — re-stamp on HubSpot re-renders ────────────────────────
359
360function watchForReRenders() {
361    document.querySelectorAll(".hbspt-form, .hs-form-html").forEach(container => {
362        new MutationObserver(() => {
363            applyPhoneMask();
364            stampDOMFields();
365        }).observe(container, { childList: true, subtree: true });
366    });
367}
368
369// ── Init ──────────────────────────────────────────────────────────────────────
370
371// Must patch network IMMEDIATELY — before any form loads or submits
372patchXHR();
373patchFetch();
374
375window.addEventListener("load", function() {
376
377    waitForGeoCookie(function(geo) {
378        resolvedGeo = geo;
379
380        setTimeout(function() {
381            revealFormWhenReady();
382            watchForReRenders();
383            if (geo) blockRestrictedCountries(geo);
384        }, 2000);
385    });
386});

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.