PageSourceSearch

https://skrivsikkert.dk/assets/js/bb-mobile-nav.js?v=20260822-mobile-more-focus-4

js skrivsikkert.dk collected 2026-09-24 12:57:27 UTC 14,616 bytes, 416 lines download raw bytes

1/* SkrivSikkert Unified Mobile Navigation V2 */
2window.__disableMiscMobileFooter = true;
3
4(function () {
5  'use strict';
6
7  var mql = window.matchMedia('(max-width: 767px)');
8  var initialized = false;
9  var cleanupScrollHide = null;
10  var langSelectorLoadPromise = null;
11  var lastMoreActivationWasKeyboard = false;
12  var LANG_SELECTOR_SCRIPT = '/assets/js/lang-selector.js?v=20260519-mobile-more-lang-login-284';
13
14  function getThemeLabel(isDarkNow) {
15    var isDa = true;
16    try {
17      var ui = localStorage.getItem('sb_ui_lang');
18      if (ui && ui !== 'da') isDa = false;
19    } catch (_) {}
20    if (isDa) return isDarkNow ? 'Lys tilstand' : 'M\u00f8rk tilstand';
21    return isDarkNow ? 'Light mode' : 'Dark mode';
22  }
23
24  function init() {
25    if (initialized) return;
26    initialized = true;
27    document.body.classList.add('has-mn-bar');
28    cleanupScrollHide = initScrollHide();
29  }
30
31  function teardown() {
32    if (!initialized) return;
33    initialized = false;
34    document.body.classList.remove('has-mn-bar');
35    setMoreOpen(false);
36    if (typeof cleanupScrollHide === 'function') cleanupScrollHide();
37    cleanupScrollHide = null;
38    var fab = document.querySelector('.mn-fab-group');
39    if (fab) fab.classList.remove('is-hidden');
40  }
41
42  function handleBreakpoint(e) {
43    if (e.matches) {
44      init();
45    } else {
46      teardown();
47    }
48  }
49
50  if (mql.addEventListener) {
51    mql.addEventListener('change', handleBreakpoint);
52  } else if (mql.addListener) {
53    mql.addListener(handleBreakpoint);
54  }
55
56  if (mql.matches) init();
57
58  function initScrollHide() {
59    var fabGroup = document.querySelector('.mn-fab-group');
60    if (!fabGroup) return null;
61
62    var lastPositions = new Map();
63    var scrollThreshold = 56;
64    var ticking = false;
65    var pendingTarget = null;
66
67    function getScrollPosition(target) {
68      if (!target || target === document || target === document.documentElement || target === document.body) {
69        return window.scrollY || document.documentElement.scrollTop || document.body.scrollTop || 0;
70      }
71      return target.scrollTop || 0;
72    }
73
74    function onScroll(e) {
75      pendingTarget = (e && e.target) || window;
76      if (ticking) return;
77      requestAnimationFrame(function () {
78        var target = pendingTarget || window;
79        var currentY = getScrollPosition(target);
80        var lastY = lastPositions.has(target) ? lastPositions.get(target) : 0;
81
82        if (currentY > lastY + scrollThreshold) {
83          fabGroup.classList.add('is-hidden');
84          lastPositions.set(target, currentY);
85        } else if (currentY < lastY - 10) {
86          fabGroup.classList.remove('is-hidden');
87          lastPositions.set(target, currentY);
88        }
89        pendingTarget = null;
90        ticking = false;
91      });
92      ticking = true;
93    }
94
95    window.addEventListener('scroll', onScroll, { passive: true });
96    document.addEventListener('scroll', onScroll, { passive: true, capture: true });
97
98    return function cleanup() {
99      window.removeEventListener('scroll', onScroll);
100      document.removeEventListener('scroll', onScroll, true);
101    };
102  }
103
104  function getMoreParts() {
105    return {
106      toggle: document.querySelector('[data-mn-more-toggle]'),
107      sheet: document.getElementById('bb-mobile-more-sheet'),
108      backdrop: document.querySelector('[data-mn-more-close]')
109    };
110  }
111
112  function clearPointerMoreFocus(toggle) {
113    if (!toggle) return;
114    // Keep Safari's restored/late pointer focus from painting the keyboard ring.
115    toggle.style.outline = 'none';
116    var clear = function () {
117      if (document.activeElement === toggle) toggle.blur();
118    };
119    clear();
120    // iOS Safari may apply focus after the click handler/default action completes.
121    window.setTimeout(clear, 0);
122    window.setTimeout(clear, 50);
123  }
124
125  function setMoreOpen(open, returnFocus, focusFirst) {
126    var parts = getMoreParts();
127    if (!parts.toggle || !parts.sheet || !parts.backdrop) return;
128
129    parts.toggle.setAttribute('aria-expanded', open ? 'true' : 'false');
130    parts.sheet.hidden = !open;
131    parts.backdrop.hidden = !open;
132    document.body.classList.toggle('has-mn-more-open', open);
133
134    if (!open) {
135      var langControls = parts.sheet.querySelectorAll('[data-mn-lang-control]');
136      for (var i = 0; i < langControls.length; i++) {
137        var langList = langControls[i].querySelector('[data-mn-lang-list]');
138        var langToggle = langControls[i].querySelector('[data-mn-lang-toggle]');
139        if (langList) langList.hidden = true;
140        if (langToggle) langToggle.setAttribute('aria-expanded', 'false');
141      }
142    }
143
144    if (open && focusFirst) {
145      var firstTarget = parts.sheet.querySelector('.mn-more-link.is-active') || parts.sheet.querySelector('.mn-more-link');
146      if (firstTarget) {
147        window.setTimeout(function () {
148          firstTarget.focus({ preventScroll: true });
149        }, 0);
150      }
151    } else if (open && document.activeElement === parts.toggle) {
152      parts.toggle.blur();
153    } else if (!open && !returnFocus && document.activeElement === parts.toggle) {
154      parts.toggle.blur();
155    } else if (!open && returnFocus && document.contains(parts.toggle)) {
156      parts.toggle.focus({ preventScroll: true });
157    }
158  }
159
160  function isMoreOpen() {
161    var parts = getMoreParts();
162    return Boolean(parts.sheet && !parts.sheet.hidden);
163  }
164
165  function withLangSelector(trigger, attempts) {
166    if (window.LangSelector && typeof window.LangSelector.open === 'function') {
167      window.LangSelector.open(trigger);
168      return;
169    }
170
171    var existing = document.querySelector('script[src*="/assets/js/lang-selector.js"]');
172    if (existing && !langSelectorLoadPromise && attempts < 20) {
173      window.setTimeout(function () {
174        withLangSelector(trigger, attempts + 1);
175      }, 50);
176      return;
177    }
178
179    if (!langSelectorLoadPromise) {
180      langSelectorLoadPromise = new Promise(function (resolve, reject) {
181        var script = document.createElement('script');
182        script.src = LANG_SELECTOR_SCRIPT;
183        script.defer = true;
184        script.addEventListener('load', resolve, { once: true });
185        script.addEventListener('error', reject, { once: true });
186        document.head.appendChild(script);
187      });
188    }
189
190    langSelectorLoadPromise.then(function () {
191      if (window.LangSelector && typeof window.LangSelector.open === 'function') {
192        window.LangSelector.open(trigger);
193      }
194    }).catch(function () {});
195  }
196
197  document.addEventListener('click', function (e) {
198    var moreToggle = e.target.closest('[data-mn-more-toggle]');
199    if (moreToggle) {
200      e.preventDefault();
201      var opening = !isMoreOpen();
202      var keyboardActivation = e.detail === 0;
203      lastMoreActivationWasKeyboard = keyboardActivation;
204      if (keyboardActivation) moreToggle.style.removeProperty('outline');
205      setMoreOpen(opening, keyboardActivation, opening && keyboardActivation);
206      if (!keyboardActivation) clearPointerMoreFocus(moreToggle);
207      return;
208    }
209
210    if (e.target.closest('[data-mn-more-close]')) {
211      var backdropKeyboardActivation = e.detail === 0;
212      lastMoreActivationWasKeyboard = backdropKeyboardActivation;
213      setMoreOpen(false, backdropKeyboardActivation);
214      if (!backdropKeyboardActivation) clearPointerMoreFocus(getMoreParts().toggle);
215      return;
216    }
217
218    var moreLink = e.target.closest('.mn-more-link');
219    if (moreLink && !moreLink.hasAttribute('data-mn-action')) {
220      setMoreOpen(false, false);
221      return;
222    }
223
224    var fab = e.target.closest('[data-mn-action]');
225    if (!fab) return;
226
227    var action = fab.getAttribute('data-mn-action');
228
229    switch (action) {
230      case 'new-document': {
231        var newDocBtn = document.getElementById('sk-doc-new-btn');
232        if (newDocBtn) newDocBtn.click();
233        break;
234      }
235
236      case 'new-task': {
237        var taskInput = document.getElementById('task-input');
238        if (taskInput) {
239          taskInput.focus();
240          taskInput.scrollIntoView({ behavior: 'smooth', block: 'center' });
241        }
242        break;
243      }
244
245      case 'compose-email': {
246        var composeBtn = document.getElementById('email-compose-btn');
247        if (composeBtn) composeBtn.click();
248        break;
249      }
250
251      case 'new-event': {
252        var eventModal = document.getElementById('event-modal');
253        if (eventModal) {
254          eventModal.removeAttribute('inert');
255          eventModal.classList.add('active');
256          var eventTitle = document.getElementById('ev-title');
257          if (eventTitle) {
258            window.setTimeout(function () {
259              eventTitle.focus({ preventScroll: true });
260            }, 300);
261          }
262        }
263        break;
264      }
265
266      case 'open-sofia': {
267        if (window.openMobileAiSheet) {
268          window.openMobileAiSheet('ai');
269        } else {
270          var askAiToolbarBtn = document.querySelector('[data-action="ask-ai"]');
271          var aiTab = document.getElementById('option-ai') || document.querySelector('[data-option="ai-panel"]');
272          if (askAiToolbarBtn) {
273            askAiToolbarBtn.click();
274          } else if (aiTab) {
275            aiTab.click();
276          } else {
277            var aiPanel = document.getElementById('ai-panel');
278            if (aiPanel) {
279              aiPanel.style.display = 'flex';
280              aiPanel.scrollIntoView({ behavior: 'smooth', block: 'start' });
281            }
282          }
283        }
284        window.setTimeout(function () {
285          var aiInput = document.getElementById('ai-card-input');
286          if (aiInput) aiInput.focus({ preventScroll: true });
287        }, 250);
288        break;
289      }
290
291      case 'open-ask-ai': {
292        var askAiBtn = document.getElementById('email-ask-ai-btn');
293        if (askAiBtn) askAiBtn.click();
294        break;
295      }
296
297      case 'go-today': {
298        if (window._calendarInstance && typeof window._calendarInstance.today === 'function') {
299          window._calendarInstance.today();
300        } else {
301          var todayBtn = document.querySelector('.fc-today-button');
302          if (todayBtn) todayBtn.click();
303        }
304        break;
305      }
306
307      case 'toggle-theme': {
308        var isDark = document.documentElement.getAttribute('data-theme') === 'dark';
309        var next = isDark ? 'light' : 'dark';
310        document.documentElement.setAttribute('data-theme', next);
311        try {
312          localStorage.setItem('skrivsikkert_theme', next);
313          localStorage.setItem('darkMode', next === 'dark' ? 'true' : 'false');
314          localStorage.setItem('ss_theme', next);
315          localStorage.setItem('theme', next);
316          document.cookie = 'ss_theme=' + next + ';path=/;max-age=31536000;SameSite=Lax';
317          document.cookie = 'ss-dark-mode=' + (next === 'dark' ? '1' : '0') + ';path=/;max-age=31536000;SameSite=Lax';
318        } catch (_) {}
319        var moonEl = document.querySelector('.mn-icon-moon');
320        var sunEl = document.querySelector('.mn-icon-sun');
321        var labelEl = document.querySelector('.mn-theme-label');
322        if (moonEl) moonEl.style.display = next === 'dark' ? 'none' : '';
323        if (sunEl) sunEl.style.display = next === 'dark' ? '' : 'none';
324        if (labelEl) labelEl.textContent = getThemeLabel(next === 'dark');
325        var sidebarToggle = document.getElementById('theme-toggle');
326        if (sidebarToggle) sidebarToggle.checked = next === 'dark';
327        setMoreOpen(false, false);
328        break;
329      }
330
331      case 'open-language': {
332        if (window.LangSelector && typeof window.LangSelector.open === 'function') {
333          window.LangSelector.open(fab);
334        } else {
335          withLangSelector(fab, 0);
336        }
337        break;
338      }
339    }
340  });
341
342  document.addEventListener('pointerdown', function () {
343    lastMoreActivationWasKeyboard = false;
344    var toggle = getMoreParts().toggle;
345    if (toggle) toggle.style.outline = 'none';
346  }, true);
347
348  document.addEventListener('touchstart', function () {
349    lastMoreActivationWasKeyboard = false;
350    var toggle = getMoreParts().toggle;
351    if (toggle) toggle.style.outline = 'none';
352  }, { passive: true, capture: true });
353
354  window.addEventListener('pagehide', function () {
355    if (lastMoreActivationWasKeyboard) return;
356    var toggle = getMoreParts().toggle;
357    if (toggle && document.activeElement === toggle) toggle.blur();
358  });
359
360  window.addEventListener('pageshow', function () {
361    if (!lastMoreActivationWasKeyboard) clearPointerMoreFocus(getMoreParts().toggle);
362  });
363
364  window.addEventListener('popstate', function () {
365    if (!lastMoreActivationWasKeyboard) clearPointerMoreFocus(getMoreParts().toggle);
366  });
367
368  (function initThemeToggle() {
369    var isDark = document.documentElement.getAttribute('data-theme') === 'dark';
370    var moonEl = document.querySelector('.mn-icon-moon');
371    var sunEl = document.querySelector('.mn-icon-sun');
372    var labelEl = document.querySelector('.mn-theme-label');
373    if (moonEl) moonEl.style.display = isDark ? 'none' : '';
374    if (sunEl) sunEl.style.display = isDark ? '' : 'none';
375    if (labelEl) labelEl.textContent = getThemeLabel(isDark);
376  })();
377
378  window.addEventListener('ss:lang-changed', function () {
379    var labelEl = document.querySelector('.mn-theme-label');
380    if (labelEl) {
381      var isDark = document.documentElement.getAttribute('data-theme') === 'dark';
382      labelEl.textContent = getThemeLabel(isDark);
383    }
384  });
385
386  window.ssSetBadge = function (page, count) {
387    // querySelectorAll, not querySelector: email now carries TWO badges — one in the More
388    // sheet where the link lives, one mirrored on the More tab so unread mail is visible
389    // without opening the sheet. Updating only the first would silently strand the other.
390    var mobileBadges = document.querySelectorAll('[data-mn-badge="' + page + '"]');
391    for (var i = 0; i < mobileBadges.length; i++) {
392      if (count > 0) {
393        mobileBadges[i].textContent = count > 99 ? '99+' : String(count);
394        mobileBadges[i].hidden = false;
395      } else {
396        mobileBadges[i].hidden = true;
397      }
398    }
399
400    var sidebarBadges = document.querySelectorAll('[data-sidebar-badge="' + page + '"]');
401    for (var j = 0; j < sidebarBadges.length; j++) {
402      sidebarBadges[j].hidden = !(count > 0);
403    }
404  };
405
406  window.mnSetBadge = window.ssSetBadge;
407
408  document.addEventListener('keydown', function (e) {
409    var toggle = getMoreParts().toggle;
410    if (toggle) toggle.style.removeProperty('outline');
411    if (e.key === 'Escape' && isMoreOpen()) {
412      lastMoreActivationWasKeyboard = true;
413      setMoreOpen(false, true);
414    }
415  });
416})();

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.