PageSourceSearch

https://www.usff.se/js/mobile-menu.js

js usff.se collected 2026-10-04 09:44:03 UTC 6,494 bytes, 160 lines download raw bytes

1// Mobile navigation for the homepage: the header's top-level dropdown
2// (MobileHeader) and the slide-in page drawer (MobileMenu).
3//
4// Pure-JS (the homepage renders as static SSR, so no Blazor event handlers).
5// Behaviour is driven by data-attributes via a single delegated click handler
6// so it survives enhanced-navigation DOM morphing:
7//   [data-topmenu-toggle]       opens/closes the header's top-level dropdown
8//   [data-drawer-open]          opens the page drawer; an optional
9//                               data-drawer-sub="ID" lands straight in
10//                               #sa-drawer-sub-ID instead of the root list
11//   [data-drawer-close]         closes it (backdrop, close button)
12//   [data-sub-open="ID"]        slides in the submenu panel #sa-drawer-sub-ID
13//   [data-sub-close]            slides the current submenu back out
14// It also toggles html.sa-scrolled, which compacts the sticky header.
15(function () {
16    var TOPMENU_OPEN = 'sa-mobile-header__menu--open';
17
18    function closeTopMenu() {
19        var open = document.querySelectorAll('.' + TOPMENU_OPEN);
20        for (var i = 0; i < open.length; i++) {
21            open[i].classList.remove(TOPMENU_OPEN);
22            var btn = open[i].querySelector('[data-topmenu-toggle]');
23            if (btn) btn.setAttribute('aria-expanded', 'false');
24        }
25    }
26
27    function toggleTopMenu(btn) {
28        var wrap = btn.closest('.sa-mobile-header__menu');
29        if (!wrap) return;
30        var wasOpen = wrap.classList.contains(TOPMENU_OPEN);
31        closeTopMenu();
32        if (!wasOpen) {
33            wrap.classList.add(TOPMENU_OPEN);
34            btn.setAttribute('aria-expanded', 'true');
35        }
36    }
37
38    function resetSubs() {
39        var open = document.querySelectorAll('.sa-drawer__panel--active');
40        for (var i = 0; i < open.length; i++) {
41            open[i].classList.remove('sa-drawer__panel--active');
42        }
43    }
44
45    function openSub(id) {
46        var panel = document.getElementById('sa-drawer-sub-' + id);
47        if (panel) {
48            panel.classList.add('sa-drawer__panel--active');
49        }
50    }
51
52    // Pending post-close reset (see closeDrawer) — cancelled on reopen so a
53    // quick close/reopen can't wipe the panel the reopen just asked for.
54    var resetTimer = null;
55
56    // The breadcrumb row's hamburger is the drawer's only trigger.
57    function markDrawerTriggers(expanded) {
58        var triggers = document.querySelectorAll('[data-drawer-open]');
59        for (var i = 0; i < triggers.length; i++) {
60            triggers[i].setAttribute('aria-expanded', expanded ? 'true' : 'false');
61        }
62    }
63
64    function openDrawer(sub) {
65        if (!document.getElementById('sa-drawer')) return;
66        if (resetTimer !== null) { window.clearTimeout(resetTimer); resetTimer = null; }
67        resetSubs();
68        if (sub) openSub(sub);
69        document.body.classList.add('sa-drawer-open');
70        markDrawerTriggers(true);
71    }
72
73    function closeDrawer() {
74        document.body.classList.remove('sa-drawer-open');
75        markDrawerTriggers(false);
76        // Reset after the slide-out finishes so submenus don't flash on reopen.
77        if (resetTimer !== null) window.clearTimeout(resetTimer);
78        resetTimer = window.setTimeout(function () {
79            resetTimer = null;
80            resetSubs();
81        }, 250);
82    }
83
84    function closeSub(el) {
85        var panel = el.closest('.sa-drawer__panel--sub');
86        if (panel) {
87            panel.classList.remove('sa-drawer__panel--active');
88        }
89    }
90
91    document.addEventListener('click', function (e) {
92        // e.target is an Element for real clicks, but guard anyway so a stray
93        // non-Element target can never throw and kill the handler.
94        var t = e.target;
95        if (!t || typeof t.closest !== 'function') return;
96
97        // Any click outside the header dropdown dismisses it, including clicks
98        // that go on to open the drawer below.
99        if (!t.closest('.sa-mobile-header__menu')) closeTopMenu();
100
101        var topToggle = t.closest('[data-topmenu-toggle]');
102        if (topToggle) { e.preventDefault(); toggleTopMenu(topToggle); return; }
103
104        var open = t.closest('[data-drawer-open]');
105        if (open) { e.preventDefault(); openDrawer(open.getAttribute('data-drawer-sub')); return; }
106
107        var close = t.closest('[data-drawer-close]');
108        if (close) { e.preventDefault(); closeDrawer(); return; }
109
110        var subOpen = t.closest('[data-sub-open]');
111        if (subOpen) { e.preventDefault(); openSub(subOpen.getAttribute('data-sub-open')); return; }
112
113        var subClose = t.closest('[data-sub-close]');
114        if (subClose) { e.preventDefault(); closeSub(subClose); return; }
115
116        // A navigation link inside the drawer: let it navigate, but drop the
117        // open state so the drawer can't survive an enhanced-navigation morph.
118        if (t.closest('#sa-drawer a[href]')) {
119            document.body.classList.remove('sa-drawer-open');
120            markDrawerTriggers(false);
121        }
122    });
123
124    document.addEventListener('keydown', function (e) {
125        if (e.key !== 'Escape') return;
126        if (document.body.classList.contains('sa-drawer-open')) closeDrawer();
127        closeTopMenu();
128    });
129
130    // Compact sticky header. The class goes on <html>, not <body>: enhanced
131    // navigation morphs the body and would drop it mid-scroll.
132    var SCROLLED = 'sa-scrolled';
133    // Separate thresholds on purpose. The header is sticky, so it occupies flow
134    // space — compacting it shortens the document, which can nudge the scroll
135    // position back over a single threshold and flap the state.
136    var COMPACT_AT = 48;
137    var EXPAND_AT = 16;
138    var scrollQueued = false;
139
140    function syncScrolled() {
141        scrollQueued = false;
142        var root = document.documentElement;
143        var y = window.pageYOffset || root.scrollTop || 0;
144        if (root.classList.contains(SCROLLED)) {
145            if (y <= EXPAND_AT) root.classList.remove(SCROLLED);
146        } else if (y >= COMPACT_AT) {
147            root.classList.add(SCROLLED);
148        }
149    }
150
151    window.addEventListener('scroll', function () {
152        if (scrollQueued) return;
153        scrollQueued = true;
154        window.requestAnimationFrame(syncScrolled);
155    }, { passive: true });
156
157    // Enhanced navigation can land on a page already scrolled, so settle the
158    // state up front rather than waiting for the first scroll event.
159    syncScrolled();
160}());

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.