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.