1/** 2 * Gravity Forms â dataLayer Event Tracking 3 * 4 * Tracks form interactions for GTM / GA4 funnel and drop-off analysis. 5 * Events: gf_form_view, gf_field_focus, gf_field_change, gf_field_abandoned, 6 * gf_page_advance, gf_validation_error, gf_field_validation_error, 7 * gf_form_submit, gf_form_abandoned 8 * 9 * Configuration is passed from PHP via `usmrGfDL` (wp_localize_script). 10 * 11 * SHA-256 for user-data hashing is provided by `vendor-sha256.min.js` 12 * (js-sha256, MIT) which exposes `window.sha256` as a synchronous UTF-8 hex helper. 13 * 14 * gf_form_submit can carry two hashed-identifier keys: 15 * meta_user_data_hashed â FROZEN legacy shape `{ em, ph }`, ph = digits only. 16 * Live GTM tags read this; its values must not change. 17 * user_data_hashed â per-platform `{ meta, google, microsoft }`, each 18 * `{ em, ph }`, normalized to each vendor's spec 19 * (E.164 for phones; Microsoft strips email dots/alias). 20 */ 21 22(function () { 23 'use strict'; 24 25 window.dataLayer = window.dataLayer || []; 26 27 var cfg = window.usmrGfDL || {}; 28 var formNames = cfg.formNames || {}; 29 var formTypes = cfg.formTypes || {}; 30 var postName = cfg.postName || ''; 31 var DEBUG = !!cfg.debug; 32 var TIMING = cfg.trackTiming !== false; 33 var ABANDON = cfg.trackAbandon !== false; 34 var INCLUDE_USER_DATA = !!cfg.includeUserData; 35 var BLOCKLIST = Array.isArray(cfg.userDataBlocklist) ? cfg.userDataBlocklist : []; 36 var META_HASHED_IDS = !!cfg.metaHashedIds; 37 var DEFAULT_COUNTRY_CODE = cfg.defaultCountryCode || '+1'; 38 var sha256Hex = typeof window.sha256 === 'function' ? window.sha256 : null; 39 var DEFAULT_EMAIL_META_TOKENS = ['email', 'emailaddress', 'email_address']; 40 var DEFAULT_PHONE_META_TOKENS = ['phone', 'phone_number', 'phonenumber', 'telephone']; 41 var HARD_BLOCKED_TYPES = ['password', 'file', 'hidden']; 42 var VALUE_MAX_LEN = 200; 43 var SS_PREFIX = 'usmrGfValues_'; 44 var SS_META_PREFIX = 'usmrGfMeta_'; 45 var SS_META_HASH_PREFIX = 'usmrGfMetaHash_'; 46 var SS_TTL_MS = 60 * 1000; 47 48 function mergeMetaTokenLists(defaults, extra) { 49 var map = {}; 50 var i; 51 for (i = 0; i < defaults.length; i++) { 52 map[defaults[i]] = true; 53 } 54 var ex = Array.isArray(extra) ? extra : []; 55 for (i = 0; i < ex.length; i++) { 56 var t = String(ex[i] || '').toLowerCase().trim(); 57 if (t) map[t] = true; 58 } 59 return Object.keys(map); 60 } 61 62 var EMAIL_META_TOKENS = mergeMetaTokenLists(DEFAULT_EMAIL_META_TOKENS, cfg.metaEmailTokensExtra); 63 var PHONE_META_TOKENS = mergeMetaTokenLists(DEFAULT_PHONE_META_TOKENS, cfg.metaPhoneTokensExtra); 64 65 /* âââââââââââââââââââââââââââââââââââââââââââââ 66 * UTILITIES 67 * âââââââââââââââââââââââââââââââââââââââââââââ */ 68 69 function getFieldLabel(el) { 70 var gfield = el.closest('.gfield'); 71 if (gfield) { 72 var label = gfield.querySelector('label'); 73 if (label) return label.textContent.trim(); 74 } 75 return ( 76 el.getAttribute('aria-label') || 77 el.getAttribute('placeholder') || 78 el.name || 79 el.id || 80 'unknown' 81 ); 82 } 83 84 function getFormId(el) { 85 var form = el.closest('form[id^="gform_"]'); 86 if (form) { 87 var match = form.id.match(/gform_(\d+)/); 88 if (match) return match[1]; 89 } 90 return 'unknown'; 91 } 92 93 function getFormName(formId) { 94 return formNames[formId] || ''; 95 } 96 97 function getFormType(formId) { 98 if (formTypes[formId]) return formTypes[formId]; 99 100 var formEl = document.getElementById('gform_' + formId); 101 var wrapperEl = formEl ? formEl.closest('.gform_wrapper') : document.getElementById('gform_wrapper_' + formId); 102 103 // Back-compat: some installs still output legacy attributes during migration. 104 var fromDom = ( 105 (formEl && (formEl.getAttribute('usmrformtype') || formEl.getAttribute('data-usmr-form-type'))) || 106 (wrapperEl && (wrapperEl.getAttribute('usmrformtype') || wrapperEl.getAttribute('data-usmr-form-type'))) || 107 '' 108 ); 109 if (fromDom) return fromDom; 110 111 return readPersistedMeta(formId); 112 } 113 114 function push(eventName, params) { 115 var payload = Object.assign({ event: eventName }, params); 116 window.dataLayer.push(payload); 117 if (DEBUG) { 118 console.log('[GF dataLayer]', payload); 119 } 120 } 121 122 function now() { 123 return Date.now ? Date.now() : +new Date(); 124 } 125 126 127 /* âââââââââââââââââââââââââââââââââââââââââââââ 128 * USER-DATA UTILITIES
129 * Sanitize labels into stable GA4-safe keys, filter out 130 * sensitive fields, and truncate values to a safe length. 131 * âââââââââââââââââââââââââââââââââââââââââââââ */ 132 133 function slugifyLabel(label) { 134 if (!label) return ''; 135 return String(label) 136 .toLowerCase() 137 .replace(/\s*\*+\s*$/, '') // trailing "*" (required marker) 138 .replace(/\s*\(required\)\s*$/i, '') // trailing "(required)" 139 .replace(/[^a-z0-9]+/g, '_') 140 .replace(/^_+|_+$/g, ''); 141 } 142 143 function matchesBlocklist(haystack) { 144 if (!haystack) return false; 145 haystack = String(haystack).toLowerCase(); 146 for (var i = 0; i < BLOCKLIST.length; i++) { 147 if (haystack.indexOf(BLOCKLIST[i]) !== -1) return true; 148 } 149 return false; 150 } 151 152 function isBlockedField(el, label) { 153 var type = (el.type || '').toLowerCase(); 154 if (HARD_BLOCKED_TYPES.indexOf(type) !== -1) return true; 155 if (matchesBlocklist(type)) return true; 156 if (matchesBlocklist(el.name)) return true; 157 if (matchesBlocklist(el.id)) return true; 158 if (matchesBlocklist(slugifyLabel(label))) return true; 159 return false; 160 } 161 162 function sanitizeValue(value) { 163 if (value === null || value === undefined) return ''; 164 var str = String(value).replace(/\s+/g, ' ').trim(); 165 if (str.length > VALUE_MAX_LEN) { 166 str = str.slice(0, VALUE_MAX_LEN); 167 } 168 return str; 169 } 170 171 var formValues = {}; 172 173 function cacheFieldValue(el) { 174 if (!INCLUDE_USER_DATA) return; 175 var label = getFieldLabel(el); 176 if (isBlockedField(el, label)) return; 177 var slug = slugifyLabel(label); 178 if (!slug) return; 179 var formId = getFormId(el); 180 formValues[formId] = formValues[formId] || {}; 181 formValues[formId][slug] = sanitizeValue(el.value); 182 } 183 184 function persistFormValues(formId) { 185 if (!INCLUDE_USER_DATA) return; 186 if (!formValues[formId]) return; 187 try { 188 window.sessionStorage.setItem(SS_PREFIX + formId, JSON.stringify({ 189 t: now(), 190 v: formValues[formId] 191 })); 192 } catch (e) { /* sessionStorage unavailable / quota */ } 193 } 194 195 function readPersistedValues(formId) { 196 try { 197 var raw = window.sessionStorage.getItem(SS_PREFIX + formId); 198 if (!raw) return null; 199 window.sessionStorage.removeItem(SS_PREFIX + formId); 200 var parsed = JSON.parse(raw); 201 if (!parsed || typeof parsed !== 'object') return null; 202 if (now() - parsed.t > SS_TTL_MS) return null; 203 return parsed.v || null; 204 } catch (e) { return null; } 205 } 206 207 function persistFormMeta(formId, formEl) { 208 var formType = ( 209 (formEl && (formEl.getAttribute('usmrformtype') || formEl.getAttribute('data-usmr-form-type'))) || 210 '' 211 ); 212 if (!formType) return; 213 try { 214 window.sessionStorage.setItem(SS_META_PREFIX + formId, JSON.stringify({ 215 t: now(), 216 form_type: formType 217 })); 218 } catch (e) { /* sessionStorage unavailable / quota */ } 219 } 220 221 function readPersistedMeta(formId) { 222 try { 223 var raw = window.sessionStorage.getItem(SS_META_PREFIX + formId); 224 if (!raw) return ''; 225 window.sessionStorage.removeItem(SS_META_PREFIX + formId); 226 var parsed = JSON.parse(raw); 227 if (!parsed || typeof parsed !== 'object') return ''; 228 if (now() - parsed.t > SS_TTL_MS) return ''; 229 return parsed.form_type || ''; 230 } catch (e) { return ''; } 231 } 232 233 function attachValues(params, values) { 234 if (!values) return; 235 params.user_data = values; 236 } 237 238 /* âââââââââââââââââââââââââââââââââââââââââââââ 239 * META CAPI â hashed email (em) / phone (ph) on gf_form_submit 240 * Normalized values are hashed with SHA-256 (hex). No raw PII in dataLayer. 241 * âââââââââââââââââââââââââââââââââââââââââââââ */ 242 243 function normalizeMetaHaystackPart(s) { 244 if (!s) return ''; 245 return String(s).toLowerCase().replace(/[^a-z0-9]+/g, '_'); 246 } 247 248 function buildMetaFieldHaystack(el) { 249 var label = getFieldLabel(el); 250 return [ 251 normalizeMetaHaystackPart(slugifyLabel(label)), 252 normalizeMetaHaystackPart(el.name || ''), 253 normalizeMetaHaystackPart(el.id || '') 254 ].join('_'); 255 } 256 257 function longestTokenInHaystack(haystack, tokens) { 258 var sorted = tokens.slice().sort(function (a, b) { return b.length - a.length; }); 259 var ti; 260 for (ti = 0; ti < sorted.length; ti++) { 261 if (haystack.indexOf(sorted[ti]) !== -1) return sorted[ti]; 262 } 263 return ''; 264 } 265 266 function classifyMetaIdentifierField(el) { 267 var tag = el.tagName ? el.tagName.toLowerCase() : ''; 268 if (tag !== 'input') return null; 269 var type = (el.type || '').toLowerCase(); 270 if (type === 'hidden' || type === 'password' || type === 'file' || type === 'submit' || type === 'button' || type === 'checkbox' || type === 'radio') { 271 return null; 272 } 273 if (type === 'email') return 'email'; 274 if (type === 'tel') return 'phone'; 275 var haystack = buildMetaFieldHaystack(el); 276 if (longestTokenInHaystack(haystack, EMAIL_META_TOKENS)) return 'email'; 277 if (longestTokenInHaystack(haystack, PHONE_META_TOKENS)) return 'phone'; 278 return null; 279 } 280 281 function normalizeEmailForMetaHash(value) { 282 if (value === null || value === undefined) return ''; 283 return String(value).trim().toLowerCase(); 284 } 285 286 function normalizePhoneForMetaHash(value) { 287 if (value === null || value === undefined) return ''; 288 return String(value).replace(/\D/g, ''); 289 } 290 291 /** 292 * Coerce a configured country code (`1`, `+1`, `+ 1`, `44`, â¦) to `+` + digits.
293 * Falls back to `+1` when the configured value contains no digits at all. 294 */ 295 function normalizeCountryCode(cc) { 296 var digits = String(cc === null || cc === undefined ? '' : cc).replace(/\D/g, ''); 297 if (!digits) return '+1'; 298 return '+' + digits; 299 } 300 301 /** 302 * Normalize a user-typed phone number to E.164 (`+` + country code + subscriber digits). 303 * 304 * Google Enhanced Conversions and Microsoft Enhanced Conversions both require 305 * E.164 *with* the leading `+` before SHA-256. Meta wants the same digits but 306 * without the `+`. 307 * 308 * HEURISTIC LIMITS: a number typed without a leading `+` carries no reliable 309 * signal about its country. We can only guess: 310 * - a leading `+` is authoritative â trust it and keep the digits as typed 311 * - 11 digits starting with `1` is treated as NANP with the country code already present 312 * - anything else gets the configured default country code prefixed 313 * That last rule will mis-tag an international number a visitor typed in local 314 * form (e.g. a UK "020 7946 0958" on a US-default site). This is exactly why the 315 * default country code is a configurable setting rather than a hardcoded `+1`: 316 * site owners whose traffic is predominantly non-US must set it to match. 317 */ 318 function normalizePhoneE164(value, defaultCc) { 319 if (value === null || value === undefined) return ''; 320 var str = String(value).trim(); 321 var hasPlus = str.charAt(0) === '+'; 322 var digits = str.replace(/\D/g, ''); 323 if (digits.length < 7) return ''; 324 if (hasPlus) return '+' + digits; 325 if (digits.length === 11 && digits.charAt(0) === '1') return '+' + digits; 326 return normalizeCountryCode(defaultCc) + digits; 327 } 328 329 /** 330 * Microsoft Enhanced Conversions normalizes email more aggressively than 331 * Google/Meta: after trim + lowercase it also strips every `.` from the user 332 * portion and truncates at the first `+` alias. The domain is left untouched. 333 */ 334 function normalizeEmailForMicrosoft(value) { 335 if (value === null || value === undefined) return ''; 336 var email = String(value).trim().toLowerCase(); 337 var at = email.indexOf('@'); 338 if (at === -1) return ''; 339 var user = email.slice(0, at).replace(/\./g, ''); 340 var domain = email.slice(at + 1); 341 var plus = user.indexOf('+'); 342 if (plus !== -1) user = user.slice(0, plus); 343 if (!user || !domain) return ''; 344 return user + '@' + domain; 345 } 346 347 function isPlausibleMetaEmail(normalized) { 348 return normalized.length > 3 && normalized.indexOf('@') !== -1 && normalized.indexOf('.', normalized.indexOf('@')) !== -1; 349 } 350 351 function isPlausibleMetaPhone(digits) { 352 return digits.length >= 7; 353 } 354 355 var metaIdentifierValues = {}; 356 357 function collectNormalizedEmailPhoneFromForm(form) { 358 var out = { email: '', phone: '', phoneE164: '' }; 359 if (!form || !form.querySelectorAll) return out; 360 var fields = form.querySelectorAll('.gfield input'); 361 var fi; 362 for (fi = 0; fi < fields.length; fi++) { 363 var el = fields[fi]; 364 var cat = classifyMetaIdentifierField(el); 365 if (!cat) continue; 366 var raw = (el.value || '').trim(); 367 if (!raw) continue; 368 if (cat === 'email' && !out.email) { 369 var ne = normalizeEmailForMetaHash(raw); 370 if (isPlausibleMetaEmail(ne)) out.email = ne; 371 } else if (cat === 'phone' && !out.phone) { 372 var np = normalizePhoneForMetaHash(raw); 373 if (isPlausibleMetaPhone(np)) { 374 out.phone = np; 375 out.phoneE164 = normalizePhoneE164(raw, DEFAULT_COUNTRY_CODE); 376 } 377 } 378 } 379 return out; 380 } 381 382 /** 383 * Legacy shape, frozen: `{ em, ph }` with `ph` hashed from digits-only, no 384 * country code. Live GTM tags read `meta_user_data_hashed` â do not change 385 * these values. Per-platform corrections live in `user_data_hashed` instead. 386 */ 387 function buildMetaUserDataHashedObject(norm) { 388 if (!META_HASHED_IDS || !sha256Hex || !norm) return null; 389 var obj = {}; 390 if (norm.email) obj.em = sha256Hex(norm.email); 391 if (norm.phone) obj.ph = sha256Hex(norm.phone); 392 if (!Object.keys(obj).length) return null; 393 return obj; 394 } 395 396 /** 397 * Per-platform hashed identifiers. Each vendor normalizes differently: 398 * meta â trim/lower email; E.164 digits WITHOUT the leading `+` 399 * google â trim/lower email; full E.164 WITH the `+` 400 * microsoft â dot/alias-stripped email; full E.164 WITH the `+` 401 */ 402 function buildUserDataHashedObject(norm) { 403 if (!META_HASHED_IDS || !sha256Hex || !norm) return null; 404 405 var email = norm.email || '';
406 var e164 = norm.phoneE164 || ''; 407 var msEmail = email ? normalizeEmailForMicrosoft(email) : ''; 408 409 var emHash = email ? sha256Hex(email) : ''; 410 var msEmHash = msEmail ? sha256Hex(msEmail) : ''; 411 var phPlus = e164 ? sha256Hex(e164) : ''; 412 var phBare = e164 ? sha256Hex(e164.replace(/^\+/, '')) : ''; 413 414 var out = {}; 415 416 function addPlatform(key, em, ph) { 417 var obj = {}; 418 if (em) obj.em = em; 419 if (ph) obj.ph = ph; 420 if (Object.keys(obj).length) out[key] = obj; 421 } 422 423 addPlatform('meta', emHash, phBare); 424 addPlatform('google', emHash, phPlus); 425 addPlatform('microsoft', msEmHash, phPlus); 426 427 if (!Object.keys(out).length) return null; 428 return out; 429 } 430 431 function buildHashedPayloads(norm) { 432 return { 433 h: buildMetaUserDataHashedObject(norm), 434 u: buildUserDataHashedObject(norm) 435 }; 436 } 437 438 function cacheMetaIdentifierValue(el) { 439 if (!META_HASHED_IDS || !el) return; 440 var formId = getFormId(el); 441 if (!formId || formId === 'unknown') return; 442 var cat = classifyMetaIdentifierField(el); 443 if (!cat) return; 444 var raw = (el.value || '').trim(); 445 if (!raw) return; 446 metaIdentifierValues[formId] = metaIdentifierValues[formId] || { email: '', phone: '', phoneE164: '' }; 447 if (cat === 'email' && !metaIdentifierValues[formId].email) { 448 var ne = normalizeEmailForMetaHash(raw); 449 if (isPlausibleMetaEmail(ne)) metaIdentifierValues[formId].email = ne; 450 } else if (cat === 'phone' && !metaIdentifierValues[formId].phone) { 451 var np = normalizePhoneForMetaHash(raw); 452 if (isPlausibleMetaPhone(np)) { 453 metaIdentifierValues[formId].phone = np; 454 metaIdentifierValues[formId].phoneE164 = normalizePhoneE164(raw, DEFAULT_COUNTRY_CODE); 455 } 456 } 457 } 458 459 function persistMetaHashesForRedirect(formId, form) { 460 if (!META_HASHED_IDS || !sha256Hex) return; 461 var norm = collectNormalizedEmailPhoneFromForm(form); 462 var payloads = buildHashedPayloads(norm); 463 if (!payloads.h && !payloads.u) return; 464 try { 465 // `h` is the frozen legacy shape; `u` is the per-platform sibling. Both are 466 // already SHA-256 hex â no raw PII ever reaches sessionStorage. 467 window.sessionStorage.setItem(SS_META_HASH_PREFIX + formId, JSON.stringify({ 468 t: now(), 469 h: payloads.h, 470 u: payloads.u 471 })); 472 } catch (e) { /* sessionStorage unavailable / quota */ } 473 } 474 475 function persistMetaHashesFromSubmitButton(el) { 476 if (!META_HASHED_IDS || !sha256Hex || !el || !el.closest) return; 477 var form = el.closest('form[id^="gform_"]'); 478 if (!form) return; 479 var match = form.id.match(/gform_(\d+)/); 480 if (!match) return; 481 persistMetaHashesForRedirect(match[1], form); 482 } 483 484 function hasLegacyHashes(payloads) { 485 return !!(payloads && payloads.h && (payloads.h.em || payloads.h.ph)); 486 } 487 488 /** 489 * Returns `{ h, u }`. Entries written by an older version of this script hold 490 * only `{ t, h }` â `u` is then simply absent and the redirect confirmation 491 * emits `meta_user_data_hashed` alone rather than throwing. 492 */ 493 function readPersistedMetaHashes(formId) { 494 try { 495 var raw = window.sessionStorage.getItem(SS_META_HASH_PREFIX + formId); 496 if (!raw) return null; 497 window.sessionStorage.removeItem(SS_META_HASH_PREFIX + formId); 498 var parsed = JSON.parse(raw); 499 if (!parsed || typeof parsed !== 'object') return null; 500 if (now() - parsed.t > SS_TTL_MS) return null; 501 var h = (parsed.h && typeof parsed.h === 'object') ? parsed.h : null; 502 var u = (parsed.u && typeof parsed.u === 'object') ? parsed.u : null; 503 if (h && !h.em && !h.ph) h = null; 504 if (u && !Object.keys(u).length) u = null; 505 if (!h && !u) return null; 506 return { h: h, u: u }; 507 } catch (e) { return null; } 508 } 509 510 function attachMetaUserDataHashed(params, formId) { 511 if (!META_HASHED_IDS || !sha256Hex) return; 512 513 // Tier 1: hashes persisted at submit time (survives a redirect confirmation). 514 var payloads = readPersistedMetaHashes(formId); 515 516 // Tier 2: in-memory cache from input/change events (survives AJAX DOM swap). 517 if (!hasLegacyHashes(payloads) && metaIdentifierValues[formId]) { 518 payloads = buildHashedPayloads(metaIdentifierValues[formId]); 519 } 520 521 // Tier 3: live re-read of the form still in the DOM. 522 if (!hasLegacyHashes(payloads)) { 523 var formEl = document.getElementById('gform_' + formId); 524 if (formEl) { 525 payloads = buildHashedPayloads(collectNormalizedEmailPhoneFromForm(formEl)); 526 } 527 } 528 529 if (!payloads) return; 530 if (hasLegacyHashes(payloads)) { 531 params.meta_user_data_hashed = payloads.h; 532 } 533 if (payloads.u && Object.keys(payloads.u).length) { 534 params.user_data_hashed = payloads.u; 535 } 536 } 537 538 539 /* âââââââââââââââââââââââââââââââââââââââââââââ 540 * TIMING STATE 541 * Tracks when the user first interacted with each form 542 * so we can report time-on-form at submit / abandon. 543 * âââââââââââââââââââââââââââââââââââââââââââââ */ 544 545 var formStartTimes = {}; 546 var fieldStartTimes = {}; 547 548 function markFormStart(formId) { 549 if (!formStartTimes[formId]) { 550 formStartTimes[formId] = now(); 551 } 552 } 553 554 function getFormDuration(formId) { 555 if (!formStartTimes[formId]) return 0; 556 return Math.round((now() - formStartTimes[formId]) / 1000); 557 } 558 559 560 /* âââââââââââââââââââââââââââââââââââââââââââââ 561 * 1. FORM VIEW 562 * Fires once per form when it becomes visible. 563 * âââââââââââââââââââââââââââââââââââââââââââââ */ 564 565 function trackFormViews() { 566 var observed = {}; 567 568 function observe(forms) {
569 forms.forEach(function (form) { 570 if (observed[form.id]) return; 571 observed[form.id] = true; 572 573 var match = form.id.match(/gform_(\d+)/); 574 var formId = match ? match[1] : 'unknown'; 575 576 if ('IntersectionObserver' in window) { 577 var io = new IntersectionObserver(function (entries, obs) { 578 entries.forEach(function (entry) { 579 if (entry.isIntersecting) { 580 push('gf_form_view', { 581 form_id: formId, 582 form_name: getFormName(formId), 583 form_type: getFormType(formId), 584 page_path: window.location.pathname, 585 page_title: document.title 586 }); 587 obs.disconnect(); 588 } 589 }); 590 }, { threshold: 0.25 }); 591 io.observe(form); 592 } else { 593 push('gf_form_view', { 594 form_id: formId, 595 form_name: getFormName(formId), 596 form_type: getFormType(formId), 597 page_path: window.location.pathname, 598 page_title: document.title 599 }); 600 } 601 }); 602 } 603 604 observe(document.querySelectorAll('form[id^="gform_"]')); 605 606 if (window.jQuery) { 607 jQuery(document).on('gform_post_render', function () { 608 observe(document.querySelectorAll('form[id^="gform_"]')); 609 }); 610 } 611 } 612 613 614 /* âââââââââââââââââââââââââââââââââââââââââââââ 615 * 2. FIELD FOCUS (first interaction with a field) 616 * âââââââââââââââââââââââââââââââââââââââââââââ */ 617 618 var focusedFields = {}; 619 620 document.addEventListener('focus', function (e) { 621 var el = e.target; 622 if (!el.matches || !el.matches('.gfield input, .gfield select, .gfield textarea')) return; 623 624 var formId = getFormId(el); 625 var key = formId + '|' + (el.name || el.id); 626 627 markFormStart(formId); 628 629 if (focusedFields[key]) { 630 if (TIMING) fieldStartTimes[key] = now(); 631 return; 632 } 633 focusedFields[key] = true; 634 if (TIMING) fieldStartTimes[key] = now(); 635 636 push('gf_field_focus', { 637 form_id: formId, 638 form_name: getFormName(formId), 639 form_type: getFormType(formId), 640 field_label: getFieldLabel(el), 641 field_type: el.type || el.tagName.toLowerCase(), 642 field_name: el.name || el.id, 643 page_path: window.location.pathname, 644 page_title: document.title 645 }); 646 }, true); 647 648 649 /* âââââââââââââââââââââââââââââââââââââââââââââ 650 * 3. FIELD CHANGE (value was modified) 651 * âââââââââââââââââââââââââââââââââââââââââââââ */ 652 653 document.addEventListener('change', function (e) { 654 var el = e.target; 655 if (!el.matches || !el.matches('.gfield input, .gfield select, .gfield textarea')) return; 656 if (el.type === 'hidden') return; 657 658 var formId = getFormId(el); 659 var key = formId + '|' + (el.name || el.id); 660 var params = { 661 form_id: formId, 662 form_name: getFormName(formId), 663 form_type: getFormType(formId), 664 field_label: getFieldLabel(el), 665 field_type: el.type || el.tagName.toLowerCase(), 666 field_name: el.name || el.id, 667 page_path: window.location.pathname, 668 page_title: document.title 669 }; 670 671 if (TIMING && fieldStartTimes[key]) { 672 params.field_duration_sec = Math.round((now() - fieldStartTimes[key]) / 1000); 673 } 674 675 cacheFieldValue(el); 676 cacheMetaIdentifierValue(el); 677 678 push('gf_field_change', params); 679 }, true); 680 681 document.addEventListener('input', function (e) { 682 var el = e.target; 683 if (!el.matches || !el.matches('.gfield input, .gfield select, .gfield textarea')) return; 684 if (el.type === 'hidden') return; 685 cacheFieldValue(el); 686 cacheMetaIdentifierValue(el); 687 }, true); 688 689 document.addEventListener('submit', function (e) { 690 var form = e.target; 691 if (!form || !form.id || form.id.indexOf('gform_') !== 0) return; 692 var match = form.id.match(/gform_(\d+)/); 693 if (!match) return; 694 var formId = match[1]; 695 persistFormValues(formId); 696 persistFormMeta(formId, form); 697 persistMetaHashesForRedirect(formId, form); 698 }, true); 699 700 document.addEventListener('click', function (e) { 701 var el = e.target; 702 if (!el || !el.matches) return; 703 if (!el.matches('input[id^="gform_submit_button_"], button[id^="gform_submit_button_"]')) return; 704 persistMetaHashesFromSubmitButton(el); 705 }, true); 706 707 708 /* âââââââââââââââââââââââââââââââââââââââââââââ 709 * 4. FIELD BLUR (left a field empty â drop-off signal) 710 * âââââââââââââââââââââââââââââââââââââââââââââ */ 711 712 document.addEventListener('blur', function (e) { 713 var el = e.target; 714 if (!el.matches || !el.matches('.gfield input, .gfield select, .gfield textarea')) return; 715 if (el.type === 'hidden' || el.type === 'submit') return; 716 717 var isEmpty = el.value === '' || el.value === null; 718 var formId = getFormId(el); 719 var key = formId + '|' + (el.name || el.id); 720 721 if (focusedFields[key] && isEmpty) { 722 push('gf_field_abandoned', { 723 form_id: formId, 724 form_name: getFormName(formId), 725 form_type: getFormType(formId), 726 field_label: getFieldLabel(el), 727 field_type: el.type || el.tagName.toLowerCase(), 728 field_name: el.name || el.id, 729 page_path: window.location.pathname, 730 page_title: document.title 731 }); 732 } 733 }, true); 734 735 736 /* âââââââââââââââââââââââââââââââââââââââââââââ 737 * 5. MULTI-PAGE STEP ADVANCE 738 * âââââââââââââââââââââââââââââââââââââââââââââ */ 739 740 if (window.jQuery) { 741 jQuery(document).on('gform_page_loaded', function (e, formId, currentPage) { 742 push('gf_page_advance', { 743 form_id: String(formId), 744 form_name: getFormName(String(formId)), 745 form_type: getFormType(String(formId)), 746 step_number: currentPage, 747 step_name: 'Step ' + currentPage, 748 page_path: window.location.pathname, 749 page_title: document.title 750 }); 751 }); 752 } 753 754 755 /* âââââââââââââââââââââââââââââââââââââââââââââ 756 * 6. VALIDATION ERRORS 757 * âââââââââââââââââââââââââââââââââââââââââââââ */ 758 759 if (window.jQuery) { 760 jQuery(document).on('gform_post_render', function (e, formId, pageNum) { 761 var form = document.getElementById('gform_' + formId); 762 if (!form) return; 763 764 var errorFields = form.querySelectorAll('.gfield_error'); 765 if (!errorFields.length) return; 766 767 var errorDetails = Array.from(errorFields).map(function (gfield) { 768 var label = gfield.querySelector('label'); 769 var msgEl = gfield.querySelector('.validation_message'); 770 return { 771 label: label ? label.textContent.trim() : 'unknown field', 772 message: msgEl ? msgEl.textContent.trim() : '' 773 }; 774 }); 775 776 var errorLabels = errorDetails.map(function (d) { return d.label; }); 777 var errorMessages = errorDetails.map(function (d) { return d.message; }); 778 779 push('gf_validation_error', { 780 form_id: String(formId), 781 form_name: getFormName(String(formId)), 782 form_type: getFormType(String(formId)), 783 step_number: pageNum, 784 error_count: errorFields.length, 785 error_fields: errorLabels.join(' | '), 786 error_messages: errorMessages.join(' | '), 787 page_path: window.location.pathname, 788 page_title: document.title 789 }); 790
791 errorDetails.forEach(function (detail) { 792 push('gf_field_validation_error', { 793 form_id: String(formId), 794 form_name: getFormName(String(formId)), 795 form_type: getFormType(String(formId)), 796 step_number: pageNum, 797 field_label: detail.label, 798 error_message: detail.message, 799 page_path: window.location.pathname, 800 page_title: document.title 801 }); 802 }); 803 }); 804 } 805 806 807 /* âââââââââââââââââââââââââââââââââââââââââââââ 808 * 7. FORM SUBMIT SUCCESS 809 * âââââââââââââââââââââââââââââââââââââââââââââ */ 810 811 if (window.jQuery) { 812 jQuery(document).on('gform_confirmation_loaded', function (e, formId) { 813 var fid = String(formId); 814 var params = { 815 form_id: fid, 816 form_name: getFormName(fid), 817 form_type: getFormType(fid), 818 post_name: postName, 819 page_path: window.location.pathname, 820 page_title: document.title 821 }; 822 if (TIMING) { 823 params.time_on_form_sec = getFormDuration(fid); 824 } 825 if (INCLUDE_USER_DATA) { 826 attachValues(params, formValues[fid]); 827 } 828 attachMetaUserDataHashed(params, fid); 829 if (window.usmrAttribution) { params.attribution = window.usmrAttribution; } 830 push('gf_form_submit', params); 831 }); 832 } 833 834 // Non-AJAX redirect confirmations 835 (function () { 836 var urlParams = new URLSearchParams(window.location.search); 837 if (urlParams.get('gf_page') === 'confirmation') { 838 var formId = urlParams.get('id') || 'unknown'; 839 var params = { 840 form_id: formId, 841 form_name: getFormName(formId), 842 form_type: getFormType(formId), 843 post_name: postName, 844 page_path: window.location.pathname, 845 page_title: document.title, 846 confirmation_type: 'redirect' 847 }; 848 if (INCLUDE_USER_DATA && formId !== 'unknown') { 849 attachValues(params, readPersistedValues(formId)); 850 } 851 if (formId !== 'unknown') { 852 attachMetaUserDataHashed(params, formId); 853 } 854 if (window.usmrAttribution) { params.attribution = window.usmrAttribution; } 855 push('gf_form_submit', params); 856 } 857 })(); 858 859 860 /* âââââââââââââââââââââââââââââââââââââââââââââ 861 * 8. FORM ABANDONMENT (user navigates away mid-fill) 862 * Only fires if at least one field was interacted with 863 * and the form was NOT successfully submitted. 864 * âââââââââââââââââââââââââââââââââââââââââââââ */ 865 866 if (ABANDON) { 867 var submitted = {}; 868 869 if (window.jQuery) { 870 jQuery(document).on('gform_confirmation_loaded', function (e, formId) { 871 submitted[String(formId)] = true; 872 }); 873 } 874 875 window.addEventListener('beforeunload', function () { 876 Object.keys(formStartTimes).forEach(function (formId) { 877 if (submitted[formId]) return; 878 879 var filledCount = 0; 880 Object.keys(focusedFields).forEach(function (key) { 881 if (key.indexOf(formId + '|') === 0) filledCount++; 882 }); 883 884 if (filledCount === 0) return; 885 886 push('gf_form_abandoned', { 887 form_id: formId, 888 form_name: getFormName(formId), 889 form_type: getFormType(formId), 890 fields_filled: filledCount, 891 time_on_form_sec: getFormDuration(formId), 892 page_path: window.location.pathname, 893 page_title: document.title 894 }); 895 }); 896 }); 897 } 898 899 900 /* âââââââââââââââââââââââââââââââââââââââââââââ 901 * INIT 902 * âââââââââââââââââââââââââââââââââââââââââââââ */ 903 904 if (document.readyState === 'loading') { 905 document.addEventListener('DOMContentLoaded', trackFormViews); 906 } else { 907 trackFormViews(); 908 } 909 910})();
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.