PageSourceSearch

https://www.fernstudi.net/media/templates/site/ontheme/js/template.bottom.js?v=5

js fernstudi.net collected 2026-10-02 06:36:54 UTC 10,063 bytes, 344 lines download raw bytes

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.