PageSourceSearch

https://www.designeroutletluxembourg.com/_assets/6a101b282a6a03d13…/JavaScript/modules/header.js?1791189129

js designeroutletluxembourg.com collected 2026-10-09 02:57:35 UTC 29,378 bytes, 786 lines download raw bytes

1(function ($) {
2
3  SR_ROS_HEADER = {
4
5    stickyHeaderScrollOffset: 100,
6
7    initOnDOMReady: function (rawRoot) {
8
9      var root = SR_ROS.getRootItem(rawRoot);
10
11      this.handleHeaderMenus(root);
12      this.toggleMenuIcon(root);
13      this.showSecondMenuLevel(root);
14      this.handleHeaderLogin();
15      // Initialize generic flyout helpers before any feature that uses them
16      this.initGenericFlyoutHelpers();
17      this.handleGenericFlyouts();
18      this.handleHeaderLanguage();
19      this.handleWeatherWidget();
20
21      // function only needed for static dummy feature
22      // it should be commented out when pushing to the T-Systems svn
23      //this.handleSearchSuggestions(root);
24
25    },
26
27        handleHeaderMenus: function (root) {
28
29      var offcanvasLeft = root.find('#js_offcanvas-left'),
30        mainNav = root.find('#js_meta-nav'),
31        mainNavBox = root.find('#js_meta-nav-container'),
32        mainNavBoxOC = root.find('#js_meta-nav-container-oc'),
33        metaNav = root.find('#js_meta-nav'),
34        metaNavBox = root.find('#js_meta-nav-container');
35
36    },
37
38
39    toggleMenuIcon: function (root) {
40
41      const offcanvasEl = document.getElementById('offcanvas-mainnav');
42      const icon = document.getElementById('menuIcon');
43      const menuButton = document.getElementById('menu-button');
44
45      // Bootstrap 5 Offcanvas Instanz
46      const bsOffcanvas = bootstrap.Offcanvas.getOrCreateInstance(offcanvasEl);
47
48      // Beim Öffnen aktivieren
49      offcanvasEl.addEventListener('show.bs.offcanvas', () => {
50        icon.classList.add('active');
51        // Bootstrap pflegt aria-expanded fuer Offcanvas nicht automatisch am Trigger
52        if (menuButton) menuButton.setAttribute('aria-expanded', 'true');
53      });
54
55      // Beim Schließen (egal ob über Backdrop, ESC oder Button) deaktivieren
56      offcanvasEl.addEventListener('hide.bs.offcanvas', () => {
57        icon.classList.remove('active');
58        if (menuButton) menuButton.setAttribute('aria-expanded', 'false');
59      });
60
61    },
62
63    showSecondMenuLevel: function (root) {
64
65      const flyoutLayer = document.getElementById('flyout-layer');
66      const offcanvasEl = document.getElementById('offcanvas-mainnav');
67
68      if (!offcanvasEl || !flyoutLayer) return;
69
70      // Prevent double-initialization across repeated initOnDOMReady/content updates
71      if (offcanvasEl.dataset.flyoutWired === '1') {
72        return;
73      }
74      offcanvasEl.dataset.flyoutWired = '1';
75
76      const isDesktop = () => window.matchMedia('screen and ' + SR_ROS_BREAKPOINTS.config.mlxl).matches;
77
78      let lastTrigger = null;
79      let lastClone = null;
80      let isSwitching = false;
81      let pendingBtn = null;
82      let isOpening = false;
83      let openingCloneRef = null;
84      const SWITCH_GAP = 110; // small delay between slide-in completion and slide-out start
85      let lastOpenedAt = 0; // timestamp (performance.now) when a flyout finished sliding in
86      let bypassSwitchCheck = false; // internal flag to avoid recursion during open-then-close switching
87      let suppressClearOnOpen = false; // prevent clearing the layer when opening during a switch
88      let switchingMode = null; // holds { oldClone, oldTrigger } during open-then-close switching
89
90      // Ensure a small pause occurs only after a slide-in completes and just before any slide-out begins.
91      // If the flyout finished opening less than SWITCH_GAP ago, wait the remaining time; otherwise, run immediately.
92      function afterOpenGap(fn) {
93        const run = () => {
94          const now = (window.performance && performance.now) ? performance.now() : Date.now();
95          const elapsed = now - lastOpenedAt;
96          const wait = Math.max(0, SWITCH_GAP - elapsed);
97          if (wait > 0) {
98            setTimeout(fn, wait);
99          } else {
100            fn();
101          }
102        };
103        if (isOpening && openingCloneRef) {
104          const onOpened = (evt) => {
105            if (evt.target === openingCloneRef && evt.propertyName === 'transform') {
106              openingCloneRef.removeEventListener('transitionend', onOpened);
107              // lastOpenedAt will be set by the onOpenedOnce handler attached during open
108              // defer run to the next tick to ensure lastOpenedAt has been updated
109              setTimeout(run, 0);
110            }
111          };
112          openingCloneRef.addEventListener('transitionend', onOpened);
113        } else {
114          run();
115        }
116      }
117
118      // Bootstrap Offcanvas instance (for focus trap control)
119      const bsOffcanvas = (typeof bootstrap !== 'undefined' && bootstrap.Offcanvas)
120        ? bootstrap.Offcanvas.getOrCreateInstance(offcanvasEl)
121        : null;
122
123      // Ensure layer becomes visible only when offcanvas is shown
124      offcanvasEl.addEventListener('show.bs.offcanvas', () => {
125        flyoutLayer.classList.add('visible');
126      });
127
128      // Ensure we clear flyout when offcanvas closes
129      const fullReset = () => {
130        // Reactivate focus trap when closing entire offcanvas
131        if (bsOffcanvas && bsOffcanvas._focustrap && typeof bsOffcanvas._focustrap.activate === 'function') {
132          try {
133            bsOffcanvas._focustrap.activate();
134          } catch (e) {
135          }
136        }
137        // Clear UI state
138        flyoutLayer.innerHTML = '';
139        flyoutLayer.classList.remove('active');
140        flyoutLayer.classList.remove('visible');
141        offcanvasEl.classList.remove('flyout-open');
142        offcanvasEl.classList.remove('submenu-open');
143        if (lastTrigger) {
144          try {
145            lastTrigger.setAttribute('aria-expanded', 'false');
146          } catch (e) {
147          }
148        }
149        // Reset internal flags to avoid getting stuck after several interactions
150        lastTrigger = null;
151        lastClone = null;
152        isSwitching = false;
153        pendingBtn = null;
154        isOpening = false;
155        openingCloneRef = null;
156        switchingMode = null;
157        suppressClearOnOpen = false;
158        bypassSwitchCheck = false;
159        lastOpenedAt = 0;
160      };
161
162      offcanvasEl.addEventListener('hide.bs.offcanvas', () => {
163        fullReset();
164        // Reset mobile accordion states
165        offcanvasEl.querySelectorAll('.has-subnav.is-open').forEach(li => {
166          li.classList.remove('is-open');
167          const link = li.querySelector(':scope > a');
168          if (link) link.setAttribute('aria-expanded', 'false');
169        });
170      });
171
172      // Close current flyout with slide-out animation, then run callback
173      function closeCurrentFlyout(callback, preferredFocusEl, options) {
174        const opts = options || {};
175        if (!lastClone) {
176          if (typeof callback === 'function') callback();
177          return;
178        }
179        const currentClone = lastClone;
180        currentClone.classList.remove('open');
181        if (!isDesktop() && !opts.keepMobileShift) {
182          offcanvasEl.classList.remove('submenu-open');
183        }
184        const cleanup = () => {
185          flyoutLayer.innerHTML = '';
186          if (!opts.preserveWrapper) {
187            flyoutLayer.classList.remove('active');
188            offcanvasEl.classList.remove('flyout-open');
189            if (bsOffcanvas && bsOffcanvas._focustrap && typeof bsOffcanvas._focustrap.activate === 'function') {
190              try {
191                bsOffcanvas._focustrap.activate();
192              } catch (e) {
193              }
194            }
195          }
196          if (lastTrigger) {
197            lastTrigger.setAttribute('aria-expanded', 'false');
198          }
199          if (preferredFocusEl && typeof preferredFocusEl.focus === 'function') {
200            preferredFocusEl.focus();
201          }
202          currentClone.removeEventListener('transitionend', onEnd);
203          lastClone = null;
204          if (typeof callback === 'function') callback();
205        };
206        const onEnd = (evt) => {
207          if (evt.target === currentClone && evt.propertyName === 'transform') {
208            cleanup();
209          }
210        };
211        // Fallback in case transitionend doesn't fire
212        setTimeout(cleanup, 500);
213        currentClone.addEventListener('transitionend', onEnd);
214      }
215
216      // Close a specific clone without tearing down the wrapper (unless specified)
217      function closeSpecificClone(targetClone, options) {
218        if (!targetClone) return Promise.resolve();
219        const opts = options || {};
220        return new Promise((resolve) => {
221          const onEnd = (evt) => {
222            if (evt.target === targetClone && evt.propertyName === 'transform') {
223              targetClone.removeEventListener('transitionend', onEnd);
224              if (!opts.preserveWrapper === false) {
225                // no-op: wrapper preserved by default in switches
226              }
227              if (targetClone.parentNode === flyoutLayer) {
228                flyoutLayer.removeChild(targetClone);
229              }
230              resolve();
231            }
232          };
233          // start slide-out
234          targetClone.classList.remove('open');
235          // Fallback
236          setTimeout(() => {
237            targetClone.removeEventListener('transitionend', onEnd);
238            if (targetClone.parentNode === flyoutLayer) {
239              flyoutLayer.removeChild(targetClone);
240            }
241            resolve();
242          }, 500);
243          targetClone.addEventListener('transitionend', onEnd);
244        });
245      }
246
247      function openFlyout(btn) {
248        const parent = btn.closest('.has-subnav');
249        if (!parent) return;
250
251        // Mobile accordion behavior
252        if (!isDesktop()) {
253          const isOpen = parent.classList.contains('is-open');
254          // Immer nur eine subnav geoeffnet: zuvor geoeffnetes Untermenue schliessen.
255          offcanvasEl.querySelectorAll('.has-subnav.is-open').forEach(li => {
256            if (li === parent) return;
257            li.classList.remove('is-open');
258            const link = li.querySelector(':scope > a');
259            if (link) link.setAttribute('aria-expanded', 'false');
260          });
261          // Angeklicktes Untermenue toggeln.
262          parent.classList.toggle('is-open');
263          btn.setAttribute('aria-expanded', String(!isOpen));
264          return;
265        }
266
267        const flyout = parent.querySelector('.flyout');
268        if (!flyout) return;
269
270        // If switching to another submenu while one is open, open the new first,
271        // then (after its slide-in + small gap) slide out the previous one.
272        if (!bypassSwitchCheck && lastClone && lastTrigger && btn !== lastTrigger) {
273          if (isSwitching) {
274            pendingBtn = btn;
275            return;
276          }
277          isSwitching = true;
278          switchingMode = {oldClone: lastClone, oldTrigger: lastTrigger};
279          suppressClearOnOpen = true; // do not wipe the layer; we need both clones simultaneously
280          // fall through to open the new flyout; cleanup happens after its slide-in
281        }
282
283        // Toggle off if clicking the same trigger again
284        if (lastClone && lastTrigger && btn === lastTrigger) {
285          const doClose = () => afterOpenGap(() => closeCurrentFlyout());
286          if (isOpening && openingCloneRef) {
287            const onOpened = (evt) => {
288              if (evt.target === openingCloneRef && evt.propertyName === 'transform') {
289                openingCloneRef.removeEventListener('transitionend', onOpened);
290                isOpening = false;
291                doClose();
292              }
293            };
294            openingCloneRef.addEventListener('transitionend', onOpened);
295          } else {
296            doClose();
297          }
298          return;
299        }
300
301        // Remove any previous flyout from the layer (should be empty here unless switching)
302        if (!suppressClearOnOpen) {
303          flyoutLayer.innerHTML = '';
304        }
305
306        // Clone into the global layer so it can slide over the viewport
307        const clone = flyout.cloneNode(true);
308        clone.classList.remove('open');
309        flyoutLayer.appendChild(clone);
310
311        // Remember references for closing/focus restore
312        lastTrigger = btn;
313        lastClone = clone;
314        btn.setAttribute('aria-expanded', 'true');
315
316        // Deactivate offcanvas focus trap so we can tab inside the external flyout layer
317        if (bsOffcanvas && bsOffcanvas._focustrap && typeof bsOffcanvas._focustrap.deactivate === 'function') {
318          try {
319            bsOffcanvas._focustrap.deactivate();
320          } catch (e) {
321          }
322        }
323
324        // Activate layer interactions
325        flyoutLayer.classList.add('active');
326        // Adjust offcanvas corner rounding when flyout visible
327        offcanvasEl.classList.add('flyout-open');
328
329        // Mobile behavior: push offcanvas out to the left in sync with flyout slide-in
330        const needsMobileShift = !isDesktop();
331        if (!needsMobileShift) {
332          // Desktop: place clone to the right of offcanvas
333          const ocWidth = offcanvasEl.getBoundingClientRect().width;
334          clone.style.left = ocWidth + 'px';
335        }
336
337        // Prevent clicks inside the flyout from bubbling to document/backdrop
338        clone.addEventListener('click', (e) => {
339          e.stopPropagation();
340        });
341
342        const closeFlyout = (preferredFocusEl) => {
343          if (!lastClone) return;
344          const currentClone = lastClone;
345          currentClone.classList.remove('open');
346          if (!isDesktop()) {
347            offcanvasEl.classList.remove('submenu-open');
348          }
349          const cleanup = () => {
350            flyoutLayer.innerHTML = '';
351            flyoutLayer.classList.remove('active');
352            offcanvasEl.classList.remove('flyout-open');
353            // Reactivate focus trap when flyout is closed (offcanvas still open)
354            if (bsOffcanvas && bsOffcanvas._focustrap && typeof bsOffcanvas._focustrap.activate === 'function') {
355              try {
356                bsOffcanvas._focustrap.activate();
357              } catch (e) {
358              }
359            }
360            if (lastTrigger) {
361              lastTrigger.setAttribute('aria-expanded', 'false');
362            }
363            // Focus management: use preferred target if provided, else restore to last trigger
364            if (preferredFocusEl && typeof preferredFocusEl.focus === 'function') {
365              preferredFocusEl.focus();
366            } else if (lastTrigger) {
367              lastTrigger.focus();
368            }
369            currentClone.removeEventListener('transitionend', onEnd);
370            lastClone = null;
371          };
372          const onEnd = (evt) => {
373            if (evt.target === currentClone && evt.propertyName === 'transform') {
374              cleanup();
375            }
376          };
377          setTimeout(cleanup, 500);
378          currentClone.addEventListener('transitionend', onEnd);
379        };
380
381        // Wire up Back button to reverse animation
382        const backBtn = clone.querySelector('.btn-back');
383        if (backBtn) {
384          backBtn.addEventListener('click', (e) => {
385            e.preventDefault();
386            e.stopPropagation();
387            closeFlyout();
388          });
389        }
390
391        // Focus management: after opening, move focus to the first link in .sub.level-2
392        // If not present, fall back to Back button, then to the first generic focusable element
393        const level2Links = clone.querySelectorAll('.sub.level-2 a, .level-2 a');
394        const firstLevel2Link = level2Links[0];
395        if (firstLevel2Link) {
396          firstLevel2Link.focus();
397        } else if (backBtn) {
398          backBtn.focus();
399        } else {
400          const firstFocusable = clone.querySelector('a, button, [tabindex]:not([tabindex="-1"])');
401          if (firstFocusable) firstFocusable.focus();
402        }
403
404        // When the user Tabs on the last link in level-2, jump focus to the next top-level (level-1) item
405        if (level2Links.length) {
406          const lastLevel2Link = level2Links[level2Links.length - 1];
407          lastLevel2Link.addEventListener('keydown', (e) => {
408            if (e.key === 'Tab' && !e.shiftKey) {
409              // Move focus to next .level-1 > li main link
410              e.preventDefault();
411              const topLevelUl = document.getElementById('offcanvas-mainnav-nav');
412              const currentTopLi = lastTrigger ? lastTrigger.closest('li') : null;
413              let nextTopLi = null;
414              if (currentTopLi && topLevelUl && topLevelUl.contains(currentTopLi)) {
415                let sib = currentTopLi.nextElementSibling;
416                while (sib) {
417                  if (sib.nodeType === 1) { // element
418                    nextTopLi = sib;
419                    break;
420                  }
421                  sib = sib.nextElementSibling;
422                }
423              }
424
425              // Determine the element to focus next
426              let targetFocus = null;
427              if (nextTopLi) {
428                targetFocus = nextTopLi.querySelector('a');
429                if (!targetFocus) {
430                  targetFocus = nextTopLi.querySelector('button, [tabindex]:not([tabindex="-1"])');
431                }
432              }
433
434              // If we found a next target, close the flyout and focus it; else fall back to offcanvas close button
435              let fallback = offcanvasEl.querySelector('.offcanvas-header .btn-close');
436              closeFlyout(targetFocus || fallback || lastTrigger);
437            }
438          });
439        }
440
441        // Mark as opening and listen for end of slide-in before allowing slide-out
442        isOpening = true;
443        openingCloneRef = clone;
444        const onOpenedOnce = (evt) => {
445          if (evt.target === clone && evt.propertyName === 'transform') {
446            clone.removeEventListener('transitionend', onOpenedOnce);
447            isOpening = false;
448            openingCloneRef = null;
449            lastOpenedAt = (window.performance && performance.now) ? performance.now() : Date.now();
450
451            // If we are in switching mode, wait a short gap, then close the previous clone
452            if (switchingMode && switchingMode.oldClone && switchingMode.oldClone !== clone) {
453              const {oldClone, oldTrigger} = switchingMode;
454              setTimeout(() => {
455                closeSpecificClone(oldClone, {preserveWrapper: true}).then(() => {
456                  if (oldTrigger) {
457                    try {
458                      oldTrigger.setAttribute('aria-expanded', 'false');
459                    } catch (e) {
460                    }
461                  }
462                  switchingMode = null;
463                  isSwitching = false;
464                  suppressClearOnOpen = false;
465                  // If a new click arrived during switching, process it now
466                  if (pendingBtn) {
467                    const next = pendingBtn;
468                    pendingBtn = null;
469                    // Avoid re-entering the switching branch for the already opened one
470                    bypassSwitchCheck = true;
471                    openFlyout(next);
472                    bypassSwitchCheck = false;
473                  }
474                });
475              }, SWITCH_GAP);
476            } else {
477              // not in switching mode anymore
478              suppressClearOnOpen = false;
479              isSwitching = false;
480            }
481          }
482        };
483        clone.addEventListener('transitionend', onOpenedOnce);
484
485        // Mirror the back-button animation in reverse for opening:
486        // Add submenu-open and .open in the same frame (no pre-open delay)
487        if (needsMobileShift) {
488          offcanvasEl.classList.add('submenu-open');
489        }
490        // Force reflow to ensure the starting transform state is applied
491        // before transitioning to .open, matching the back-button smoothness
492        void clone.offsetWidth;
493        clone.classList.add('open');
494
495        // ESC to close flyout while focus is inside it
496        clone.addEventListener('keydown', (e) => {
497          if (e.key === 'Escape' || e.key === 'Esc') {
498            e.preventDefault();
499            closeFlyout();
500          }
501        });
502      }
503
504      // Bind interactions to submenu triggers using a single delegated listener
505      offcanvasEl.addEventListener('click', (ev) => {
506        const target = ev.target && ev.target.closest && ev.target.closest('.level-1 .has-subnav > a');
507        if (target && offcanvasEl.contains(target)) {
508          ev.preventDefault();
509          openFlyout(target);
510        }
511      });
512
513      // Recalculate when breakpoint changes
514      enquire.register('screen and ' + SR_ROS_BREAKPOINTS.config.mlxl, {
515        match: function () {
516          // on desktop ensure offcanvas is reset
517          offcanvasEl.classList.remove('submenu-open');
518
519          // Reset mobile accordion states
520          offcanvasEl.querySelectorAll('.has-subnav.is-open').forEach(li => {
521            li.classList.remove('is-open');
522            const link = li.querySelector(':scope > a');
523            if (link) link.setAttribute('aria-expanded', 'false');
524          });
525
526          // Restore focus trap on breakpoint change
527          if (bsOffcanvas && bsOffcanvas._focustrap && typeof bsOffcanvas._focustrap.activate === 'function') {
528            try {
529              bsOffcanvas._focustrap.activate();
530            } catch (e) {
531            }
532          }
533        },
534        unmatch: function () {
535          // on mobile also reset on change
536          offcanvasEl.classList.remove('submenu-open');
537
538          // Reset mobile accordion states
539          offcanvasEl.querySelectorAll('.has-subnav.is-open').forEach(li => {
540            li.classList.remove('is-open');
541            const link = li.querySelector(':scope > a');
542            if (link) link.setAttribute('aria-expanded', 'false');
543          });
544
545          // Ensure trap is active in fresh state
546          if (bsOffcanvas && bsOffcanvas._focustrap && typeof bsOffcanvas._focustrap.activate === 'function') {
547            try {
548              bsOffcanvas._focustrap.activate();
549            } catch (e) {
550            }
551          }
552        }
553      }, true);
554
555    },
556
557
558    handleHeaderLogin: function () {
559
560      var $loginLink = $('.js_login-link'),
561        $input = $loginLink.find('#dwfrm_login_emailcardno'),
562        hoverAutocomplete = false;
563
564      $loginLink.on(
565        {
566          mouseenter: function () {
567
568            $loginLink
569              .addClass('is-active');
570          },
571
572          mouseleave: function () {
573
574            if (!hoverAutocomplete) {
575              $loginLink.removeClass('is-active');
576            }
577          }
578        }
579      );
580
581      $loginLink.on(
582        'focusin',
583        '#dwfrm_login_emailcardno',
584        function () {
585          hoverAutocomplete = true;
586        }
587      );
588
589      $loginLink.on(
590        'focusout',
591        '#dwfrm_login_emailcardno',
592        function () {
593          hoverAutocomplete = false;
594        }
595      );
596    },
597
598    initGenericFlyoutHelpers: function () {
599      var self = this;
600
601      // Feature detect pointer/hover capability once
602      self._isHoverable = window.matchMedia && window.matchMedia('(hover: hover) and (pointer: fine)').matches;
603
604      // Generic flyout initializer
605      // options: { trigger, dropdown, closeBtn?, linkToClose?, openClass?, namespace?, preventDefaultOnTriggerClick? }
606      self.initFlyout = function (options) {
607        var $trigger = options.trigger,
608          $dropdown = options.dropdown,
609          $closeBtn = options.closeBtn || $(),
610          $linkToClose = options.linkToClose || $(),
611          ns = options.namespace || 'flyout',
612          openClass = options.openClass || 'is-active',
613          preventDefaultOnTriggerClick = options.preventDefaultOnTriggerClick === true;
614
615        if (!$trigger || !$trigger.length || !$dropdown || !$dropdown.length) {
616          return; // nothing to wire
617        }
618
619        function open() {
620          $trigger.addClass(openClass);
621          $dropdown.stop(true, true).fadeIn(150);
622        }
623
624        function close() {
625          $trigger.removeClass(openClass);
626          $dropdown.stop(true, true).fadeOut(150);
627        }
628
629        // Clean previous handlers for this namespace
630        $trigger.off('.' + ns);
631        $dropdown.off('.' + ns);
632        $closeBtn.off('.' + ns);
633        $linkToClose.off('.' + ns);
634
635        if (self._isHoverable) {
636          // Open on hover or focus
637          $trigger.on('mouseenter.' + ns + ' focusin.' + ns, open);
638          $dropdown.on('mouseenter.' + ns + ' focusin.' + ns, open);
639
640          // Close when mouse leaves both trigger and dropdown
641          var closeWithDelay = function () {
642            setTimeout(function () {
643              var hoveringTrigger = $trigger.is(':hover') || $trigger.is(':focus') || $trigger.find(':focus').length > 0;
644              var hoveringDropdown = $dropdown.is(':hover') || $dropdown.is(':focus') || $dropdown.find(':focus').length > 0;
645              if (!hoveringTrigger && !hoveringDropdown) {
646                close();
647              }
648            }, 100);
649          };
650
651          $trigger.on('mouseleave.' + ns + ' focusout.' + ns, closeWithDelay);
652          $dropdown.on('mouseleave.' + ns + ' focusout.' + ns, closeWithDelay);
653        } else {
654          // Toggle on tap/click on the trigger, ignore clicks inside dropdown
655          $trigger.on('click.' + ns, function (e) {
656            // Ignore clicks that originate inside dropdown
657            if ($(e.target).closest($dropdown).length) {
658              return;
659            }
660            if (preventDefaultOnTriggerClick) {
661              e.preventDefault();
662            }
663            e.stopPropagation();
664            if ($trigger.hasClass(openClass)) {
665              close();
666            } else {
667              open();
668            }
669          });
670
671          // Close when tapping outside
672          $(document).on('click.' + ns, function (e) {
673            if ($(e.target).closest($trigger).length || $(e.target).closest($dropdown).length) {
674              return;
675            }
676            close();
677          });
678        }
679
680        // Allow explicit close via link/button inside dropdown on all devices
681        $linkToClose.on('click.' + ns, function (e) {
682          e && e.stopPropagation && e.stopPropagation();
683          close();
684        });
685        $closeBtn.on('click.' + ns, function (e) {
686          if (e) {
687            e.preventDefault && e.preventDefault();
688            e.stopPropagation && e.stopPropagation();
689          }
690          close();
691        });
692
693        // Expose simple API if needed
694        return {open: open, close: close};
695      };
696    },
697
698    handleGenericFlyouts: function () {
699      var self = this;
700      $('.js_flyout-toggle').each(function (idx, el) {
701        var $trigger = $(el);
702        var $dropdown = $trigger.find('.js_flyout-dropdown');
703        if ($dropdown.length) {
704          var $closeBtn = $dropdown.find('.js_flyout-close');
705          var $linkToClose = $dropdown.find('.js_flyout-link-close');
706          self.initFlyout({
707            trigger: $trigger,
708            dropdown: $dropdown,
709            closeBtn: $closeBtn,
710            linkToClose: $linkToClose,
711            namespace: 'genericFlyout' + idx,
712            openClass: 'is-active',
713            preventDefaultOnTriggerClick: true
714          });
715        }
716      });
717    },
718
719
720    handleHeaderLanguage: function () {
721      document.addEventListener('DOMContentLoaded', () => {
722        const trigger = document.querySelector('.language-navigation > button');
723        const flyout = document.querySelector('.language-navigation-flyout');
724
725        if (!trigger || !flyout) return;
726
727        const toggleMenu = (e) => {
728          e.preventDefault();
729          const isOpen = flyout.classList.toggle('is-open');
730          trigger.setAttribute('aria-expanded', isOpen);
731
732          if (isOpen) {
733            checkBoundary();
734          }
735        };
736
737        const checkBoundary = () => {
738          const rect = flyout.getBoundingClientRect();
739
740          // Wenn der obere Rand des Flyouts kleiner als 0 ist (aus dem Viewport ragt)
741          if (rect.top < 0) {
742            flyout.classList.add('drop-down');
743          } else {
744            // Optional: Wieder zurücksetzen, wenn wieder Platz wäre
745            // flyout.classList.remove('drop-down');
746          }
747        };
748
749        trigger.addEventListener('click', toggleMenu);
750
751        // Close on click outside
752        document.addEventListener('click', (e) => {
753          if (!trigger.contains(e.target) && !flyout.contains(e.target)) {
754            flyout.classList.remove('is-open');
755            trigger.setAttribute('aria-expanded', 'false');
756            flyout.classList.remove('drop-down'); // Reset für den nächsten Klick
757          }
758        });
759      });
760    },
761
762    handleWeatherWidget: function () {
763      var $weatherWidget = $('.js_weather-widget'),
764        $weatherWeek = $weatherWidget.find('.js_weather-week');
765
766      $weatherWidget.on('mouseenter focusin', function () {
767        $weatherWeek.stop(true, true).fadeIn(150);
768      });
769
770      $weatherWidget.on('mouseleave focusout', function () {
771        setTimeout(function() {
772          if (!$weatherWidget.is(':hover') && !$weatherWidget.is(':focus') && $weatherWidget.find(':focus').length === 0) {
773            $weatherWeek.stop(true, true).fadeOut(150);
774          }
775        }, 100);
776      });
777    }
778  };
779
780  SR_ROS_HEADER.initOnDOMReady();
781
782  $(document).on('mms:contentupdate', function (event) {
783    SR_ROS_HEADER.initOnDOMReady(event.target);
784  });
785
786})(jQuery);

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.