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.