1export class DataLayerHelper { 2 dataLayerLocalStorageKey = "dxp_data_layer"; 3 4 pushPendingDataLayerEvents() { 5 const dataLayerEvents = this.getDataLayerEventsFromLocalStorage().concat(this.getDataLayerEventsFromServer()); 6 for (let i = 0; i < dataLayerEvents.length; i++) { 7 this.pushDataLayerEvent(dataLayerEvents[i]); 8 } 9 10 localStorage.removeItem(this.dataLayerLocalStorageKey); 11 } 12 13 pushDataLayerEvent(dataLayerEvent, toLocalStorage = false) { 14 if (toLocalStorage) { 15 this.addDataLayerEventToLocalStorage(dataLayerEvent); 16 } else { 17 dataLayer.push(dataLayerEvent); 18 } 19 } 20 21 getDataLayerEventsFromServer() { 22 const value = $("[data-dxp-data-layer-events-server]").html().trim(); 23 if (!value) { 24 return []; 25 } 26 27 return JSON.parse(value); 28 } 29 30 getDataLayerEventsFromLocalStorage() { 31 const value = localStorage.getItem(this.dataLayerLocalStorageKey); 32 if (!value) { 33 return []; 34 } 35 36 return JSON.parse(value); 37 } 38 39 addDataLayerEventToLocalStorage(dataLayerEvent) { 40 let value = this.getDataLayerEventsFromLocalStorage(); 41 value.push(dataLayerEvent); 42 localStorage.setItem(this.dataLayerLocalStorageKey, JSON.stringify(value)); 43 } 44} 45 46document.addEventListener('DOMContentLoaded', function () { 47 48 const dxpDataLayer = document.querySelector('.dxp-data-layer-area'); 49 const dxpInputTags = ['INPUT', 'SELECT', 'TEXTAREA']; 50 const dxpClickTags = ['A', 'I', 'BUTTON']; 51 52 // Track fields that have had their validation-error pushed to avoid duplicates 53 const triggeredFields = {}; 54 55 // MutationObserver to detect text content changes or child node additions in .invalid-feedback elements 56 const observer = new MutationObserver(function (mutationsList) { 57 mutationsList.forEach(mutation => { 58 // If the mutation is related to text changes or children being added 59 if (mutation.type === 'characterData' || mutation.type === 'childList') { 60 const invalidFeedback = mutation.target; 61 const isUmbracoFormElement = invalidFeedback.parentElement?.closest('.umbraco-forms-form'); 62 63 if (!isUmbracoFormElement && 64 invalidFeedback.nodeType === Node.ELEMENT_NODE && 65 invalidFeedback.classList.contains('invalid-feedback')) { 66 const fieldName = invalidFeedback.getAttribute('data-validation-messages-for'); 67 const fieldInput = document.querySelector(`[data-input-field="${fieldName}"]`); 68 const fieldOrder = getFieldOrder(fieldInput); 69 const message = invalidFeedback.textContent.trim(); 70 71 // Only trigger event if there is a message content and if it hasn't been triggered yet 72 if (message && !triggeredFields[fieldName]) { 73 // Find the closest parent form and get its ID 74 const form = invalidFeedback.closest('form'); 75 const formId = form ? form.id : null; 76 77 // Push event to dataLayer 78 window.dataLayer = window.dataLayer || []; 79 window.dataLayer.push({ 80 'event': 'validation-error', 81 'validation': { 82 'status': 'invalid', 83 'form': formId, 84 'field': fieldName, 85 'message': message, 86 } 87 }); 88 89 pushFormFieldEvent('formFieldError', invalidFeedback, fieldName, fieldOrder, false); 90 91 // Mark this field as triggered to prevent duplicate events 92 triggeredFields[fieldName] = true; 93 94 // Optional: Reset the triggered state after a timeout to allow for potential updates 95 setTimeout(() => { 96 triggeredFields[fieldName] = false;
97 }, 1000); // Adjust timeout based on how long validation messages stay visible 98 } 99 } 100 } 101 }); 102 }); 103 104 const invalidFeedbackElements = document.querySelectorAll('.invalid-feedback'); 105 invalidFeedbackElements.forEach(element => { 106 observer.observe(element, { characterData: true, childList: true, subtree: true }); 107 }); 108 109 dxpDataLayer.addEventListener('click', (event) => { 110 handleClickEvent(event.target, event); 111 }); 112 113 dxpDataLayer.addEventListener('change', (event) => { 114 pushFormFieldEvent('formFieldEdit', event.target); 115 }); 116 117 dxpDataLayer.addEventListener('focusin', (event) => { 118 handleInputEvent(event.target, event); 119 pushFormFieldEvent('formFieldFocus', event.target); 120 }); 121 122 dxpDataLayer.addEventListener('focusout', (event) => { 123 handleInputEvent(event.target, event); 124 }); 125 126 function handleClickEvent(target, event) { 127 if (dxpClickTags.includes(target.tagName) && !target.closest('[data-tglyr-plugin-id]')) { 128 129 if (!['A', 'BUTTON'].includes(target.tagName)) { 130 target = target.parentElement; 131 } 132 133 const targetHref = target.href || null; 134 const targetName = target.getAttribute('aria-label') || target.id || target.textContent.trim() || 'unknown_element'; 135 pushDataLayerEvent(event.type, 'button_click', targetName); 136 137 if (targetHref) { 138 event.preventDefault(); 139 setTimeout(() => { 140 var anchor = document.createElement('a'); 141 anchor.href = targetHref; 142 anchor.target = target.target; 143 anchor.click(); 144 }, 100); 145 } 146 } 147 } 148 149 function handleInputEvent(target, event) { 150 if (dxpInputTags.includes(target.tagName)) { 151 const targetName = target.id || target.name || 'unknown_input'; 152 const interaction = target.type === 'file' ? 'upload_interaction' : 'input_interaction'; 153 const targetValue = target.type === 'file' ? target.value?.split('.')?.pop() ?? null : null; 154 pushDataLayerEvent(event.type, interaction, targetName, targetValue); 155 } 156 } 157 158 function pushDataLayerEvent(eventType, interactionType, fieldName, value = null) { 159 window.dataLayer = window.dataLayer || []; 160 const eventData = { 161 'event': interactionType, 162 'interaction': { 163 'type': eventType, 164 'field': fieldName 165 } 166 }; 167 if (value) { 168 eventData.interaction.value = value; 169 } 170 window.dataLayer.push(eventData); 171 } 172 173 function pushFormFieldEvent(eventType, target, fieldName = null, fieldOrder = null, checkTag = true) { 174 if (dxpInputTags.includes(target.tagName) || isSelect2Element(target) || !checkTag) { 175 window.dataLayer = window.dataLayer || []; 176 window.dataLayer.push({ 177 'event': eventType, 178 'form': { 179 field: { 180 'name': fieldName ?? getFieldName(target), 181 'order': fieldOrder ?? getFieldOrder(target), 182 'time': ((Date.now() - new Date().setHours(0, 0, 0, 0)) * 10000) 183 } 184 } 185 }); 186 } 187 } 188 189 function getFieldName(target) { 190 const unknown = 'unknown_input'; 191 192 if (!target) { 193 return unknown; 194 } 195 196 if (isSelect2Element(target)) { 197 var select = getSelect2Select(target); 198 return select.name?.split('.')?.pop() || select.id || unknown; 199 } 200 201 return target.name?.split('.')?.pop() || target.id || unknown; 202 } 203 204 function getFieldOrder(target) { 205 const form = target.form; 206 207 if (!form && isSelect2Element(target)) { 208 const select = getSelect2Select(target); 209 if (select) { 210 const selectForm = select.closest('form'); 211 const formFields = getFormFields(selectForm); 212 if (formFields.length) { 213 return [...formFields].indexOf(select); 214 } 215 } 216 return 0; 217 } 218 219 if (!form) { 220 return 0; 221 } 222 223 const formFields = getFormFields(form); 224 return [...formFields].indexOf(target); 225 } 226 227 function getFormFields(form) { 228 229 if (!form) { 230 return []; 231 } 232 233 const isValid = (field) => { 234 if (field.disabled) return false;
235 if (field.readOnly) return false; 236 if (field.type === 'hidden') return false; 237 const style = window.getComputedStyle(field); 238 if (style.display === 'none' || style.visibility === 'hidden') return false; 239 return true; 240 } 241 242 return [...form.elements].filter(isValid); 243 } 244 245 function getSelect2Select(target) { 246 if (isSelect2Element(target)) { 247 const container = target.closest('.select2-container'); 248 const selectItem = container.previousElementSibling; 249 if (selectItem && selectItem.tagName === 'SELECT') { 250 return selectItem; 251 } 252 } 253 254 return null; 255 } 256 257 function isSelect2Element(target) { 258 return target.classList.contains('select2-selection'); 259 } 260});
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.