1const dataSent = { 2 "onClick": false, 3 "onUnload": false, 4 "onSubmit": false 5}; 6const modifiedForms = []; 7 8/** 9 * Serializes form data into an object with additional metadata. 10 * @param {FormData} data - FormData object from the form. 11 * @param {HTMLFormElement} originForm - The source form used to gather field details. 12 * @returns {Object} Serialized form data with metadata (field type, tag, etc.). 13 */ 14function serialize(data, originForm) { 15 const obj = {}; 16 for (const [key, value] of data) { 17 const fieldDetails = { 18 value: decodeEscapedEntities(value), // Field value 19 timeStart: null, // Start time of interaction 20 timeEnd: null, // End time of interaction 21 isChanged: false, // Tracks whether the field value has been modified 22 fieldType: getFieldType(originForm, key), // Field type (e.g., text, hidden) 23 fieldTag: getFieldTag(originForm, key) // HTML tag (e.g., INPUT, TEXTAREA) 24 }; 25 if (obj[key]) { 26 // If key already exists, convert it into an array (e.g., for multiple values from the same key) 27 obj[key] = Array.isArray(obj[key]) ? [...obj[key], fieldDetails] : [obj[key], fieldDetails]; 28 } else { 29 obj[key] = fieldDetails; // Create a new entry for the key if it doesn't exist 30 } 31 } 32 return obj; 33} 34 35/** 36 * Retrieves the type of a form field. 37 * @param {HTMLFormElement} form - The form containing the field. 38 * @param {string} fieldName - The name of the field. 39 * @returns {string} Field type as a string or an empty string if undefined. 40 */ 41function getFieldType(form, fieldName) { 42 return form[fieldName]?.type || ""; 43} 44 45/** 46 * Retrieves the HTML tag of a form field. 47 * @param {HTMLFormElement} form - The form containing the field. 48 * @param {string} fieldName - The name of the field. 49 * @returns {string} HTML tag name as a string or an empty string if undefined. 50 */ 51function getFieldTag(form, fieldName) { 52 return form[fieldName]?.tagName || ""; 53} 54 55/** 56 * Updates the data in formA with changes from formB. 57 * @param {Object} formA - Original serialized form data. 58 * @param {Object} formB - Updated serialized form data. 59 */ 60function formDataUpdate(formA, formB) { 61 Object.keys(formA).forEach(key => { 62 const fieldA = formA[key]; 63 const fieldB = formB[key]; 64 if (fieldA && fieldB && fieldA.value !== fieldB.value) { 65 formA[key] = { 66 ...fieldA, 67 value: decodeEscapedEntities(fieldB.value), // Update with the new value 68 isChanged: true, // Mark as changed 69 timeEnd: new Date().toISOString() // Record the time of change 70 }; 71 } 72 }); 73} 74 75/** 76 * Collects all forms in the document that use the POST method. 77 * Only returns forms with a stable id (required for your tracking model). 78 * @returns {Array<HTMLFormElement>} Array of form elements. 79 */ 80function gatherForms() { 81 return Array.from(document.querySelectorAll("form")) 82 .filter((form) => { 83 // Use DOM property because it normalizes method casing. 84 const method = (form.method || form.getAttribute("method") || "").toLowerCase(); 85 if (method !== "post") return false; 86 87 // Enforce your requirement: no id => do not track 88 if (!form.id || !form.id.trim()) return false; 89 90 return true; 91 }); 92} 93 94 95/** 96 * Serializes an array of forms into an array of objects with metadata. 97 * @param {Array<HTMLFormElement>} formArray - Array of form elements. 98 * @returns {Array<Object>} Array of serialized form data. 99 */ 100function serializeForms(formArray) { 101 return formArray.map(form => serialize(new FormData(form), form)); 102} 103 104// ... existing code ... 105/** 106 * Tracks changes and interactions in forms by adding event listeners. 107 * Prevent double-binding by marking the form node. 108 * @param {Array<HTMLFormElement>} formArray - Array of form elements. 109 */ 110function trackForms(formArray) { 111 formArray.forEach((form, index) => { 112 if (form.dataset.pfTracked === "1") { 113 return; 114 } 115 form.dataset.pfTracked = "1"; 116 117 form.addEventListener('focusin', (event) => { 118 if (!modifiedForms.includes(form.id)) { 119 modifiedForms.push(form.id); 120 } 121 if (event.target) { 122 let targetField = getFormElementFromTarget(event.target); 123 const field = serializedFormDataArray[index][targetField.name]; 124 if (field && field.timeStart == null) { 125 field.timeStart = new Date().toISOString(); 126 } 127 } 128 }); 129 130 form.addEventListener('change', (event) => { 131 if (!modifiedForms.includes(form.id)) { 132 modifiedForms.push(form.id); 133 } 134 formDataUpdate(serializedFormDataArray[index], serialize(new FormData(form), form)); 135 }); 136 137 form.addEventListener('input', (event) => { 138 if (!modifiedForms.includes(form.id)) { 139 modifiedForms.push(form.id); 140 } 141 formDataUpdate(serializedFormDataArray[index], serialize(new FormData(form), form)); 142 }); 143 144 form.addEventListener('focusout', (event) => { 145 if (!modifiedForms.includes(form.id)) { 146 modifiedForms.push(form.id); 147 } 148 formDataUpdate(serializedFormDataArray[index], serialize(new FormData(form), form)); 149 }); 150 form.addEventListener('submit', (event) => { 151 if (event.target.innerText.toLowerCase().trim() !== "continue") { 152 if (!dataSent.onSubmit) { 153 pf2dl(form.id, 'submit'); 154 dataSent.onSubmit = true; 155 } 156 } 157 }); 158 159 form.querySelectorAll('input[type=submit]').forEach((submitButton) => { 160 submitButton.addEventListener("click", (event) => { 161 if (event.target.value.toLowerCase().trim() !== "continue") { 162 if (!dataSent.onClick) { 163 pf2dl(form.id, 'click'); 164 dataSent.onClick = true; 165 } 166 } 167 }); 168 }); 169 }); 170} 171 172 173/** 174 * Prepares form data for submission by hashing sensitive fields and removing metadata. 175 * @param {Object} form - Serialized form data. 176 */ 177function prepare(form) { 178 let protectedFields = ["source", "burecruiter", "buemployercde", "agreementcode", "ga_client_id", "ga_ttid", "msclkid", "postal_code", "campaignid", "gclid"];
179 Object.entries(form).forEach(([key, field]) => { 180 // Hash sensitive fields (e.g., text, email, phone) 181 if (field?.fieldType === "text" || field?.fieldType === "email" || field?.fieldType === "tel") { 182 field.value = gUMG.hashThisHard(field.value); 183 } else { 184 field.value = decodeEscapedEntities(field.value); 185 } 186 // Remove hidden fields entirely 187 if (field?.fieldType === "hidden" && !key.toString().includes("utm_") && !protectedFields.includes(key)) { 188 delete form[key]; 189 } else { 190 // Remove metadata 191 // delete field.fieldType; 192 // delete field.fieldTag; 193 } 194 195 }); 196 return form; 197} 198 199/** 200 * Converts serialized form data into a JSON string. 201 * @param {Object} serializedFormData - Serialized form data object. 202 * @returns {string} JSON string representation of the form data. 203 */ 204function goJSON(serializedFormData) { 205 return JSON.stringify(serializedFormData); 206} 207 208function decodeEscapedEntities(text) { 209 const tdiv = document.createElement('div'); 210 tdiv.innerHTML = text; 211 return tdiv.textContent || tdiv.innerText; 212} 213 214let formArray; 215let serializedFormDataArray; 216 217 218function initForms() { 219 formArray = gatherForms(); 220 if (!formArray || formArray.length === 0) { 221 return false; 222 } 223 serializedFormDataArray = serializeForms(formArray); 224 trackForms(formArray); 225 return true; 226} 227 228function initFormsWhenReady() { 229 // First attempt: works for non-modal pages 230 if (initForms()) return; 231 232 // Modal/AJAX pages: observe DOM until the form is injected 233 const mo = new MutationObserver(() => { 234 if (initForms()) { 235 mo.disconnect(); 236 } 237 }); 238 239 mo.observe(document.documentElement, {childList: true, subtree: true}); 240 241 // Stop observing after 15s to avoid running forever on pages where modal never opens 242 setTimeout(() => { 243 mo.disconnect(); 244 }, 15000); 245} 246 247window.addEventListener("load", (event) => { 248 initFormsWhenReady(); 249 setUpTrackedEvents(); 250}); 251 252function getFormName(form) { 253 const qry = ':scope '; 254 let formName = ''; 255 if (form.name) { 256 formName = form.name; 257 } else { 258 let headingsInOrder = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']; 259 let headingsInReverseOrder = headingsInOrder.reverse(); 260 let heading = '' 261 262 headingsInReverseOrder.forEach(headingType => { 263 if (form.querySelectorAll(qry + headingType)[0]) { 264 heading = form.querySelectorAll(qry + headingType)[0]; 265 } 266 }); 267 268 if (heading) { 269 formName = heading.innerText; 270 } else { 271 formName = form.id; 272 } 273 } 274 return formName || 'Unknown Form'; 275} 276 277function submitAttempt(formId) { 278 let formIndex; 279 formArray.forEach((formElement, index) => { 280 if (formElement.id === formId) { 281 formIndex = index; 282 } 283 }); 284 return formIndex; 285} 286 287function pf2dl(formId, submitAction) { 288 window.dataLayer = window.dataLayer || []; 289 let submittedFormID = submitAttempt(formId); 290 if (typeof submittedFormID !== "number") { 291 return; // form not found / not initialized yet 292 } 293 if (!serializedFormDataArray || !serializedFormDataArray[submittedFormID]) { 294 return; // no serialized data available 295 } 296 let preparedForm = prepare(JSON.parse(JSON.stringify(serializedFormDataArray[submittedFormID]))); 297 // add submitAction information to the form payload 298 preparedForm.submitAction = submitAction || 'unknown'; 299 preparedForm.interactionToken = gUMG.interactionToken || 'unknown'; 300 //Mixpanel Form Copy 301 document.mp_form_copy = JSON.parse(JSON.stringify(serializedFormDataArray[submittedFormID])); 302 let formName = getFormName(formArray[submittedFormID]); 303 let formEventObject = {}; 304 formEventObject.event = 'Form Submit - ' + formName; 305 formEventObject['Form Submit - ' + formName] = JSON.parse(JSON.stringify(preparedForm)); 306 window.dataLayer.push(formEventObject); 307} 308 309 310function pushFormChanges(submitAction) {
311 modifiedForms.forEach((modifiedForm) => { 312 pf2dl(modifiedForm, submitAction); 313 }); 314} 315 316function setUpTrackedEvents() { 317 document.addEventListener('visibilitychange', function () { 318 if (document.visibilityState == 'hidden' && !dataSent.onUnload) { 319 pushFormChanges('visibilitychange'); 320 dataSent.onUnload = true; 321 322 } 323 }); 324 document.addEventListener('beforeunload', function () { 325 if (!dataSent.onUnload) { 326 pushFormChanges('beforeunload'); 327 dataSent.onUnload = true; 328 } 329 }); 330} 331 332function getFormElementFromTarget(target) { 333 const formElements = ["input", "select", "button"]; 334 if (formElements.includes(target.tagName.toLowerCase())) { 335 return target; 336 } else { 337 return target.closest("div").querySelectorAll("select, input, button")[0]; 338 } 339}
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.