PageSourceSearch

https://www.kunsthauswien.com/assets/js/utilities/navHandler.js

js kunsthauswien.com collected 2026-10-02 07:35:28 UTC 5,372 bytes, 183 lines download raw bytes

1// navHandler.js
2
3import { getFocusableElements, isTab, isShiftTab } from "./focusTrapHandler.js";
4
5
6export class NavHandler {
7  //   static instance = null; // single instance
8
9  constructor(navElement) {
10    // if (NavHandler.instance) return NavHandler.instance; // singleton pattern
11    this.nav = navElement || document.querySelector('.nav');
12    this.button = this.nav?.querySelector('.nav__button--control');
13    this.buttonText = this.nav?.querySelector('.nav__button--control span');
14    this.body = this.nav?.querySelector('.nav__body');
15    this.isOpen = this.nav?.classList.contains('is-expanded') || false;
16    this.language = this.nav.dataset.language == 'de-AT' ? 'de' : 'en';
17
18    if (!this.nav || !this.button || !this.body) {
19      console.warn('NavHandler: required elements missing.');
20      return;
21    }
22
23    // Setup accessibility
24    this.body.id = this.body.id || 'nav-body';
25    this.button.setAttribute('role', 'button');
26    this.button.setAttribute('aria-controls', this.body.id);
27    this.button.setAttribute('aria-expanded', this.isOpen ? 'true' : 'false');
28    if (this.language == 'de') {
29      this.buttonText.textContent = this.isOpen ? 'Menü schließen' : 'Menü öffnen';
30      // this.button.setAttribute('aria-label', this.isOpen ? 'Menü schließen' : 'Menü öffnen');
31      this.buttonText.setAttribute('lang', 'de');
32    } else {
33      this.buttonText.textContent = this.isOpen ? 'Close menu' : 'Open menu';
34      // this.button.setAttribute('aria-label', this.isOpen ? 'Close menu' : 'Open menu');
35      this.buttonText.setAttribute('lang', 'en');
36    }
37    this.body.setAttribute('aria-hidden', this.isOpen ? 'false' : 'true');
38
39    // Set initial height
40    this.body.style.height = this.isOpen ? `${this.body.scrollHeight}px` : '0px';
41    this.body.style.overflow = 'hidden';
42    this.body.style.display = 'none';
43
44    // Bind events
45    this.button.addEventListener('click', () => this.toggle());
46    this.button.addEventListener('keydown', (e) => {
47      if (e.key === 'Enter' || e.key === ' ') {
48        e.preventDefault();
49        this.toggle();
50      }
51    });
52
53    // NavHandler.instance = this;
54  }
55
56  toggle() {
57    this.isOpen ? this.collapse() : this.expand();
58  }
59
60  expand() {
61    if (!this.body) return;
62
63    this.body.style.display = 'flex';
64
65    const fullHeight = this.body.scrollHeight + 'px';
66    this.body.style.height = fullHeight;
67
68    this.nav.classList.add('is-expanded');
69    this.body.setAttribute('aria-hidden', 'false');
70    this.button.setAttribute('aria-expanded', 'true');
71    if (this.language == 'de') {
72      this.buttonText.setAttribute('aria-label', 'Menü schließen');
73    } else {
74      this.buttonText.setAttribute('aria-label', 'Close menu');
75    }
76    this.isOpen = true;
77
78    const transitionDuration =
79      parseFloat(
80        getComputedStyle(document.documentElement).getPropertyValue('--nav-transition-duration')
81      ) || 0.4;
82
83    // Reset height after transition to allow natural resizing
84    setTimeout(() => {
85      if (this.isOpen) this.body.style.height = 'auto';
86    }, transitionDuration * 1000);
87
88
89    this.body.addEventListener('keydown', event => {
90      const focusables = getFocusableElements(this.body).keyboardOnly
91
92      // prepend the toggle button
93      const all = [this.button, ...focusables]
94
95      const first = all[0]
96      const last = all[all.length - 1]
97
98      if (document.activeElement === last && isTab(event)) {
99        event.preventDefault()
100        first.focus()
101      }
102
103      if (document.activeElement === first && isShiftTab(event)) {
104        event.preventDefault()
105        last.focus()
106      }
107    })
108
109
110  }
111
112  collapse() {
113    if (!this.body) return;
114
115    const currentHeight = this.body.scrollHeight + 'px';
116    this.body.style.height = currentHeight;
117
118    // Force reflow to ensure transition
119    void this.body.offsetHeight;
120
121    this.body.style.height = '0px';
122    this.nav.classList.remove('is-expanded');
123    this.body.setAttribute('aria-hidden', 'true');
124    this.button.setAttribute('aria-expanded', 'false');
125    if (this.language == 'de') {
126      this.buttonText.setAttribute('aria-label', 'Menü öffnen');
127    } else {
128      this.buttonText.setAttribute('aria-label', 'Open menu');
129    }
130    this.isOpen = false;
131
132    const transitionDuration =
133      parseFloat(
134        getComputedStyle(document.documentElement).getPropertyValue('--nav-transition-duration')
135      ) || 0.4;
136
137    // Reset height after transition to allow natural resizing
138    setTimeout(() => {
139      this.body.style.display = 'none';
140    }, transitionDuration * 1000);
141  }
142}
143
144/**
145 * Initialize navigation handler
146 * @returns {NavHandler | null}
147 */
148export function initNavHandler() {
149  const navEl = document.querySelector('.nav');
150  if (!navEl) return null;
151
152  const navHandler = new NavHandler(navEl);
153
154  // Close on outside click
155  document.addEventListener('click', (e) => {
156
157    const noCloseSelectors = [
158      '.nav__body',
159      '.nav__button'
160    ];
161
162    if (noCloseSelectors.some(sel => e.target.closest(sel))) {
163      return;
164    }
165
166    // Otherwise close the nav
167    if (navHandler.isOpen) {
168      navHandler.collapse();
169    }
170  });
171
172  // Close when pressing 'esc'
173  window.addEventListener('keydown', e => {
174    if (e.key === 'Escape') {
175      if (navHandler.isOpen) {
176        navHandler.collapse();
177      }
178    }
179  });
180
181
182  return navHandler;
183}

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.