PageSourceSearch

https://powerscore.com/o/barbri-global-js/marketo-phone.js?t=1790684906000

js powerscore.com collected 2026-10-01 11:26:20 UTC 16,510 bytes, 387 lines download raw bytes

1/**
2 * Global Marketo phone-field enhancement.
3 *
4 * Applies intl-tel-input formatting + validation to the Phone field of EVERY
5 * Marketo form on the page, regardless of how the form was embedded (page
6 * fragment, web content template, pasted embed code, etc). It hooks Marketo's
7 * global MktoForms2.whenReady(), which fires once per form — current and
8 * future — so no per-embed changes are needed.
9 *
10 * - Default country auto-detected from the visitor IP via the first-party geo
11 *   endpoint, falling back to the US.
12 * - Submission is blocked with a contextual inline error for invalid numbers.
13 * - Valid numbers are written back to the Marketo field in E.164 format.
14 */
15(function () {
16    'use strict';
17
18    // Loaded two ways: via the barbri-global-js client extension on content
19    // pages, and via a <script> tag in the classic themes on widget pages.
20    // Guard so it only initializes once if both ever apply to the same page.
21    if (window.__barbriMarketoPhone) return;
22    window.__barbriMarketoPhone = true;
23
24    var ITI_BASE = '/o/barbri-global-js/lib/intl-tel-input';
25
26    // BUMP THIS whenever intl-tel-input-custom.css changes. It's folded into the
27    // CSS ?v= below, so a barbri-global-js-only redeploy busts the cached CSS
28    // without needing a theme redeploy (both this constant and the CSS ship in
29    // the same client extension).
30    var CUSTOM_CSS_BUILD = '4';
31
32    // Cache-bust the custom CSS. Combines the theme deploy timestamp (the classic
33    // themes load us as marketo-phone.js?t=<theme_timestamp>, which changes on every
34    // theme deploy) with CUSTOM_CSS_BUILD above. Either one changing busts the CSS,
35    // so a CX-only deploy is enough. Falls back to a static token if there's no ?t=.
36    var CUSTOM_CSS_VERSION = (function () {
37        var self = document.currentScript ||
38            document.querySelector('script[src*="marketo-phone"]');
39        var src = self && self.getAttribute('src');
40        var match = src && src.match(/[?&]t=([^&]+)/);
41        var themeToken = match ? match[1] : '2';
42        return themeToken + '-' + CUSTOM_CSS_BUILD;
43    })();
44    var ITI_SCRIPT_ATTR = 'data-iti-loader';
45    var GEO_COUNTRY_URL = '/o/barbri-common-rest/v1.0/api/geo/country';
46    var GEO_CACHE_KEY = 'barbri_geo_country';
47    var DEFAULT_COUNTRY = 'us';
48    var STYLE_ID = 'barbri-marketo-phone-styles';
49
50    // intl-tel-input getValidationError() returns a libphonenumber
51    // ValidationResult. This build returns string codes (e.g. "TOO_SHORT");
52    // older builds return the numeric index. Map both so we're version-proof.
53    // A number can be "possible" yet still fail isValidNumber() (right length,
54    // wrong pattern), so IS_POSSIBLE maps to the generic message.
55    // IS_POSSIBLE_LOCAL_ONLY means it parses as a local number missing the
56    // area code, i.e. too short.
57    // The only four messages we ever surface (per spec): too short, too long,
58    // invalid country code, and a generic catch-all for everything else.
59    var PHONE_ERROR_MESSAGES = {
60        IS_POSSIBLE: 'Please enter a valid phone number.',
61        INVALID_COUNTRY_CODE: 'Invalid country code.',
62        TOO_SHORT: 'Phone number is too short.',
63        TOO_LONG: 'Phone number is too long.',
64        IS_POSSIBLE_LOCAL_ONLY: 'Phone number is too short.',
65        INVALID_LENGTH: 'Please enter a valid phone number.',
66        0: 'Please enter a valid phone number.',
67        1: 'Invalid country code.',
68        2: 'Phone number is too short.',
69        3: 'Phone number is too long.',
70        4: 'Phone number is too short.',
71        5: 'Please enter a valid phone number.'
72    };
73    var PHONE_ERROR_DEFAULT = 'Please enter a valid phone number.';
74    // The empty/required case falls under "General / other" per spec.
75    var PHONE_ERROR_REQUIRED = 'Please enter a valid phone number.';
76
77    function ensureStyles() {
78        if (document.getElementById(STYLE_ID)) return;
79        var style = document.createElement('style');
80        style.id = STYLE_ID;
81        style.textContent =
82            '.iti{display:block;width:100%}' +
83            '.iti input.mktoField{width:100%;box-sizing:border-box}' +
84            '.barbri-phone-error{display:block;margin-top:.25rem;' +
85            'color:#d32f2f;font-size:.875rem}';
86        document.head.appendChild(style);
87    }
88
89    function ensureIntlTelInput(onReady) {
90        ensureStyles();
91
92        if (!document.querySelector('link[data-iti-css]')) {
93            var link = document.createElement('link');
94            link.rel = 'stylesheet';
95            link.href = ITI_BASE + '/css/intlTelInput.min.css';
96            link.setAttribute('data-iti-css', '');
97            document.head.appendChild(link);
98        }
99
100        if (!document.querySelector('link[data-iti-css-custom]')) {
101            var customLink = document.createElement('link');
102            customLink.rel = 'stylesheet';
103            customLink.href = ITI_BASE + '/css/intl-tel-input-custom.css?v=' + CUSTOM_CSS_VERSION;
104            customLink.setAttribute('data-iti-css-custom', '');
105            document.head.appendChild(customLink);
106        }
107
108        if (typeof window.intlTelInput === 'function') {
109            onReady();
110            return;
111        }
112
113        var existing = document.querySelector('script[' + ITI_SCRIPT_ATTR + ']');
114        if (existing) {
115            existing.addEventListener('load', onReady);
116            return;
117        }
118
119        var s = document.createElement('script');
120        s.src = ITI_BASE + '/js/intlTelInputWithUtils.min.js';
121        s.async = true;
122        s.setAttribute(ITI_SCRIPT_ATTR, '');
123        s.addEventListener('load', onReady);
124        document.head.appendChild(s);
125    }
126
127    function lookupCountry() {
128        try {
129            var cached = window.sessionStorage.getItem(GEO_CACHE_KEY);
130            if (cached) return Promise.resolve(cached);
131        } catch (e) {}
132
133        return fetch(GEO_COUNTRY_URL, { headers: { Accept: 'application/json' } })
134            .then(function (r) { return r.ok ? r.json() : null; })
135            .then(function (d) {
136                var c = (d && d.country)
137                    ? String(d.country).toLowerCase()
138                    : DEFAULT_COUNTRY;
139                try { window.sessionStorage.setItem(GEO_CACHE_KEY, c); } catch (e) {}
140                return c;
141            })
142            .catch(function () { return DEFAULT_COUNTRY; });
143    }
144
145    function getPhoneError(input) {
146        var wrap = input.closest('.mktoFieldWrap') || input.parentNode;
147        if (!wrap) return null;
148        return wrap.querySelector('.barbri-phone-error');
149    }
150
151    function showPhoneError(input, message) {
152        var wrap = input.closest('.mktoFieldWrap') || input.parentNode;
153        if (!wrap) return;
154        var el = getPhoneError(input);
155        if (!el) {
156            el = document.createElement('span');
157            el.className = 'barbri-phone-error';
158            el.setAttribute('role', 'alert');
159            wrap.appendChild(el);
160        }
161        el.textContent = message;
162        input.setAttribute('aria-invalid', 'true');
163    }
164
165    function clearPhoneError(input) {
166        var el = getPhoneError(input);
167        if (el && el.parentNode) el.parentNode.removeChild(el);
168        input.removeAttribute('aria-invalid');
169    }
170
171    // Marketo renders its own native validation message in a .mktoError node
172    // inside the field wrap. We provide our own inline message for the phone
173    // field, so we strip Marketo's native one to avoid conflicting/duplicate
174    // and mistimed errors (see the observer in enhanceForm).
175    function clearMarketoError(input) {
176        var wrap = input.closest('.mktoFieldWrap');
177        if (!wrap) return;
178        var err = wrap.querySelector('.mktoError');
179        if (err && err.parentNode) err.parentNode.removeChild(err);
180    }
181
182    function enhanceForm(form) {
183        var formEl = null;
184        try { formEl = form.getFormElem()[0]; } catch (e) {}
185        if (!formEl) return;
186
187        var input = formEl.querySelector('#Phone, input[name="Phone"]');
188        if (!input || input.getAttribute('data-iti-enhanced')) return;
189
190        ensureIntlTelInput(function () {
191            if (input.getAttribute('data-iti-enhanced')) return;
192            input.setAttribute('data-iti-enhanced', 'true');
193
194            var prefill = (input.value || '').trim();
195
196            var iti = window.intlTelInput(input, {
197                initialCountry: '',
198                initialCountryLookup: lookupCountry,
199                separateDialCode: true,
200                strictMode: true,
201                placeholderNumberPolicy: 'OFF'
202            });
203
204            if (prefill) {
205                try { iti.setNumber(prefill); } catch (e) {}
206            }
207
208            // True while the country dropdown is open. The selected-country
209            // button exposes aria-expanded, which is reliable even when the
210            // dropdown list is portaled to <body> (so it can't be found by
211            // checking focus/containment inside the .iti wrapper).
212            function isCountryDropdownOpen() {
213                var itiWrap = input.closest('.iti');
214                if (!itiWrap) return false;
215                var btn = itiWrap.querySelector(
216                    '.iti__selected-country, .iti__selected-flag');
217                return !!(btn && btn.getAttribute('aria-expanded') === 'true');
218            }
219
220            // Marketo marks required fields with aria-required and a red
221            // asterisk (.mktoAsterix) in the field's label.
222            function isRequired() {
223                if (input.getAttribute('aria-required') === 'true') return true;
224                var row = input.closest('.mktoFormRow') ||
225                    input.closest('.mktoFieldWrap');
226                return !!(row && row.querySelector(
227                    '.mktoAsterix, .mktoRequiredField'));
228            }
229
230            // Resolve the inline error message for an invalid number from the
231            // libphonenumber validation code. Centralized so the blur and
232            // submit paths can never disagree (getValidationError() can return
233            // a different code — or null — at submit time than on blur, which
234            // previously let the generic message win on submit).
235            function invalidNumberMessage() {
236                var code = iti.getValidationError();
237                var message = PHONE_ERROR_MESSAGES[code];
238                if (!message) {
239                    // Code wasn't a known enum (e.g. null, or IS_POSSIBLE for a
240                    // possible-length-but-wrong-pattern number). Fall back to a
241                    // length-based guess so we still give a specific message
242                    // instead of the generic one where we can.
243                    if (typeof iti.isPossibleNumber === 'function' &&
244                        !iti.isPossibleNumber()) {
245                        message = PHONE_ERROR_MESSAGES.TOO_SHORT;
246                    } else {
247                        message = PHONE_ERROR_DEFAULT;
248                    }
249                }
250                return message;
251            }
252
253            // Show/clear our inline error for the current value. We own all
254            // phone messaging now (Marketo's native popup is stripped below),
255            // so we surface the required-empty case ourselves.
256            function showValidationState() {
257                var val = (input.value || '').trim();
258                if (!val) {
259                    if (isRequired()) {
260                        showPhoneError(input, PHONE_ERROR_REQUIRED);
261                    } else {
262                        clearPhoneError(input);
263                    }
264                    return;
265                }
266                if (iti.isValidNumber()) {
267                    clearPhoneError(input);
268                } else {
269                    showPhoneError(input, invalidNumberMessage());
270                }
271            }
272
273            input.addEventListener('input', function () {
274                clearPhoneError(input);
275            });
276
277            // Validate when the user leaves the field. Marketo only fires its
278            // global onValidate on submit, so without this an invalid number
279            // would show no error until submission.
280            input.addEventListener('blur', function () {
281                // Defer so focus/aria state has settled. Opening the country
282                // dropdown blurs the input but is not a real blur, so skip.
283                window.setTimeout(function () {
284                    var open = isCountryDropdownOpen();
285                    if (open) {
286                        clearMarketoError(input);
287                        return;
288                    }
289                    showValidationState();
290                }, 0);
291            });
292
293            // Re-validate when the selected country changes (the dial code and
294            // thus validity may change). Only when there's already a value —
295            // picking a country before typing shouldn't surface a required
296            // error. Skip entirely while still mid-interaction.
297            input.addEventListener('countrychange', function () {
298                if (isCountryDropdownOpen()) return;
299                if (!(input.value || '').trim()) {
300                    clearPhoneError(input);
301                    return;
302                }
303                showValidationState();
304            });
305
306            // Marketo runs its own native phone validation (on blur, country
307            // change, submit) and renders a "Must be a phone number" popup that
308            // conflicts with ours and fires at the wrong times (e.g. just after
309            // selecting a country). We own all phone messaging via the inline
310            // .barbri-phone-error, so strip Marketo's native popup for this
311            // field whenever it appears. An observer catches it regardless of
312            // when Marketo inserts it.
313            var mktoWrap = input.closest('.mktoFieldWrap') || formEl;
314            var nativeErrorObserver = new MutationObserver(function () {
315                if (mktoWrap.querySelector('.mktoError')) {
316                    clearMarketoError(input);
317                }
318            });
319            nativeErrorObserver.observe(mktoWrap, {
320                childList: true,
321                subtree: true
322            });
323
324            form.onValidate(function () {
325                var val = (input.value || '').trim();
326
327                // We own messaging now, so handle the required-empty case
328                // ourselves (Marketo's native popup is stripped).
329                if (!val) {
330                    if (isRequired()) {
331                        form.submittable(false);
332                        showPhoneError(input, PHONE_ERROR_REQUIRED);
333                    } else {
334                        clearPhoneError(input);
335                    }
336                    return;
337                }
338
339                form.submittable(false);
340
341                if (iti.isValidNumber()) {
342                    clearPhoneError(input);
343                    var values = {};
344                    values[input.name || 'Phone'] = iti.getNumber();
345                    form.setValues(values);
346                    form.submittable(true);
347                } else {
348                    showPhoneError(input, invalidNumberMessage());
349                }
350            });
351        });
352    }
353
354    // MktoForms2 may not exist yet (forms2.min.js loads async, per embed).
355    // Poll until it appears, then register a single whenReady listener.
356    // whenReady replays for already-rendered forms and fires for future ones,
357    // so one registration covers every form for the life of the page.
358    function register() {
359        window.MktoForms2.whenReady(enhanceForm);
360    }
361
362    function init() {
363        if (window.MktoForms2 && typeof window.MktoForms2.whenReady === 'function') {
364            register();
365            return;
366        }
367
368        var attempts = 0;
369        var maxAttempts = 120; // ~60s at 500ms
370        var timer = window.setInterval(function () {
371            attempts++;
372            if (window.MktoForms2 &&
373                typeof window.MktoForms2.whenReady === 'function') {
374                window.clearInterval(timer);
375                register();
376            } else if (attempts >= maxAttempts) {
377                window.clearInterval(timer);
378            }
379        }, 500);
380    }
381
382    if (document.readyState === 'loading') {
383        document.addEventListener('DOMContentLoaded', init);
384    } else {
385        init();
386    }
387}());

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.