PageSourceSearch

https://www.welthungerhilfe.org/typo3conf/ext/ig_project/Resources/Public/Js/tracking.js?1789554682

js welthungerhilfe.org collected 2026-09-24 17:36:50 UTC 51,146 bytes, 1,412 lines download raw bytes

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.