1/** 2 * @file tracking.js 3 * @description This file contains functions for tracking various user interactions on a webpage, such as visibility, clicks, form submissions, and more. 4 */ 5 6/** 7 * Set to store already tracked promo views (robust deduplication) 8 * Using a Set instead of checking _mtm array, as _mtm may be cleared by tag manager 9 */ 10const trackedPromoViews = new Set(); 11 12/** 13 * Debounce function to limit how often a function can be called 14 * @param {Function} func - The function to debounce 15 * @param {number} wait - The debounce delay in milliseconds 16 * @returns {Function} The debounced function 17 */ 18function debounce(func, wait) { 19 let timeout; 20 return function executedFunction(...args) { 21 const later = () => { 22 clearTimeout(timeout); 23 func(...args); 24 }; 25 clearTimeout(timeout); 26 timeout = setTimeout(later, wait); 27 }; 28} 29 30/** 31 * Make contains case insensitive 32 */ 33$.expr[':'].contains = $.expr.createPseudo(function (arg) { 34 return function (elem) { 35 return $(elem).text().toUpperCase().indexOf(arg.toUpperCase()) >= 0; 36 }; 37}); 38 39/** 40 * Document ready function to initialize content groups and dimensions tracking 41 */ 42$(document).ready(function () { 43 getContentGroupsAndDimensions(); 44}); 45 46/** 47 * Flag to track if consent has been given 48 */ 49let consentGiven = false; 50let initialTrackingDone = false; 51 52/** 53 * Handle consent event and trigger visibility tracking 54 */ 55function handleConsentEvent() { 56 if (!consentGiven) { 57 consentGiven = true; 58 setTimeout(function () { 59 if (!initialTrackingDone) { 60 initialTrackingDone = true; 61 trackVisibilityOfElements(); 62 } 63 }, 150); 64 } 65} 66 67/** 68 * Check if _mtm array contains consent events 69 */ 70function checkExistingConsent() { 71 if (typeof window._mtm !== 'undefined' && Array.isArray(window._mtm)) { 72 for (let i = 0; i < window._mtm.length; i++) { 73 const item = window._mtm[i]; 74 if (item && (item.event === 'consent_status' || item.event === 'consent_status_explicit_given')) { 75 handleConsentEvent(); 76 return true; 77 } 78 } 79 } 80 return false; 81} 82 83/** 84 * Poll for consent status - handles both array phase and Matomo phase 85 * This is more reliable than hooking into push which can be overwritten 86 */ 87(function initializeConsentTracking() { 88 // Check immediately 89 if (checkExistingConsent()) { 90 return; 91 } 92 93 // Poll periodically until consent is found or timeout 94 let pollCount = 0; 95 const maxPolls = 100; // 10 seconds max 96 const pollInterval = setInterval(function () { 97 pollCount++; 98 99 if (checkExistingConsent() || pollCount >= maxPolls) { 100 clearInterval(pollInterval); 101 } 102 }, 100); 103})(); 104 105/** 106 * Debounced visibility tracking function 107 * Using 150ms debounce to prevent excessive calls during scroll/resize 108 */ 109const debouncedTrackVisibility = debounce(function () { 110 if (consentGiven) { 111 trackVisibilityOfElements(); 112 } 113}, 150); 114 115/** 116 * Window resize and scroll event to track visibility of elements (debounced) 117 * Only tracks if consent has been given 118 */ 119$(window).on('resize scroll', function () { 120 debouncedTrackVisibility(); 121}); 122 123/** 124 * Document ready function to initialize click tracking of elements 125 */ 126$(document).ready(function () { 127 trackClickOfElements(); 128}); 129 130/** 131 * Descendants that count as a campaign click by default. Prevents tracking clicks 132 * on empty background areas. 133 * 134 * '.select-opener' is excluded: main.js replaces every native select with a 135 * "fakeselect" whose opener is an <a href="javascript:;">. Merely unfolding a dropdown 136 * is never a campaign action, and main.js also triggers that same opener 137 * programmatically when an option is picked, which would count a second time. 138 */ 139const INTERACTIVE_CLICK_SELECTOR = 'a:not(.select-opener), button, input[type="submit"], input[type="button"], [role="button"]'; 140 141/** 142 * Resolve the promo tracking data of a campaign element. 143 * Shared by view and click tracking so both always agree on the promo_product. 144 * @param {HTMLElement} element - Element carrying data-campaign 145 * @returns {{id: string, campaign: string, trackingTitle: string, position: string}|null} 146 * Tracking data, or null if the element is not trackable 147 */ 148function getPromoTrackingData (element) { 149 const $element = $(element); 150 151 // Validation: data-campaign and data-tracking-title must be present 152 const campaign = $element.data('campaign'); 153 const trackingTitle = $element.data('tracking-title'); 154 155 if (!campaign || campaign.length === 0 || !trackingTitle || trackingTitle.length === 0) { 156 return null; // Skip elements without valid data 157 } 158 159 const contentElements = $('.frame-default'). 160 not('.frame-type-gridelements_pi1'); 161 162 let elID = $element.attr('id'); 163 let elCount = contentElements.index($element); 164 165 // Check if whole CE is a trackable element or just parts 166 if (elCount === -1) { 167 const parentCE = $element. 168 closest('.frame-default'). 169 not('.frame-type-gridelements_pi1'); 170 171 if (parentCE.length === 0) { 172 return null; // Skip if no parent CE was found 173 } 174 175 elID = parentCE.attr('id'); 176 elCount = getPositionInDom(contentElements, parentCE, $element); 177 } else { 178 elCount = elCount + '_1'; 179 } 180 181 // Validation: Element ID must be present 182 if (!elID || elID.length === 0) { 183 return null; // Skip elements without ID 184 } 185 186 return { 187 id: elID, 188 campaign: campaign, 189 trackingTitle: trackingTitle, 190 position: elCount, 191 }; 192} 193 194/** 195 * Track visibility of content elements and position of content element 196 */ 197function trackVisibilityOfElements () { 198 const trackingElements = $('[data-campaign]'); 199 $.each(trackingElements, function () { 200 // Track all elements except already tracked elements 201 if ($(this).hasClass('visibility-tracked')) { 202 return; // Skip already tracked elements 203 } 204 205 const trackingData = getPromoTrackingData(this); 206 if (!trackingData) { 207 return; // Skip elements without valid data 208 } 209 210 // Track element visibility (including slider elements with splide__slide--track) 211 checkPositionAndTrackElement(this, trackingData); 212 }); 213} 214 215/** 216 * Track click events of elements 217 * Only tracks clicks on interactive elements (links, buttons) to avoid 218 * tracking clicks on empty background areas 219 */ 220function trackClickOfElements () { 221 const trackingElements = $('[data-campaign]'); 222 $.each(trackingElements, function () { 223 $(this).off('click.promoTracking').on('click.promoTracking', function (e) { 224 // An element may narrow down which descendants count as a campaign click via 225 // data-tracking-click-selector, e.g. the donation teaser where only the submit 226 // button is the campaign action and the amount buttons are not. 227 const clickSelector = $(this).data('tracking-click-selector') || INTERACTIVE_CLICK_SELECTOR; 228 229 if ($(e.target).closest(clickSelector).length === 0) { 230 return; // Skip clicks that are not the campaign action 231 } 232 233 const trackingData = getPromoTrackingData(this); 234 if (!trackingData) { 235 return; // Skip elements without valid data 236 } 237 238 // Push click event with same structure as view event 239 pushPromotionClick(trackingData.id, trackingData.campaign, trackingData.trackingTitle, trackingData.position); 240 }); 241 }); 242} 243 244/** 245 * Push promotion view event to tracking system 246 * @param {string} id - Element ID (with 'c' prefix, e.g., 'c23435') 247 * @param {string} campaignName - Campaign name (data-campaign) - used as first part of promo_product 248 * @param {string} trackingTitle - Tracking title (data-tracking-title) 249 * @param {string} position - Element position 250 */ 251function pushPromoView (id, campaignName, trackingTitle, position) { 252 if (typeof window._mtm !== 'undefined') { 253 // Structure: '[CAMPAIGN]-[POSITION]-[TRACKING_TITLE]' 254 // campaignName = campaign name (e.g., '21') 255 // position = element position (e.g., '3_1') 256 // trackingTitle = tracking title (e.g., 'kymani-tv_image_text_teaser_home') 257 const promoProduct = campaignName + '-' + position + '-' + trackingTitle; 258 259 // Check if this event has already been sent using dedicated Set (robust deduplication) 260 // Using Set instead of checking _mtm array, as _mtm may be cleared by tag manager 261 if (trackedPromoViews.has(promoProduct)) { 262 return; 263 } 264 265 // Mark as tracked before pushing to prevent race conditions 266 trackedPromoViews.add(promoProduct); 267 268 window._mtm = window._mtm || []; 269 window._mtm.push({ 270 'event': 'ecPromoView', 271 'promo_product': promoProduct, 272 }); 273 } 274} 275 276/** 277 * Push promotion click event to tracking system 278 * @param {string} id - Element ID (with 'c' prefix, e.g., 'c23435') 279 * @param {string} campaignName - Campaign name (data-campaign) - used as first part of promo_product 280 * @param {string} trackingTitle - Tracking title (data-tracking-title) 281 * @param {string} position - Element position 282 */ 283function pushPromotionClick (id, campaignName, trackingTitle, position) { 284 if (typeof window._mtm !== 'undefined') { 285 // Structure: '[CAMPAIGN]-[POSITION]-[TRACKING_TITLE]' 286 // campaignName = campaign name (e.g., '21') 287 // position = element position (e.g., '3_1') 288 // trackingTitle = tracking title (e.g., 'image_text_teaser_home') 289 const promoProduct = campaignName + '-' + position + '-' + trackingTitle; 290 291 window._mtm = window._mtm || []; 292 window._mtm.push({ 293 'event': 'ecPromoClick', 294 'promo_product': promoProduct, 295 }); 296 } 297} 298 299/**
300 * Build promo_product string for type-99 banners. 301 * Format: 'banner/[BANNER-NAME]/[BANNER-POSITION]' 302 * @param {jQuery} $banner - The .type-99 banner element 303 * @param {number} positionIndex - Zero-based position among .type-99 banners on the page 304 * @returns {string|null} promo_product string or null if headline is missing 305 */ 306function buildBannerPromoProduct ($banner, positionIndex) { 307 const headline = $banner.find('.banner__headline h4').first().text().trim(); 308 if (!headline) { 309 return null; 310 } 311 const bannerName = slugifyText(headline.toLowerCase()); 312 const bannerPosition = String(positionIndex + 1).padStart(2, '0'); 313 return 'banner/' + bannerName + '/' + bannerPosition; 314} 315 316/** 317 * CSS selector that matches all banners eligible for onsite-campaign promo tracking. 318 */ 319const BANNER_PROMO_SELECTOR = '.type-99, .banner-default'; 320 321/** 322 * Get the current position (0-based) of a banner among all promo-tracked banners 323 * in document order. Computed live so dynamically inserted banners are handled. 324 * @param {HTMLElement} banner 325 * @returns {number} 0-based index, or -1 if not found 326 */ 327function getBannerPromoIndex (banner) { 328 return Array.prototype.indexOf.call(document.querySelectorAll(BANNER_PROMO_SELECTOR), banner); 329} 330 331/** 332 * Initialize onsite-campaign tracking for promo banners (type-99 / banner-default). 333 * - View: fires ecPromoView once per page load when the banner first enters the viewport. 334 * - Click: fires ecPromoClick on every click on an interactive element (link/button) inside the banner. 335 */ 336function initBannerPromoTracking () { 337 const banners = document.querySelectorAll(BANNER_PROMO_SELECTOR); 338 if (banners.length === 0) { 339 return; 340 } 341 342 const viewedBanners = new WeakSet(); 343 344 const fireView = function (banner) { 345 if (viewedBanners.has(banner)) { 346 return; 347 } 348 const index = getBannerPromoIndex(banner); 349 if (index < 0) { 350 return; 351 } 352 const promoProduct = buildBannerPromoProduct($(banner), index); 353 if (!promoProduct) { 354 return; 355 } 356 viewedBanners.add(banner); 357 window._mtm = window._mtm || []; 358 window._mtm.push({ 359 'event': 'ecPromoView', 360 'promo_product': promoProduct, 361 }); 362 }; 363 364 if ('IntersectionObserver' in window) { 365 const observer = new IntersectionObserver(function (entries) { 366 entries.forEach(function (entry) { 367 if (entry.isIntersecting) { 368 fireView(entry.target); 369 observer.unobserve(entry.target); 370 } 371 }); 372 }); 373 banners.forEach(function (banner) { 374 observer.observe(banner); 375 }); 376 } else { 377 // Fallback for browsers without IntersectionObserver: fire view immediately 378 banners.forEach(fireView); 379 } 380 381 // Click tracking on interactive elements (links/buttons) inside the banner. 382 // Delegated on document so dynamically inserted banners work too. 383 const clickSelector = '.type-99 a, .type-99 button, .banner-default a, .banner-default button'; 384 $(document).on('click.bannerPromoTracking', clickSelector, function () { 385 const banner = $(this).closest(BANNER_PROMO_SELECTOR).get(0); 386 if (!banner) { 387 return; 388 } 389 const index = getBannerPromoIndex(banner); 390 if (index < 0) { 391 return; 392 } 393 const promoProduct = buildBannerPromoProduct($(banner), index); 394 if (!promoProduct) { 395 return; 396 } 397 window._mtm = window._mtm || []; 398 window._mtm.push({ 399 'event': 'ecPromoClick', 400 'promo_product': promoProduct, 401 }); 402 }); 403} 404 405/** 406 * Check position and track element if visible 407 * @param {HTMLElement} element - Element to check 408 * @param {{id: string, campaign: string, trackingTitle: string, position: string}} trackingData 409 * Tracking data as resolved by getPromoTrackingData() 410 */ 411function checkPositionAndTrackElement (element, trackingData) { 412 // Check whether element is visible in DOM (display: none, etc.) 413 if (!$(element).is(':visible')) { 414 return; 415 } 416 417 const position = element.getBoundingClientRect(); 418 const elementHeight = position.bottom - position.top; 419 const viewportHeight = window.innerHeight; 420 421 // Calculate how much of the element is visible in the viewport 422 const visibleTop = Math.max(0, position.top);
423 const visibleBottom = Math.min(viewportHeight, position.bottom); 424 const visibleHeight = Math.max(0, visibleBottom - visibleTop); 425 426 // Element must be at least 50% visible AND at least 50px must be in viewport 427 // This prevents tracking of barely visible elements 428 const percentVisible = elementHeight > 0 ? visibleHeight / elementHeight : 0; 429 const minPixelsVisible = 50; 430 const minPercentVisible = 0.5; 431 432 // Check for sufficient visibility: 433 // - At least 50% of the element is visible, OR 434 // - At least 50px is visible (for very large elements) 435 // - AND the element is actually in the viewport 436 const isSufficientlyVisible = (percentVisible >= minPercentVisible || visibleHeight >= minPixelsVisible) && 437 visibleHeight > 0 && 438 position.top < viewportHeight && 439 position.bottom > 0; 440 441 if (isSufficientlyVisible) { 442 pushPromoView(trackingData.id, trackingData.campaign, trackingData.trackingTitle, trackingData.position); 443 $(element).addClass('visibility-tracked'); 444 } 445} 446 447/** 448 * Get position of element in DOM 449 * @param {jQuery} contentElements - Content elements 450 * @param {jQuery} parentCE - Parent content element 451 * @param {jQuery} element - Element to get position for 452 * @returns {string} Element position 453 */ 454function getPositionInDom (contentElements, parentCE, element) { 455 let elCount = contentElements.index(parentCE); 456 if (element.hasClass('splide__slide--thumb')) { 457 const sliderElements = parentCE.find('.splide__slide--thumb'); 458 elCount = elCount + '_' + (sliderElements.index(element) + 1); 459 } else if (element.hasClass('splide__slide--track')) { 460 const sliderElements = parentCE.find('.splide__slide--track'); 461 elCount = elCount + '_' + (sliderElements.index(element) + 1); 462 } else if (element.hasClass('focus-module__teaser')) { 463 const sliderElements = parentCE.find('.focus-module__teaser'); 464 elCount = elCount + '_' + (sliderElements.index(element) + 1); 465 } else if (element.hasClass('image-text-teaser')) { 466 const sliderElements = parentCE.find('.image-text-teaser'); 467 elCount = elCount + '_' + (sliderElements.index(element) + 1); 468 } else if (element.hasClass('icon-text-link')) { 469 const sliderElements = parentCE.find('.icon-text-link'); 470 elCount = elCount + '_' + (sliderElements.index(element) + 2); 471 } else if (element.hasClass('donation-opportunities__teaser')) { 472 const sliderElements = parentCE.find('.donation-opportunities__teaser'); 473 elCount = elCount + '_' + (sliderElements.index(element) + 1); 474 } else if (element.hasClass('donation_efficiency_slide')) { 475 const sliderElements = parentCE.find('.donation_efficiency_slide'); 476 elCount = elCount + '_' + (sliderElements.index(element) + 1); 477 } else if (element.hasClass('artikel-teaser')) { 478 const teaserElements = parentCE.find('.artikel-teaser'); 479 elCount = elCount + '_' + (teaserElements.index(element) + 1); 480 } else if (element.hasClass('event-teaser')) { 481 const teaserElements = parentCE.find('.event-teaser'); 482 elCount = elCount + '_' + (teaserElements.index(element) + 1); 483 } else if (element.hasClass('buehne-fix__link')) { 484 elCount = elCount + '_' + '1'; 485 } 486 return elCount; 487} 488 489/** 490 * Tracking for gifts 491 */ 492if (typeof giftTracking !== 'undefined') { 493 if (typeof window._mtm !== 'undefined') { 494 window._mtm.push(giftTracking); 495 } 496} 497 498function handleNavDonationButtonsTracking(mobileBreakpoint) { 499 /** 500 * Track navigation donation buttons 501 */ 502 const navDonationButtonTop = $('.search-and-donate .donate:visible'); 503 const megaformDonationButtonOnce = $('.mega-form__buttons .donate[value="1"]'); 504 const megaformDonationButtonRegular = $('.mega-form__buttons .donate[value="2"]'); 505 const navDonationButtonOnceMobile = $('.donate-buttons.mobile-only .donate.arrow'); 506 const navDonationButtonRegularMobile = $('.donate-buttons.mobile-only .donate.outline.mobile-only'); 507 508 // Special handling for top button with dynamic label 509 if (navDonationButtonTop.length > 0) { 510 $.each(navDonationButtonTop, function () { 511 $(this).click(function () { 512 const label = mobileBreakpoint.matches ? 'navigation-top-mobile' : 'navigation-top-desktop'; 513 trackCustomEvent('button', 'spenden', label); 514 }); 515 }); 516 } 517 if (megaformDonationButtonOnce.length > 0) { 518 trackButtons(megaformDonationButtonOnce, 'navigation-hero-einmalig-desktop', 'spenden', true); 519 } 520 if (megaformDonationButtonRegular.length > 0) { 521 trackButtons(megaformDonationButtonRegular, 'navigation-hero-regelmaessig-desktop', 'spenden', true); 522 } 523 if (navDonationButtonOnceMobile.length > 0) { 524 trackButtons(navDonationButtonOnceMobile, 'navigation-sub-einmalig-mobile', 'spenden', true); 525 } 526 if (navDonationButtonRegularMobile.length > 0) { 527 trackButtons(navDonationButtonRegularMobile, 'navigation-sub-regelmaessig-mobile', 'spenden', true); 528 } 529} 530 531const BETTERPLACE_FUNDRAISING_URL = 'https://www.betterplace.org/de/fundraising-events'; 532 533$(document).ready(function () { 534 /** 535 * Tracking for donation and newsletter buttons 536 * ['.search-and-donate .donate:visible' and 'header .donate:visible' are now tracked via handleNavDonationButtonsTracking()] 537 */ 538 const donationButtonsHeader = $('.whh-main-nav .donate:visible,' + 539 'header ul li#elem_87 a:visible,' + 540 'header ul li#elem_1664 a:visible,' + 541 '.logo-menu-wrap .donate,' + 542 'header ul li#elem_1713 a' 543 ); 544 545 if (donationButtonsHeader.length > 0) { 546 trackButtons(donationButtonsHeader, 'topnavigation', 'spenden'); 547 } 548 549 const mobileBreakpoint = window.matchMedia('(max-width: 767px)'); 550 handleNavDonationButtonsTracking(mobileBreakpoint); 551 552 /**
553 * Track whitepaper form submission 554 */ 555 const whitepaperForm = $('form.powermail_form_67'); 556 if (whitepaperForm.length > 0) { 557 whitepaperForm.on('submit', function() { 558 trackCustomEvent('kontaktformular', 'whitepaper-plan-a-to-gen-z', 'whitepaper_bestellen'); 559 }); 560 } 561 562 /** 563 * Track betterplace fundraising event links 564 */ 565 const fundraisingEventsLinks = $(`a.link-unterseiten-teaser[href^="${BETTERPLACE_FUNDRAISING_URL}"]`); 566 if (fundraisingEventsLinks.length > 0) { 567 fundraisingEventsLinks.on('click', function (event) { 568 let label = event.currentTarget.title; 569 trackCustomEvent('klick', 'spendenaktion/betterplace.org', label, 1); 570 }); 571 } 572 573 574 if (donationButtonsHeader.length > 0) { 575 trackButtons(donationButtonsHeader, 'topnavigation', 'spenden'); 576 } 577 578 // Get all donation buttons in content 579 const baseContent = '#main-content .content:not(.content-spendenseite)'; 580 const formsWithEndings = 'form[action$="/spenden/"], form[action$="/spenden"], form[action$="/donate/"], form[action$="/donate"], form[action$="/donation/"], form[action$="/donation"], form[action$="https://www.welthungerhilfe.de/spenden/"], form[action$="https://www.welthungerhilfe.org/spenden/"], form[action$="https://www.welthungerhilfe.de/donate/"], form[action$="https://www.welthungerhilfe.org/donate/"], form[action$="https://www.welthungerhilfe.de/donation/"], form[action$="https://www.welthungerhilfe.org/donation/"], .regular-donation-teaser__form, .common-donation-teaser__form'; 581 const formsWithEndingsDash = 'form[action$="/spenden-"], form[action$="/donate-"], form[action$="/donation-"], form[action$="https://www.welthungerhilfe.de/spenden-"], form[action$="https://www.welthungerhilfe.org/spenden-"], form[action$="https://www.welthungerhilfe.de/donate-"], form[action$="https://www.welthungerhilfe.org/donate-"], form[action$="https://www.welthungerhilfe.de/donation-"], form[action$="https://www.welthungerhilfe.org/donation-"]'; 582 const formsWithQuestion = 'form[action*="/spenden/?"]:not(.spendenformular), form[action*="/donate/?"]:not(.spendenformular), form[action*="/donation/?"]:not(.spendenformular), form[action*="https://www.welthungerhilfe.de/spenden/?"]:not(.spendenformular), form[action*="https://www.welthungerhilfe.org/spenden/?"]:not(.spendenformular), form[action*="https://www.welthungerhilfe.de/donate/?"]:not(.spendenformular), form[action*="https://www.welthungerhilfe.org/donate/?"]:not(.spendenformular), form[action*="https://www.welthungerhilfe.de/donation/?"]:not(.spendenformular), form[action*="https://www.welthungerhilfe.org/donation/?"]:not(.spendenformular)'; 583 const formsWithStarts = 'form[action^="/jetzt-spenden-und-helfen"]:not(.spendenformular), form[action^="https://www.welthungerhilfe.de/jetzt-spenden-und-helfen"]:not(.spendenformular), form[action^="https://www.welthungerhilfe.org/jetzt-spenden-und-helfen"]:not(.spendenformular)'; 584 const mainsliderLinks = '.mainslider a[href^="/spenden"], .mainslider a[href^="/donate"], .mainslider a[href^="/donation"], .mainslider a[href^="https://www.welthungerhilfe.de/spenden"], .mainslider a[href^="https://www.welthungerhilfe.org/spenden"], .mainslider a[href^="https://www.welthungerhilfe.de/donate"], .mainslider a[href^="https://www.welthungerhilfe.org/donate"], .mainslider a[href^="https://www.welthungerhilfe.de/donation"], .mainslider a[href^="https://www.welthungerhilfe.org/donation"]'; 585 const buttonsWithEndings = 'a.button[href^="/spenden-"], a.button[href^="/donate-"], a.button[href^="/donation-"], a.button[href^="https://www.welthungerhilfe.de/spenden-"], a.button[href^="https://www.welthungerhilfe.org/spenden-"], a.button[href^="https://www.welthungerhilfe.de/donate-"], a.button[href^="https://www.welthungerhilfe.org/donate-"], a.button[href^="https://www.welthungerhilfe.de/donation-"], a.button[href^="https://www.welthungerhilfe.org/donation-"]'; 586 const buttonsWithEndingsSlash = 'a.button[href^="/spenden"], a.button[href^="/donate"], a.button[href^="/donation"], a.button[href^="https://www.welthungerhilfe.de/spenden/"], a.button[href^="https://www.welthungerhilfe.org/spenden"], a.button[href^="https://www.welthungerhilfe.de/donate"], a.button[href^="https://www.welthungerhilfe.org/donate"], a.button[href^="https://www.welthungerhilfe.de/donation"], a.button[href^="https://www.welthungerhilfe.org/donation"]'; 587 const buttonsWithQuestion = 'a.button[href*="/spenden/?"], a.button[href*="/donate/?"], a.button[href*="/donation/?"], a.button[href*="https://www.welthungerhilfe.de/spenden/?"], a.button[href*="https://www.welthungerhilfe.org/spenden/?"], a.button[href*="https://www.welthungerhilfe.de/donate/?"], a.button[href*="https://www.welthungerhilfe.org/donate/?"], a.button[href*="https://www.welthungerhilfe.de/donation/?"], a.button[href*="https://www.welthungerhilfe.org/donation/?"]'; 588 const buttonsWithStarts = 'a.button[href^="/jetzt-spenden-und-helfen"], a.button[href^="https://www.welthungerhilfe.de/jetzt-spenden-und-helfen"], a.button[href^="https://www.welthungerhilfe.org/jetzt-spenden-und-helfen"]'; 589 590 const selector = [ 591 `${baseContent} ${formsWithEndings},`, 592 `${baseContent} ${formsWithEndingsDash},`, 593 `${baseContent} ${formsWithQuestion},`, 594 `${baseContent} ${formsWithStarts},`, 595 `${baseContent} ${mainsliderLinks},`, 596 `${baseContent} ${buttonsWithEndings},`, 597 `${baseContent} ${buttonsWithEndingsSlash},`, 598 `${baseContent} ${buttonsWithQuestion},`, 599 `${baseContent} ${buttonsWithStarts}`, 600 ].join(' '); 601 602 // Get the visible buttons that are not cancel buttons 603 const donationButtonsContent = $( 604 selector + ':visible button[type!="button"]').not('.landingpage-kit__donation-tab-form'); 605 606 if (donationButtonsContent.length > 0) { 607 trackButtons(donationButtonsContent, 'content', 'spenden'); 608 } 609 610 const donationButtonsFooter = $( 611 'footer form[action$="/spenden/"] button:visible,' + 612 'footer form[action$="/donate/"] button:visible,' + 613 'footer form[action$="/donation/"] button:visible,' + 614 'footer form[action*="/spenden/?"] button:visible,' + 615 'footer form[action*="/donate/?"] button:visible,' + 616 'footer form[action*="/donation/?"] button:visible,' + 617 'footer a.button[href$="/spenden/"]:visible,' + 618 'footer a.button[href$="/donate/"]:visible,' + 619 'footer a.button[href$="/donation/"]:visible,' + 620 'footer a.button[href*="/spenden/?"]:visible,' + 621 'footer a.button[href*="/donate/?"]:visible,' + 622 'footer a.button[href*="/donation/?"]:visible'); 623 624 if (donationButtonsFooter.length > 0) { 625 trackButtons(donationButtonsFooter, 'footer', 'spenden'); 626 } 627 628 const newsletterButtonsContent = $( 629 '#main-content form[action$="newsletter/"][action!="/welternaehrung/newsletter/"] button'); 630 if (newsletterButtonsContent.length > 0) { 631 trackButtons(newsletterButtonsContent, 'content', 'newsletter'); 632 } 633 634 const newsletterButtonsWorldFoodSupply = $( 635 '#main-content form[action="/welternaehrung/newsletter/"] button'); 636 if (newsletterButtonsWorldFoodSupply.length > 0) { 637 trackButtons(newsletterButtonsWorldFoodSupply, 'welternaehrung-content', 638 'newsletter'); 639 } 640 641 const newsletterButtonsWorldFoodSupplyHeader = $('#elem_3100 a'); 642 if (newsletterButtonsWorldFoodSupplyHeader.length > 0) { 643 trackButtons(newsletterButtonsWorldFoodSupplyHeader, 644 'welternaehrung-header', 'newsletter'); 645 } 646 647 const newsletterButtonsFooter = $('footer form[action="/newsletter"] button'); 648 if (newsletterButtonsFooter.length > 0) { 649 trackButtons(newsletterButtonsFooter, 'subfooter', 'newsletter'); 650 } 651 652 initBannerPromoTracking(); 653 654 const pollForm = $('.content-leadout .miniumfrage form'); 655 if (pollForm) { 656 pollForm.on('submit', function () { 657 trackPollFormSubmit($(this)); 658 }); 659 } 660 661 /** 662 * Track poll form submission (new version for detailed tracking) 663 */ 664 const detailedPollForm = $('.tx-poll form.poll') 665 if (detailedPollForm.length > 0) { 666 detailedPollForm.on('submit', function (e) { 667 trackDetailedPollFormSubmit($(this)) 668 }) 669 } 670 671 /** 672 * Track donation form submission 673 */ 674 const donationForm = $('.spendenformular form'); 675 if (donationForm) { 676 donationForm.on('submit', function () { 677 trackDonationFormSubmit($(this)); 678 }); 679 } 680 681 /** 682 * Track contact form submission 683 */ 684 const contactForm = $('form.powermail_form'); 685 if (contactForm) { 686 contactForm.on('submit', function () { 687 trackContactFormSubmit($(this)); 688 }); 689 } 690 691 /** 692 * Tracking for file downloads 693 */ 694 trackDownloads(); 695 696 /** 697 * Track Social Media Links in Footer 698 */ 699 const socialMediaLinksFooter = $('footer .social-link'); 700 if (socialMediaLinksFooter.length > 0) { 701 trackSocialMediaLinks(socialMediaLinksFooter); 702 } 703 704 /** 705 * Track Phone links 706 */ 707 const phoneLinks = $('a[href^="tel:"]'); 708 if (phoneLinks.length > 0) { 709 trackPhoneLinks(phoneLinks); 710 } 711 712 /** 713 * Track Mail links 714 */ 715 const mailLinks = $( 716 'a[href^="javascript:linkTo_UnCryptMailto"],a[data-mailto-token]'); 717 if (mailLinks.length > 0) { 718 trackMailLinks(mailLinks); 719 } 720 721 /** 722 * Track Print links 723 */ 724 const printLinks = $('a[href="javascript:window.print();"]'); 725 if (printLinks.length > 0) { 726 trackPrintLinks(printLinks); 727 } 728 729 /** 730 * Track Social Media Share Links 731 */ 732 const socialMediaShareLinks = $('.whh-share a[data-social]'); 733 if (socialMediaShareLinks.length > 0) { 734 trackSocialMediaShareLinks(socialMediaShareLinks); 735 } 736 737 /** 738 * Track Related Content 739 */ 740 const relatedLinks = $('.related-content a, .relatedcontent a'); 741 if (relatedLinks.length > 0) { 742 trackRelatedLinks(relatedLinks); 743 } 744 745 /** 746 * Track donation tab 747 */ 748 if (document.querySelector('form.landingpage-kit__donation-tab-form')) { 749 const donationTab = document.querySelector( 750 'form.landingpage-kit__donation-tab-form button'); 751 if (donationTab) { 752 donationTab.addEventListener('click', function () { 753 trackCustomEvent('button', 'spenden', 'seitenlasche-1-1'); 754 }); 755 } 756 } 757 758 /** 759 * Track the event of copying the IBAN from the footer. 760 * If the element with ID 'footerCopyIban' exists, add an event listener to track the click event. 761 */ 762 const ibanClickCopyButtons = document.querySelectorAll( 763 '#footerCopyIban, .copy-iban'); 764 if (ibanClickCopyButtons.length > 0) { 765 ibanClickCopyButtons.forEach(button => { 766 button.addEventListener('click', function () { 767 trackCustomEvent('klick', 'funktion', 'iban-kopieren'); 768 }); 769 }); 770 } 771 772 /** 773 * Track currency selection on donation forms (.org) 774 */ 775 const currencyRadios = document.querySelectorAll( 776 'input[name="tx_igxmicropayment_micropayment[newDonation][currency]"]'); 777 if (currencyRadios.length > 0) { 778 currencyRadios.forEach(radio => { 779 radio.addEventListener('change', function () { 780 const currency = this.value.toLowerCase(); 781 trackCustomEvent('waehrung', 'klick', 'auswahl-' + currency); 782 }); 783 }); 784 } 785}); 786 787/** 788 * Track button clicks. 789 * @param {jQuery} buttons - The buttons to track. 790 * @param {string} area - The area where the buttons are located. 791 * @param {string} type - The type of buttons (e.g., 'spenden'). 792 * @param {boolean} isNav=false - Whether the buttons are in the navigation. 793 */ 794function trackButtons (buttons, area, type, isNav = false) { 795 const elCount = buttons.length; 796 $.each(buttons, function () { 797 $(this).click(function () { 798 let label = area; 799 if (type === 'spenden' && !isNav) { 800 label = area + '-' + (buttons.index($(this)) + 1) + '-' + elCount; 801 } 802 trackCustomEvent('button', type, label); 803 }); 804 }); 805} 806 807/** 808 * Track the submission of a donation form. 809 * @param {jQuery} form - The donation form to track. 810 */ 811function trackDonationFormSubmit (form) { 812 if (typeof window._mtm !== 'undefined') { 813 let amount = ''; 814 let rhythm = ''; 815 let variant = '';
816 let category = ''; 817 const formDataArray = form.serializeArray(); 818 $(formDataArray).each(function () { 819 if (this.name == 'tx_igxmicropayment_micropayment[newDonation][amount]') { 820 amount = this.value; 821 } 822 if (this.name == 'tx_igxmicropayment_micropayment[newDonation][rhythm]') { 823 rhythm = this.value; 824 } 825 }); 826 switch (rhythm) { 827 case '1': 828 category = 'einzelspende'; 829 variant = '0'; 830 break; 831 case '2': 832 category = 'dauerspende'; 833 variant = '1'; 834 break; 835 case '3': 836 category = 'dauerspende'; 837 variant = '3'; 838 break; 839 case '4': 840 category = 'dauerspende'; 841 variant = '6'; 842 break; 843 case '5': 844 category = 'dauerspende'; 845 variant = '12'; 846 break; 847 default: 848 category = 'einzelspende'; 849 variant = '0'; 850 } 851 const selectedOption = form.find( 852 'select[name="tx_igxmicropayment_micropayment[newDonation][purpose]"] option:selected'); 853 const purpose = selectedOption.data('title') || selectedOption.text(); 854 window._mtm.push({ 855 'event': 'ecAdd', 856 'products': [ 857 { 858 type: 'c_add', 859 name: purpose, 860 pid: category, 861 price: amount, 862 var1: variant, 863 group: category, 864 count: 1, 865 var3: '', 866 }], 867 }); 868 } 869} 870 871/** 872 * Track the submission of a contact form. 873 * Ensures English forms (.org) are tracked with German labels for consistency. 874 * @param {jQuery} form - The contact form to track. 875 */ 876function trackContactFormSubmit (form) { 877 if (typeof window._mtm !== 'undefined') { 878 // Mapping for English topic values to German equivalents 879 const topicMapping = { 880 'general': 'allgemein', 881 'donations': 'spenden' 882 }; 883 884 // Mapping for English submit button texts to German equivalents 885 const submitButtonMapping = { 886 'send_enquiry': 'anfrage_senden' 887 }; 888 889 let subject = null; 890 const formDataArray = form.serializeArray(); 891 $(formDataArray).each(function () { 892 // Check for German field (thema) or English field (topic) 893 if (this.name == 'tx_powermail_pi1[field][thema]') { 894 subject = this.value; 895 subject = subject.toLowerCase(); 896 } else if (this.name == 'tx_powermail_pi1[field][topic]') { 897 subject = this.value; 898 subject = subject.toLowerCase(); 899 // Map English topic to German if mapping exists 900 if (topicMapping[subject]) { 901 subject = topicMapping[subject]; 902 } 903 } 904 }); 905 if (!subject) { 906 const pathArray = window.location.pathname.split('/'); 907 if (pathArray[pathArray.length - 1] !== '' && pathArray.length > 1) { 908 subject = pathArray[pathArray.length - 1]; 909 } else { 910 subject = pathArray[pathArray.length - 2]; 911 } 912 } 913 914 let submitButtonText = form.find('button[type="submit"]').html(); 915 if (submitButtonText) { 916 submitButtonText = submitButtonText.replace(/ /g, '_'); 917 submitButtonText = submitButtonText.toLowerCase(); 918 // Map English submit button text to German if mapping exists 919 if (submitButtonMapping[submitButtonText]) { 920 submitButtonText = submitButtonMapping[submitButtonText]; 921 } 922 } 923 if (subject && submitButtonText) { 924 trackCustomEvent('kontaktformular', subject, submitButtonText); 925 } 926 } 927} 928 929/** 930 * Track the submission of a poll form. 931 * @param {jQuery} form - The poll form to track. 932 */ 933function trackPollFormSubmit (form) { 934 if (typeof window._mtm !== 'undefined') { 935 let subject = null; 936 const formDataArray = form.serializeArray(); 937 let eventLabel = ''; 938 $(formDataArray).each(function () { 939 if (this.name == 'whh_umfrage[antwort]') { 940 subject = this.value; 941 if (subject) { 942 const label = form.find('.text-label[for=antwort_' + subject + ']'); 943 if (label.length > 0) { 944 let labelText = $(label[0]).html(); 945 labelText = labelText.replace(/ /g, '_'); 946 eventLabel = labelText.toLowerCase(); 947 } 948 } 949 } 950 }); 951 if (subject && eventLabel) { 952 trackCustomEvent('klick', 'antwort-absenden', eventLabel); 953 } 954 } 955} 956 957/** 958 * Track the submission of a detailed poll form with multiple questions 959 * @param {jQuery} form - The detailed poll form to track. 960 */ 961function trackDetailedPollFormSubmit (form) { 962 if (typeof window._mtm === 'undefined') { 963 return 964 } 965 966 // Get poll name from form dynamically (needs to be determined first for error tracking) 967 let pollName = 'spende' // default fallback 968 const pollContainer = form.closest('.tx-poll') 969 if (pollContainer.length > 0) { 970 const pollTitle = pollContainer.find('h2').first().text().trim() 971 if (pollTitle) { 972 // Convert poll title to URL-friendly format (slugify) 973 pollName = slugifyText(pollTitle.toLowerCase()) 974 } 975 } 976 977 // Client-side validation: Check if all required questions are answered 978 // Required questions are marked with * at the end of the question text 979 const unansweredRequiredQuestions = [] 980 form.find('.poll-question').each(function () { 981 const questionContainer = $(this) 982 const questionText = questionContainer.find('.poll-question-label p').text().trim() 983 984 // Check if question is required (ends with *) 985 if (!questionText.endsWith('*')) { 986 return // Skip non-required questions 987 } 988 989 const questionNumber = questionContainer.find('.poll-question-number').text().trim() 990 let isAnswered = false 991 992 // Check answer based on question type 993 if (questionContainer.hasClass('single-user-answer')) { 994 // Free text question - check if text is entered 995 const userAnswerField = questionContainer.find('input[type="text"]') 996 isAnswered = userAnswerField.length > 0 && userAnswerField.val().trim().length > 0 997 } else if (questionContainer.hasClass('multiple')) { 998 // Multiple choice question - check if at least one checkbox is checked 999 isAnswered = questionContainer.find('input[type="checkbox"]:checked').length > 0 1000 } else if (questionContainer.hasClass('single')) { 1001 // Single choice question - check if a radio button is selected 1002 isAnswered = questionContainer.find('input[type="radio"]:checked').length > 0 1003 } 1004 1005 if (!isAnswered) { 1006 const fullQuestion = questionNumber ? questionNumber + ' ' + questionText : questionText 1007 unansweredRequiredQuestions.push(fullQuestion) 1008 } 1009 }) 1010 1011 // If required questions are unanswered, track error event and do not track answers 1012 if (unansweredRequiredQuestions.length > 0) { 1013 trackCustomEvent( 1014 'onsite-umfrage-' + pollName, 1015 'eingabefehler', 1016 'fehlende-frage-' + unansweredRequiredQuestions.join('_') 1017 ) 1018 return 1019 } 1020 1021 // Process each question 1022 form.find('.poll-question').each(function () { 1023 const questionContainer = $(this) 1024 const questionNumber = questionContainer.find('.poll-question-number').text().trim() 1025 const questionText = questionContainer.find('.poll-question-label p').text().trim() 1026 1027 if (!questionText) { 1028 return 1029 } 1030 1031 const questionAction = 'frage-' + questionNumber + ' ' + questionText 1032 let answerLabel = '' 1033 1034 // Check question type and get answers 1035 if (questionContainer.hasClass('single-user-answer')) { 1036 // Free text question - track whether filled or not 1037 const userAnswerField = questionContainer.find('input[type="text"]') 1038 if (userAnswerField.length > 0) { 1039 const userAnswerValue = userAnswerField.val().trim() 1040 answerLabel = userAnswerValue.length > 0 ? 'ausgefüllt' : 'nichtausgefüllt' 1041 } 1042 } else if (questionContainer.hasClass('multiple')) { 1043 // Multiple choice question 1044 const selectedAnswers = [] 1045 questionContainer.find('input[type="checkbox"]:checked').each(function () { 1046 const answerText = $(this).siblings('.poll-radio-label').text().trim() 1047 if (answerText) { 1048 selectedAnswers.push(answerText) 1049 } 1050 }) 1051 answerLabel = selectedAnswers.join('_') 1052 } else if (questionContainer.hasClass('single')) { 1053 // Single choice question 1054 const selectedRadio = questionContainer.find('input[type="radio"]:checked') 1055 if (selectedRadio.length > 0) { 1056 const answerText = selectedRadio.siblings('.poll-radio-label').text().trim() 1057 // For scaled questions, extract just the number if answer starts with a digit 1058 // This works for both "1 - Very bad" and "1 - Sehr schlecht" format 1059 const match = answerText.match(/^(\d+)/) 1060 if (match) { 1061 answerLabel = match[1] 1062 } else { 1063 answerLabel = answerText 1064 } 1065 } 1066 } 1067 1068 // Push tracking event if we have an answer 1069 if (answerLabel) { 1070 trackCustomEvent( 1071 'onsite-umfrage-' + pollName, 1072 questionAction, 1073 'antwort-' + answerLabel 1074 ) 1075 } 1076 }) 1077} 1078 1079/** 1080 * Track file downloads. 1081 * Adds click event listeners to all anchor tags that link to downloadable files. 1082 */ 1083function trackDownloads () { 1084 $('a').each(function (i, el) { 1085 const hrefValue = el.href; 1086 if (/\.(jpeg|jpg|png|gif|doc|pdf|docx|txt|odt)$/.test(hrefValue)) { 1087 const fileExtension = el.href.split('.').pop(); 1088 const filename = el.href.match(/([^\/]+)(?=\.\w+$)/)[0]; 1089 $(this).click(function () { 1090 trackCustomEvent('klick', 'download', fileExtension + '_' + filename); 1091 }); 1092 } 1093 }); 1094} 1095 1096/** 1097 * Track social media link clicks. 1098 * @param {jQuery} socialLinks - The social media links to track. 1099 */ 1100function trackSocialMediaLinks (socialLinks) { 1101 $.each(socialLinks, function () { 1102 $(this).click(function () { 1103 let platform = ''; 1104 if ($(this).hasClass('fb')) { 1105 platform = 'facebook'; 1106 } else if ($(this).hasClass('tw')) { 1107 platform = 'twitter'; 1108 } else if ($(this).hasClass('yt')) { 1109 platform = 'youtube'; 1110 } else if ($(this).hasClass('ig')) { 1111 platform = 'instagram'; 1112 } else if ($(this).hasClass('tt')) { 1113 platform = 'tiktok'; 1114 } else if ($(this).hasClass('li')) { 1115 platform = 'linkedin'; 1116 } 1117 if (platform) { 1118 trackCustomEvent('footer', 'social', platform); 1119 } 1120 }); 1121 }); 1122} 1123 1124/** 1125 * Track phone link clicks. 1126 * @param {jQuery}
1126 phoneLinks - The phone links to track. 1127 */ 1128function trackPhoneLinks (phoneLinks) { 1129 $.each(phoneLinks, function () { 1130 $(this).click(function (e) { 1131 trackCustomEvent('klick', 'funktion', 'telefonnummer'); 1132 }); 1133 }); 1134} 1135 1136/** 1137 * Track mail link clicks. 1138 * @param {jQuery} mailLinks - The mail links to track. 1139 */ 1140function trackMailLinks (mailLinks) { 1141 $.each(mailLinks, function () { 1142 $(this).click(function (e) { 1143 trackCustomEvent('klick', 'funktion', 'mailto-link-null'); 1144 }); 1145 }); 1146} 1147 1148/** 1149 * Track print link clicks. 1150 * @param {jQuery} printLinks - The print links to track. 1151 */ 1152function trackPrintLinks (printLinks) { 1153 $.each(printLinks, function () { 1154 $(this).click(function (e) { 1155 trackCustomEvent('klick', 'funktion', 'drucken'); 1156 }); 1157 }); 1158} 1159 1160/** 1161 * Track social media share link clicks. 1162 * @param {jQuery} socialShareLinks - The social media share links to track. 1163 */ 1164function trackSocialMediaShareLinks (socialShareLinks) { 1165 $.each(socialShareLinks, function () { 1166 $(this).click(function () { 1167 const label = $(this).data('social').toLowerCase(); 1168 trackCustomEvent('klick', 'social-share', label); 1169 }); 1170 }); 1171} 1172 1173/** 1174 * Track related content link clicks. 1175 * @param {jQuery} relatedLinks - The related content links to track. 1176 */ 1177function trackRelatedLinks (relatedLinks) { 1178 $.each(relatedLinks, function () { 1179 $(this).click(function () { 1180 const label = $(this).attr('href'); 1181 trackCustomEvent('klick', 'related-content', label); 1182 }); 1183 }); 1184} 1185 1186/** 1187 * Track the page type and push it to the tracking system. 1188 */ 1189function getContentGroupsAndDimensions () { 1190 // track Page Type 1191 if (typeof trackingPageType !== 'undefined') { 1192 if (typeof window._mtm !== 'undefined') { 1193 window._mtm.push({ 1194 'page_type': trackingPageType, 1195 }); 1196 } 1197 } 1198} 1199 1200/** 1201 * Track a custom event. 1202 * @param {string} eventCategory - The category of the event. 1203 * @param {string} eventAction - The action of the event. 1204 * @param {string} eventLabel - The label of the event. 1205 * @param {number|null} eventValue - The value of the event (optional). 1206 */ 1207function trackCustomEvent (eventCategory, eventAction, eventLabel, eventValue = null) { 1208 if (typeof window._mtm !== 'undefined') { 1209 const eventData = { 1210 'event': 'customEvent', 1211 'event_category': eventCategory, 1212 'event_action': eventAction, 1213 'event_label': eventLabel, 1214 }; 1215 1216 if (eventValue !== null) { 1217 eventData['event_value'] = eventValue; 1218 } 1219 1220 window._mtm.push(eventData); 1221 } 1222} 1223 1224/** 1225 * Check if the 'q' parameter exists and has a non-empty value.
1226 * If it does, push an object containing the internal search query and search hits to the '_mtm' array. 1227 */ 1228if (getUrlParameter('q') && getUrlParameter('q').length > 0 && 1229 $('.search-result-wrapper').length > 0) { 1230 // Check if the '_mtm' property exists on the window object 1231 if (typeof window._mtm !== 'undefined') { 1232 // Push an object containing internal search query and search hits to the '_mtm' array 1233 1234 // default results to 0 1235 let results = 0; 1236 // check if there are results and results aren't undefined and set results to the number of results 1237 if ($('.result-total').length && $('.result-total').html() !== undefined && 1238 $('.result-total').html() !== null) { 1239 results = $('.result-total').html(); 1240 } 1241 1242 window._mtm.push({ 1243 'internal_search_query': getUrlParameter('q'), 1244 'internal_search_hits': results, 1245 }); 1246 } 1247} 1248 1249/** 1250 * Check if the 'wc' parameter exists and has a non-empty value. 1251 * If it does, push an object containing the wc code to the '_mtm' array. 1252 */ 1253if (getUrlParameter('wc') && getUrlParameter('wc').length > 0) { 1254 // Check if the '_mtm' property exists on the window object 1255 if (typeof window._mtm !== 'undefined') {
1256 // Push an object containing wc code to the '_mtm' array 1257 window._mtm.push({ 1258 'werbecode': getUrlParameter('wc'), 1259 }); 1260 } 1261} 1262 1263/** 1264 * Track navigation events. 1265 * Adds click event listeners to the specified selector and tracks the event with the given category, action, and prefix. 1266 * @param {string} selector - The selector for the elements to track. 1267 * @param {string} eventCategory - The category of the event. 1268 * @param {string} eventAction - The action of the event. 1269 * @param {string} eventPrefix - The prefix for the event label. 1270 */ 1271 1272function trackNavigation (selector, eventCategory, eventAction, eventPrefix) { 1273 const links = $(selector); 1274 if (links.length > 0) { 1275 links.on('click', function (event) { 1276 // check if submenu icon is clicked 1277 if ($(event.target).closest('.submenu-opener').length) { 1278 // do nothing 1279 return; 1280 } 1281 const linkTarget = $(this).attr('href'); 1282 const linkTargetTracking = linkTarget.replace(/^\//, ''). 1283 replace(/\//g, '_'); 1284 trackCustomEvent(eventCategory, eventAction, 1285 eventPrefix + linkTargetTracking); 1286 }); 1287 } 1288} 1289 1290/** 1291 * Track breadcrumb navigation events. 1292 * Adds click event listeners to breadcrumb links and tracks the event with the appropriate label. 1293 */ 1294function trackBreadcrumb () { 1295 // Select all breadcrumb links 1296 const breadcrumbLinks = $('.menu-breadcrumb.breadcrumbs a'); 1297 1298 // Iterate over each link 1299 breadcrumbLinks.on('click', function () { 1300 // Capture texts of previous breadcrumb elements in an array 1301 const labels = []; 1302 let currentElement = $(this); 1303 // As long as the current element is a breadcrumb element 1304 while (currentElement && currentElement.hasClass('breadcrumb__link')) { 1305 // Add the text of the link (or the <span> inside the link) 1306 labels.unshift(currentElement.find('span').text().toLowerCase()); 1307 // Move to the previous element 1308 currentElement = currentElement.parent().prev().find('a'); 1309 } 1310 1311 // Convert the array into a string, replacing spaces with hyphens 1312 const eventLabel = labels.join('_').replace(/\s+/g, '-'); 1313 const label = replaceUmlauts(eventLabel); 1314 trackCustomEvent('navigation', 'breadcrumb', label); 1315 // Here, you can send the value of event_label to services like Google Analytics or any other tracking service 1316 }); 1317} 1318 1319/** 1320 * Document ready function to initialize breadcrumb and navigation tracking. 1321 */ 1322$(document).ready(function () { 1323 /** track breadcrumb */ 1324 trackBreadcrumb(); 1325 /** track main and service navivgation */ 1326 trackNavigation('.meta-navigation a', 'navigation', 'topbar', ''); 1327 trackNavigation('.nav2024__main-navigation a.navigation-level-1__link', 1328 'navigation', 'hauptnavigation', 'menu_'); 1329 trackNavigation('.nav2024__main-navigation a.mainNav__link--teaser', 1330 'navigation', 'hauptnavigation', 'hero_'); 1331 trackNavigation('.nav2024__main-navigation a.navigation-level-2__link', 1332 'navigation', 'hauptnavigation', 'dropdown_'); 1333 trackNavigation('.nav2024__main-navigation a.navigation-level-3__link', 1334 'navigation', 'hauptnavigation', 'flyout_'); 1335 /** track footer **/ 1336 // German footer (.de) 1337 trackNavigation('.footerMain .shortcuts a', 'footer', 'footer', ''); 1338 trackNavigation('.footerMain .meta-shortcuts a', 'footer', 'subfooter', ''); 1339 // English footer (.org) 1340 trackNavigation('.shortcut-block a', 'footer', 'footer', ''); 1341 trackNavigation('.meta-menu a', 'footer', 'subfooter', ''); 1342 // Social media links in both footers are tracked via trackSocialMediaLinks() 1343 1344 // Partner logos in English subfooter (.org) 1345 $('a.partner-logo.entwicklung-buendnis').on('click', function() { 1346 trackCustomEvent('footer', 'subfooter', 'entwicklung-hilft'); 1347 }); 1348 $('a.partner-logo.dzi').on('click', function() { 1349 trackCustomEvent('footer', 'subfooter', 'dzi'); 1350 }); 1351 $('a.partner-logo.allianz').on('click', function() { 1352 trackCustomEvent('footer', 'subfooter', 'alliance-2015'); 1353 }); 1354}); 1355 1356// 1357function replaceUmlauts (text) { 1358 return text.replace(/ä/g, 'ae'). 1359 replace(/ö/g, 'oe'). 1360 replace(/ü/g, 'ue'). 1361 replace(/Ã/g, 'Ae'). 1362 replace(/Ã/g, 'Oe'). 1363 replace(/Ã/g, 'Ue'). 1364 replace(/Ã/g, 'ss'); 1365} 1366 1367function slugifyText (text) { 1368 text = replaceUmlauts(text); 1369 return text.replace(/\s+/g, '-'); 1370} 1371 1372/** 1373 * Public tracking API for scripts outside of this file. 1374 * Needed wherever the generic [data-campaign] click handler cannot do the job, e.g. 1375 * the project map, which navigates away on click and therefore has to push its 1376 * campaign click itself before the page unloads. 1377 */ 1378window.whhTracking = window.whhTracking || {}; 1379 1380/** 1381 * Push an ecPromoClick for the campaign element the given element belongs to. 1382 * Uses the same promo_product as the view event, so view and click pair up in the report. 1383 * @param {HTMLElement|jQuery} element - Element inside (or equal to) a [data-campaign] element 1384 * @returns {boolean} True if an event was pushed 1385 */ 1386window.whhTracking.trackCampaignClick = function (element) { 1387 const campaignElement = $(element).closest('[data-campaign]'); 1388 if (campaignElement.length === 0) { 1389 return false;
1390 } 1391 1392 const trackingData = getPromoTrackingData(campaignElement.get(0)); 1393 if (!trackingData) { 1394 return false; 1395 } 1396 1397 pushPromotionClick(trackingData.id, trackingData.campaign, trackingData.trackingTitle, trackingData.position); 1398 return true; 1399}; 1400 1401/** 1402 * Re-scan the DOM for campaign elements. 1403 * Required after content has been injected asynchronously (e.g. the news and events 1404 * teasers on the homepage, which are loaded via XHR after the initial scan). 1405 */ 1406window.whhTracking.refreshCampaignElements = function () { 1407 trackClickOfElements(); 1408 debouncedTrackVisibility(); 1409}; 1410 1411window.whhTracking.trackCustomEvent = trackCustomEvent; 1412window.whhTracking.slugifyText = slugifyText;
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.