1/** 2 * Event tracking (2026 / GA4 + GTM + Meta Pixel), no jQuery, no UA. 3 * 4 * Usage: 5 * - Add class "-js-ga-track" to clickable elements or forms you want tracked. 6 * - Supported data-attributes on the tracked element (or the form itself): 7 * data-ga-cat="contact" 8 * data-ga-label="email" 9 * data-ga-event="contact_click" // optional (defaults: "click" or "submit") 10 * data-ga-name="some_name" // optional 11 * data-ga-val="10" // optional (integer) 12 * data-fb-event="Lead" // optional (Meta Pixel event name override) 13 * 14 * Example: 15 * <a href="mailto:[email protected]" 16 * class="-js-ga-track" 17 * data-ga-cat="contact" 18 * data-ga-label="email" 19 * data-ga-event="contact_click"> 20 * [email protected] 21 * </a> 22 * 23 * Notes: 24 * - Sends GA4 either via gtag() (if present) OR via GTM dataLayer push (if present) OR both. 25 * - For forms, prevents default submission and submits after callback or max wait (700ms). 26 * - For clicks that navigate (normal links), delays navigation briefly to improve delivery. 27 * 28 * Debugging: 29 * - Tries to get a configuration object window.trackEventsConfig, e.g. 30 * 31 * <script> 32 * window.trackEventsConfig = { "debug": true, preventNavigationOnClicks: false, navigationDelay: 3000 }; 33 * </script> 34 * 35 */ 36 37(() => { 38 // Default configuration 39 const defaultConfig = { 40 debug: false, 41 preventNavigationOnClicks: false, 42 navigationDelay: 300, 43 submitDelay: 700 44 }; 45 46 const config = Object.assign( 47 defaultConfig, 48 window.trackEventsConfig || {} 49 ); 50 51 let debug = config.debug; 52 53 if (config.debug) console.log("[track-events] === Debugging enabled ===="); 54 if (config.debug) console.log("[track-events] Configuration", config); 55 56 const state = { 57 hasGA4: false, // gtag 58 hasGTM: false, // dataLayer 59 hasFB: false, // fbq 60 }; 61 62 function init() { 63 state.hasGA4 = typeof window.gtag !== "undefined"; 64 state.hasGTM = typeof window.dataLayer !== "undefined" && Array.isArray(window.dataLayer); 65 state.hasFB = typeof window.fbq !== "undefined"; 66 67 if (config.debug) { 68 console.log("[track-events]", "GTM", state.hasGTM, "FB", state.hasFB, "GA4(gtag)", state.hasGA4); 69 } 70 } 71 init(); 72 73 function parseIntSafe(v) { 74 if (v === undefined || v === null || v === "") return undefined; 75 const n = Number.parseInt(String(v), 10); 76 return Number.isNaN(n) ? undefined : n; 77 } 78 79 function getTrackingElement(target) { 80 if (!target || !(target instanceof Element)) return null; 81 return target.closest(".-js-ga-track"); 82 } 83 84 function buildPayload(elem, actionDefault) { 85 const ds = elem.dataset || {}; 86 87 // event name: data-ga-event overrides default 88 const eventName = (ds.gaEvent && String(ds.gaEvent).trim()) ? String(ds.gaEvent).trim() : actionDefault; 89 90 const category = ds.gaCat ? String(ds.gaCat) : undefined; 91 const label = ds.gaLabel ? String(ds.gaLabel) : undefined; 92 const name = ds.gaName ? String(ds.gaName) : undefined; 93 94 // support both data-ga-val and data-ga-value (legacy mismatch) 95 const value = parseIntSafe(ds.gaVal ?? ds.gaValue); 96 97 // GA4 params (category/label are not ânativeâ GA4 fields, but common custom params) 98 const ga4Params = { 99 event_category: category, 100 event_label: label, 101 event_name: name, 102 value: value, 103 }; 104 105 // Strip undefined values (clean payloads help GTM mappings too) 106 Object.keys(ga4Params).forEach((k) => ga4Params[k] === undefined && delete ga4Params[k]); 107 108 return { eventName, ga4Params }; 109 } 110 111 function sendGA4(eventName, params, done) { 112 // Prefer both if available: gtag direct + GTM push (lets you wire in GTM-only tags too) {{{ 113 let callbackCalled = false; 114 const finish = () => { 115 if (!callbackCalled) { 116 callbackCalled = true; 117 done && done(); 118 } 119 }; 120 121 // gtag() (GA4) 122 if (state.hasGA4) { 123 try { 124 const gaParams = { 125 ...params, 126 transport_type: "beacon", 127 event_callback: finish, 128 debug_mode: config.debug 129 }; 130 131 window.gtag("event", eventName, gaParams); 132 133 } catch (e) { 134 if (config.debug) console.warn("[track-events] gtag failed", e); 135 } 136 } 137 138 // GTM dataLayer push (GA4 tag or other tags can listen) 139 if (state.hasGTM) { 140 try { 141 window.dataLayer.push({ 142 event: eventName, 143 ...params, 144 }); 145 if (config.debug) console.log("[track-events] DATA Layer Params", params); 146 if (config.debug) console.log("[track-events] DATA Layer State", window.dataLayer); 147 } catch (e) { 148 if (config.debug) console.warn("[track-events] dataLayer push failed", e); 149 } 150 } 151 152 // If we didnât send via gtag (no callback), finish quickly. 153 if (!state.hasGA4) { 154 finish(); 155 } 156 // }}} 157 } 158 159 function sendFB(elem, actionDefault) { 160 // {{{ 161 if (!state.hasFB) return; 162 163 const ds = elem.dataset || {}; 164 const fbEventName = 165 (ds.fbEvent && String(ds.fbEvent).trim()) 166 ? String(ds.fbEvent).trim() 167 : `${ds.gaCat || "event"}_${ds.gaLabel || "label"}_${actionDefault}`; 168 169 const val = parseIntSafe(ds.gaVal ?? ds.gaValue); 170 171 try { 172 // Pixel expects params object; keep it minimal 173 const params = {}; 174 if (val !== undefined) params.value = val; 175 if (config.debug) { console.log("[track-events] fbq eventName", fbEventName)} 176 //window.fbq("track", fbEventName, params);
177 window.fbq("trackCustom", fbEventName, params); 178 if (config.debug) { console.log("[track-events] fbq params", params)} 179 } catch (e) { 180 if (config.debug) console.warn("[track-events] fbq failed", e); 181 } 182 // }}} 183 } 184 185 function isNavigatingClick(elem) { 186 // If itâs a normal link that will navigate, delay to help tracking deliver. 187 if (!(elem instanceof Element)) return false; 188 const a = elem.closest("a[href]"); 189 if (!a) return false; 190 191 const href = a.getAttribute("href") || ""; 192 const target = a.getAttribute("target"); 193 194 if (!href) return false; 195 if (target && target.toLowerCase() === "_blank") return false; 196 if (href.startsWith("#")) return false; 197 if (href.startsWith("javascript:")) return false; 198 199 // mailto/tel often open external handlers; still can delay a tiny bit safely 200 return true; 201 } 202 203 // --------- 204 // FORM SUBMIT TRACKING 205 // --------- 206 document.addEventListener( 207 "submit", 208 (ev) => { 209 // call init to be safe for resources that too longer to load 210 init(); 211 212 const form = ev.target; 213 if (!(form instanceof HTMLFormElement)) return; 214 if (form.classList.contains("form-has-errors")) return; 215 if (form.dataset.validator !== undefined && ! form.classList.contains("was-validated")) return; 216 217 // Track forms that have class -js-ga-track 218 if (!form.classList.contains("-js-ga-track")) return; 219 220 const elem = form; // tracking element is the form itself for submit 221 222 const { eventName, ga4Params } = buildPayload(elem, "submit"); 223 224 // If any tracking system exists, delay submit (max 700ms) 225 const shouldDelay = state.hasGA4 || state.hasGTM || state.hasFB; 226 if (!shouldDelay) return; 227 228 let submitForm = null; 229 230 if (form.classList.contains("-js-track-no-delay")) { 231 // 232 } else { 233 ev.preventDefault(); 234 //ev.stopPropagation(); 235 236 let submitted = false; 237 submitForm = () => { 238 if (!submitted) { 239 submitted = true; 240 form.submit(); 241 } 242 }; 243 244 // hard timeout 245 setTimeout(submitForm, config.submitDelay); 246 } 247 248 if (config.debug) console.log("[track-events] submit", eventName, ga4Params); 249 250 sendFB(elem, "submit"); 251 sendGA4(eventName, ga4Params, submitForm); 252 }, 253 true 254 ); 255 256 // --------- 257 // CLICK TRACKING 258 // --------- 259 document.addEventListener( 260 "click", 261 (ev) => { 262 // call init to be safe for resources that too longer to load 263 init(); 264 265 const elem = getTrackingElement(ev.target); 266 if (!elem) return; 267 268 const { eventName, ga4Params } = buildPayload(elem, "click"); 269 270 if (config.debug) console.log("[track-events] click", eventName, ga4Params); 271 272 // If this click will navigate, briefly delay navigation 273 const willNavigate = isNavigatingClick(elem); 274 if (willNavigate && (state.hasGA4 || state.hasGTM || state.hasFB)) { 275 ev.preventDefault(); 276 ev.stopPropagation(); 277 278 const link = elem.closest("a[href]"); 279 const href = link ? link.getAttribute("href") : null; 280 281 let navigated = false; 282 283 const go = () => { 284 if (config.preventNavigationOnClicks) return; 285 if (navigated) return; 286 navigated = true; 287 if (href) window.location.href = href; 288 }; 289 290 // max wait 291 if (config.debug) console.log(`[track-events] waiting ${config.navigationDelay}ms for Navigation`); 292 setTimeout(go, config.navigationDelay); 293 294 sendFB(elem, "click"); 295 sendGA4(eventName, ga4Params, go); 296 return; 297 } 298 299 // Non-navigating clicks: just fire 300 sendFB(elem, "click"); 301 sendGA4(eventName, ga4Params); 302 }, 303 true 304 ); 305 306 // --------- 307 // PHONE / SKYPE / FACETIME AUTO TRACKING 308 // --------- 309 function trackSimpleClick(selector, eventName, params, fbEventName) { 310 document.addEventListener( 311 "click", 312 (ev) => { 313 init(); 314 const el = ev.target instanceof Element ? ev.target.closest(selector) : null; 315 if (!el) return; 316 317 if (config.debug) console.log("[track-events] simple", eventName, params); 318 319 if (state.hasFB && fbEventName) { 320 try { 321 window.fbq("track", fbEventName); 322 } catch (e) { 323 if (config.debug) console.warn("[track-events] fbq failed", e); 324 } 325 } 326 327 sendGA4(eventName, params || {}); 328 }, 329 true 330 ); 331 } 332 333 trackSimpleClick(".-sys-tel-standard", "contact_click", { method: "telephone" }, "contact_click_telephone"); 334 trackSimpleClick(".-sys-tel-skype", "contact_click", { method: "skype" }, "contact_click_skype"); 335 trackSimpleClick(".-sys-tel-facetime", "contact_click", { method: "facetime" }, "contact_click_facetime"); 336 337 // --------- 338 // ADD TO CART 339 // --------- 340 trackSimpleClick(".add-to-cart-button, .-st-add-to-cart", "add_to_cart", {}, "AddToCart"); 341})(); 342
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.