1document.addEventListener('DOMContentLoaded', function () { 2 3 // Load lazy styles 4 [].slice.call(document.head.querySelectorAll('link[rel="lazy-stylesheet"]')) 5 .forEach(function ($link) { 6 $link.rel = 'stylesheet'; 7 }); 8 9 // Sidebar submenu: toggle a parent item's Bootstrap collapse and keep aria-expanded in sync 10 document.querySelectorAll('#sidenav .nav-link').forEach(function (element) { 11 const submenu = element.nextElementSibling; 12 13 if (!submenu || !submenu.classList.contains('submenu')) { 14 return; 15 } 16 17 element.addEventListener('click', function (e) { 18 e.preventDefault(); 19 20 const collapse = bootstrap.Collapse.getOrCreateInstance(submenu, { toggle: false }); 21 22 if (submenu.classList.contains('show')) { 23 collapse.hide(); 24 element.setAttribute('aria-expanded', 'false'); 25 return; 26 } 27 28 // Accordion behaviour: close sibling submenus on the same level 29 const parentList = element.closest('ul'); 30 if (parentList) { 31 parentList.querySelectorAll(':scope > li > .submenu.show').forEach(function (openSubmenu) { 32 bootstrap.Collapse.getOrCreateInstance(openSubmenu, { toggle: false }).hide(); 33 const openToggle = openSubmenu.previousElementSibling; 34 if (openToggle) { 35 openToggle.setAttribute('aria-expanded', 'false'); 36 } 37 }); 38 } 39 40 collapse.show(); 41 element.setAttribute('aria-expanded', 'true'); 42 }); 43 }); 44}); 45 46// Desktop sidebar collapse (icon-rail), persisted in localStorage 47(function () { 48 const STORAGE_KEY = 'sidenavCollapsed'; 49 const sidenav = document.getElementById('sidenav'); 50 const toggle = document.getElementById('sidenav-collapse-toggle'); 51 52 if (!sidenav || !toggle) { 53 return; 54 } 55 56 if (localStorage.getItem(STORAGE_KEY) === '1') { 57 sidenav.classList.add('sidenav--collapsed'); 58 } 59 60 toggle.addEventListener('click', function () { 61 const collapsed = sidenav.classList.toggle('sidenav--collapsed'); 62 localStorage.setItem(STORAGE_KEY, collapsed ? '1' : '0'); 63 }); 64})(); 65 66// Settings menu toggle (avatar dropdown) 67(function () { 68 const toggleSettingsmenu = document.querySelector('#header-settingsbutton'); 69 const settingsMenu = document.querySelector('#header-settingsmenu'); 70 const overlay = document.querySelector('#overlay'); 71 72 if (!toggleSettingsmenu || !settingsMenu || !overlay) { 73 return; 74 } 75 76 overlay.addEventListener('click', function (e) { 77 e.preventDefault(); 78 settingsMenu.style.display = 'none'; 79 overlay.style.display = 'none'; 80 }); 81 82 toggleSettingsmenu.addEventListener('click', function (e) { 83 e.preventDefault(); 84 const isHidden = settingsMenu.style.display === 'none' || settingsMenu.style.display === ''; 85 settingsMenu.style.display = isHidden ? 'block' : 'none'; 86 overlay.style.display = isHidden ? 'block' : 'none'; 87 }); 88})(); 89 90// Navigation: loading spinner + Matomo tracking, scoped to the chrome navigation surfaces 91(function () { 92 const spinner = document.getElementById('spinner'); 93 const content = document.getElementById('container-content'); 94 95 /** 96 * Push a navigation event to Matomo. 97 * @param {string} surface - The navigation surface (sidenav, header, bottomnav). 98 * @param {string} href - The clicked link target. 99 */ 100 function trackNav(surface, href) { 101 if (typeof _paq !== 'undefined') { 102 _paq.push(['trackEvent', 'navigation', surface, href]); 103 } 104 } 105 106 /** 107 * Show the loading spinner and dim the content area. 108 */ 109 function showSpinner() { 110 if (spinner) { 111 spinner.style.display = 'block'; 112 } 113 if (content) { 114 content.style.opacity = '0.3'; 115 } 116 } 117 118 const surfaces = [ 119 { selector: '#sidenav a[href]', surface: 'sidenav' }, 120 { selector: '#header a[href]', surface: 'header' }, 121 { selector: '#bottom-navigation a[href]', surface: 'bottomnav' }, 122 ]; 123 124 surfaces.forEach(function (entry) { 125 document.querySelectorAll(entry.selector).forEach(function (link) { 126 link.addEventListener('click', function (e) { 127 const href = this.getAttribute('href'); 128 129 if (!href || href.charAt(0) === '#' || href.indexOf('javascript:') === 0) { 130 return; 131 } 132 133 trackNav(entry.surface, href); 134 135 // Respect modified clicks so the browser can open a new tab or window 136 if (e.defaultPrevented || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0) { 137 return; 138 } 139 140 e.preventDefault(); 141 showSpinner(); 142 window.location.href = href; 143 }); 144 }); 145 }); 146})(); 147 148// If the browser back button restored a cached page, reload it 149(function () { 150 window.onpageshow = function (event) { 151 if (event.persisted) { 152 window.location.reload(); 153 } 154 }; 155})(); 156 157/** 158 * Mobile header scroll behaviour. 159 * 160 * Hides the fixed header on scroll-down and shows it on scroll-up below the 161 * mobile breakpoint. Writes the current effective header height to the 162 * --header-offset CSS custom property so other sticky elements (e.g. the 163 * course anchor bar) can align against it without coupling to the scroll 164 * handler directly. 165 * 166 * Hysteresis mitigates the "reread" false-positive: 167 * - Hide requires >= HIDE_DELTA downward movement past MIN_SCROLL. 168 * - Show requires >= SHOW_DELTA upward movement, or scroll near top. 169 */ 170(function () { 171 const MOBILE_BREAKPOINT = 768; 172 const MIN_SCROLL = 80; 173 const HIDE_DELTA = 10; 174 const SHOW_DELTA = 10; 175 const TOP_THRESHOLD = 40; 176 177 const header = document.getElementById('header'); 178 if (!header) { 179 return; 180 } 181 182 const root = document.documentElement; 183 const headerHeight = header.offsetHeight || 60; 184 185 // Expose the rendered header height so sticky descendants can compute offsets. 186 root.style.setProperty('--header-height', headerHeight + 'px'); 187 root.style.setProperty('--header-offset', headerHeight + 'px'); 188 189 let lastY = window.pageYOffset || 0; 190 let hidden = false;
191 let ticking = false; 192 let anchorY = lastY; 193 194 const reducedMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; 195 196 const setHidden = (nextHidden) => { 197 if (nextHidden === hidden) { 198 return; 199 } 200 hidden = nextHidden; 201 header.classList.toggle('is-hidden', hidden); 202 root.style.setProperty('--header-offset', hidden ? '0px' : headerHeight + 'px'); 203 }; 204 205 const update = () => { 206 ticking = false; 207 const y = window.pageYOffset || 0; 208 209 if (window.innerWidth >= MOBILE_BREAKPOINT) { 210 if (hidden) { 211 setHidden(false); 212 } 213 lastY = y; 214 anchorY = y; 215 return; 216 } 217 218 if (reducedMotion) { 219 // Keep header always visible when user prefers reduced motion. 220 if (hidden) { 221 setHidden(false); 222 } 223 lastY = y; 224 anchorY = y; 225 return; 226 } 227 228 if (y <= TOP_THRESHOLD) { 229 setHidden(false); 230 lastY = y; 231 anchorY = y; 232 return; 233 } 234 235 const diffFromAnchor = y - anchorY; 236 237 if (!hidden && diffFromAnchor > HIDE_DELTA && y > MIN_SCROLL) { 238 setHidden(true); 239 anchorY = y; 240 } else if (hidden && diffFromAnchor < -SHOW_DELTA) { 241 setHidden(false); 242 anchorY = y; 243 } else if ((!hidden && diffFromAnchor < 0) || (hidden && diffFromAnchor > 0)) { 244 // Reset anchor when direction flips, start measuring fresh from here. 245 anchorY = y; 246 } 247 248 lastY = y; 249 }; 250 251 window.addEventListener('scroll', function () { 252 if (!ticking) { 253 window.requestAnimationFrame(update); 254 ticking = true; 255 } 256 }, {passive: true}); 257 258 window.addEventListener('resize', function () { 259 if (window.innerWidth >= MOBILE_BREAKPOINT && hidden) { 260 setHidden(false); 261 } 262 }); 263})(); 264 265// Teased content blocks: cap long description and transcript sections behind a toggle. 266(function () { 267 'use strict'; 268 269 const blocks = document.querySelectorAll('.content-cap'); 270 if (blocks.length === 0) { 271 return; 272 } 273 274 const trackedEvents = new Set(); 275 276 blocks.forEach(function (block) { 277 const toggle = block.nextElementSibling; 278 if (!toggle || !toggle.classList.contains('content-cap-toggle')) { 279 return; 280 } 281 282 const labelSpan = toggle.querySelector('.content-cap-toggle__label'); 283 const labelExpand = toggle.dataset.labelExpand || 'Mehr anzeigen'; 284 const labelCollapse = toggle.dataset.labelCollapse || 'Weniger anzeigen'; 285 286 if (labelSpan) { 287 labelSpan.textContent = labelExpand; 288 } 289 290 // Measure each block against its own cap, then drop the cap when nothing is clipped. 291 if (block.scrollHeight <= block.clientHeight + 4) { 292 block.classList.remove('content-cap'); 293 return; 294 } 295 296 toggle.hidden = false; 297 298 const expand = function () { 299 block.classList.add('is-expanded'); 300 toggle.setAttribute('aria-expanded', 'true'); 301 302 if (labelSpan) { 303 labelSpan.textContent = labelCollapse; 304 } 305 306 const tag = toggle.dataset.cappableLabel || 'block'; 307 if (!trackedEvents.has(tag) && typeof window._paq !== 'undefined') { 308 window._paq.push(['trackEvent', 'Video Content', 'expand', tag]); 309 trackedEvents.add(tag); 310 } 311 }; 312 313 toggle.addEventListener('click', function () { 314 if (!block.classList.contains('is-expanded')) { 315 expand(); 316 return; 317 } 318 319 block.classList.remove('is-expanded'); 320 toggle.setAttribute('aria-expanded', 'false'); 321 322 if (labelSpan) { 323 labelSpan.textContent = labelExpand; 324 } 325 326 const rect = block.getBoundingClientRect(); 327 if (rect.top < 0) { 328 const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; 329 window.scrollTo({ 330 top: window.pageYOffset + rect.top - 80, 331 behavior: reduceMotion ? 'auto' : 'smooth', 332 }); 333 } 334 }); 335 336 // Links inside the clipped area stay focusable, so expand before the browser 337 // scrolls the hidden overflow into view. 338 block.addEventListener('focusin', function () { 339 if (!block.classList.contains('is-expanded')) { 340 expand(); 341 } 342 }); 343 }); 344})();
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.